fix(csp): replace inline on* handlers with data-on* + JS wiring
Lint & Test / test (push) Successful in 38s
Lint & Test / test (push) Successful in 38s
The strict `script-src 'self'` CSP blocks inline onclick/onchange/oninput/ onsubmit attribute evaluation, breaking every button and form in the UI. - Rename all 53 inline handler attributes in index.html to data-on* - Add wireInlineHandlers() in app.js that parses each data-on* expression on DOMContentLoaded and attaches a proper addEventListener calling the matching window-global function. Supports no-arg, string/number/bool/null literals, and the `event` token. CSP stays strict; no unsafe-inline or unsafe-hashes needed.
This commit is contained in:
@@ -26,15 +26,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini-controls">
|
<div class="mini-controls">
|
||||||
<button class="mini-control-btn mini-nav-btn" onclick="previousTrack()" data-i18n-title="player.previous" title="Previous">
|
<button class="mini-control-btn mini-nav-btn" data-onclick="previousTrack()" data-i18n-title="player.previous" title="Previous">
|
||||||
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="mini-control-btn" onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause">
|
<button class="mini-control-btn" data-onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause">
|
||||||
<svg viewBox="0 0 24 24" id="mini-play-pause-icon">
|
<svg viewBox="0 0 24 24" id="mini-play-pause-icon">
|
||||||
<path d="M8 5v14l11-7z"/>
|
<path d="M8 5v14l11-7z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="mini-control-btn mini-nav-btn" onclick="nextTrack()" data-i18n-title="player.next" title="Next">
|
<button class="mini-control-btn mini-nav-btn" data-onclick="nextTrack()" data-i18n-title="player.next" title="Next">
|
||||||
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini-volume-container">
|
<div class="mini-volume-container">
|
||||||
<button class="mini-control-btn" onclick="toggleMute()" id="mini-btn-mute" title="Mute">
|
<button class="mini-control-btn" data-onclick="toggleMute()" id="mini-btn-mute" title="Mute">
|
||||||
<svg viewBox="0 0 24 24" id="mini-mute-icon">
|
<svg viewBox="0 0 24 24" id="mini-mute-icon">
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<h2 data-i18n="app.title">Media Server</h2>
|
<h2 data-i18n="app.title">Media Server</h2>
|
||||||
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
|
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
|
||||||
<input type="text" id="token-input" data-i18n-placeholder="auth.placeholder" placeholder="Enter API Token" autocomplete="off">
|
<input type="text" id="token-input" data-i18n-placeholder="auth.placeholder" placeholder="Enter API Token" autocomplete="off">
|
||||||
<button class="btn-connect" onclick="authenticate()" data-i18n="auth.connect">Connect</button>
|
<button class="btn-connect" data-onclick="authenticate()" data-i18n="auth.connect">Connect</button>
|
||||||
<div class="help-text">
|
<div class="help-text">
|
||||||
<p data-i18n="auth.help">To get your token, run:</p>
|
<p data-i18n="auth.help">To get your token, run:</p>
|
||||||
<code>media-server --show-token</code>
|
<code>media-server --show-token</code>
|
||||||
@@ -91,23 +91,23 @@
|
|||||||
<a class="header-btn" href="/docs" target="_blank" title="API Documentation" aria-label="API Documentation">
|
<a class="header-btn" href="/docs" target="_blank" title="API Documentation" aria-label="API Documentation">
|
||||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h5v7h7v9H6zm2-4h8v2H8v-2zm0-3h8v2H8v-2z"/></svg>
|
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h5v7h7v9H6zm2-4h8v2H8v-2zm0-3h8v2H8v-2z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<button class="header-btn" onclick="showAboutDialog()" data-i18n-title="about.button_title" title="About" aria-label="About">
|
<button class="header-btn" data-onclick="showAboutDialog()" data-i18n-title="about.button_title" title="About" aria-label="About">
|
||||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
|
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="accent-picker">
|
<div class="accent-picker">
|
||||||
<button class="header-btn" onclick="toggleAccentPicker()" title="Accent color" aria-label="Accent color">
|
<button class="header-btn" data-onclick="toggleAccentPicker()" title="Accent color" aria-label="Accent color">
|
||||||
<span class="accent-dot" id="accentDot"></span>
|
<span class="accent-dot" id="accentDot"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="accent-picker-dropdown" id="accentDropdown"></div>
|
<div class="accent-picker-dropdown" id="accentDropdown"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="header-btn" onclick="toggleDynamicBackground()" data-i18n-title="player.background" title="Dynamic background" aria-label="Dynamic background" id="bgToggle">
|
<button class="header-btn" data-onclick="toggleDynamicBackground()" data-i18n-title="player.background" title="Dynamic background" aria-label="Dynamic background" id="bgToggle">
|
||||||
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>
|
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="header-btn" onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen" title="Fullscreen player" aria-label="Fullscreen player" id="fullscreenToggle">
|
<button class="header-btn" data-onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen" title="Fullscreen player" aria-label="Fullscreen player" id="fullscreenToggle">
|
||||||
<svg id="fullscreen-icon-enter" viewBox="0 0 24 24"><path fill="currentColor" d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm0 14v-2h3v-3h2v5h-5zM5 14h2v3h3v2H5v-5z"/></svg>
|
<svg id="fullscreen-icon-enter" viewBox="0 0 24 24"><path fill="currentColor" d="M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zm0 14v-2h3v-3h2v5h-5zM5 14h2v3h3v2H5v-5z"/></svg>
|
||||||
<svg id="fullscreen-icon-exit" viewBox="0 0 24 24" style="display:none"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg>
|
<svg id="fullscreen-icon-exit" viewBox="0 0 24 24" style="display:none"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="header-btn" onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" aria-label="Toggle theme" id="theme-toggle">
|
<button class="header-btn" data-onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" aria-label="Toggle theme" id="theme-toggle">
|
||||||
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
|
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
|
||||||
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
|
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -115,12 +115,12 @@
|
|||||||
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
|
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<select id="locale-select" class="header-locale" onchange="changeLocale()" title="Change language">
|
<select id="locale-select" class="header-locale" data-onchange="changeLocale()" title="Change language">
|
||||||
<option value="en">EN</option>
|
<option value="en">EN</option>
|
||||||
<option value="ru">RU</option>
|
<option value="ru">RU</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="header-toolbar-sep"></span>
|
<span class="header-toolbar-sep"></span>
|
||||||
<button class="header-btn header-btn-logout" onclick="clearToken()" data-i18n-title="auth.logout.title" title="Clear saved token" aria-label="Logout">
|
<button class="header-btn header-btn-logout" data-onclick="clearToken()" data-i18n-title="auth.logout.title" title="Clear saved token" aria-label="Logout">
|
||||||
<svg viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,29 +136,29 @@
|
|||||||
<!-- Connection Banner -->
|
<!-- Connection Banner -->
|
||||||
<div class="connection-banner hidden" id="connectionBanner">
|
<div class="connection-banner hidden" id="connectionBanner">
|
||||||
<span id="connectionBannerText"></span>
|
<span id="connectionBannerText"></span>
|
||||||
<button class="connection-banner-btn" id="connectionBannerBtn" onclick="manualReconnect()" style="display: none;" data-i18n="connection.reconnect">Reconnect</button>
|
<button class="connection-banner-btn" id="connectionBannerBtn" data-onclick="manualReconnect()" style="display: none;" data-i18n="connection.reconnect">Reconnect</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Bar (editorial: numbered, italic active) -->
|
<!-- Tab Bar (editorial: numbered, italic active) -->
|
||||||
<div class="tab-bar" id="tabBar" role="tablist">
|
<div class="tab-bar" id="tabBar" role="tablist">
|
||||||
<div class="tab-indicator" id="tabIndicator"></div>
|
<div class="tab-indicator" id="tabIndicator"></div>
|
||||||
<button class="tab-btn active" data-tab="player" onclick="switchTab('player')" role="tab" aria-selected="true" aria-controls="panel-player" tabindex="0">
|
<button class="tab-btn active" data-tab="player" data-onclick="switchTab('player')" role="tab" aria-selected="true" aria-controls="panel-player" tabindex="0">
|
||||||
<span class="tab-num">01</span>
|
<span class="tab-num">01</span>
|
||||||
<span data-i18n="tab.player">Now Spinning</span>
|
<span data-i18n="tab.player">Now Spinning</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="display" onclick="switchTab('display')" role="tab" aria-selected="false" aria-controls="panel-display" tabindex="-1">
|
<button class="tab-btn" data-tab="display" data-onclick="switchTab('display')" role="tab" aria-selected="false" aria-controls="panel-display" tabindex="-1">
|
||||||
<span class="tab-num">02</span>
|
<span class="tab-num">02</span>
|
||||||
<span data-i18n="tab.display">Display</span>
|
<span data-i18n="tab.display">Display</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="browser" onclick="switchTab('browser')" role="tab" aria-selected="false" aria-controls="panel-browser" tabindex="-1">
|
<button class="tab-btn" data-tab="browser" data-onclick="switchTab('browser')" role="tab" aria-selected="false" aria-controls="panel-browser" tabindex="-1">
|
||||||
<span class="tab-num">03</span>
|
<span class="tab-num">03</span>
|
||||||
<span data-i18n="tab.browser">Library</span>
|
<span data-i18n="tab.browser">Library</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="quick-actions" onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1">
|
<button class="tab-btn" data-tab="quick-actions" data-onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1">
|
||||||
<span class="tab-num">04</span>
|
<span class="tab-num">04</span>
|
||||||
<span data-i18n="tab.quick_access">Quick Access</span>
|
<span data-i18n="tab.quick_access">Quick Access</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="settings" onclick="switchTab('settings')" role="tab" aria-selected="false" aria-controls="panel-settings" tabindex="-1">
|
<button class="tab-btn" data-tab="settings" data-onclick="switchTab('settings')" role="tab" aria-selected="false" aria-controls="panel-settings" tabindex="-1">
|
||||||
<span class="tab-num">05</span>
|
<span class="tab-num">05</span>
|
||||||
<span data-i18n="tab.settings">Settings</span>
|
<span data-i18n="tab.settings">Settings</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
<span class="fs-chrome-sep">·</span>
|
<span class="fs-chrome-sep">·</span>
|
||||||
<span class="fs-chrome-kicker" data-i18n="player.kicker">Now Playing</span>
|
<span class="fs-chrome-kicker" data-i18n="player.kicker">Now Playing</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="fs-chrome-exit" onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen.exit" title="Exit fullscreen" aria-label="Exit fullscreen">
|
<button class="fs-chrome-exit" data-onclick="togglePlayerFullscreen()" data-i18n-title="player.fullscreen.exit" title="Exit fullscreen" aria-label="Exit fullscreen">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M8 8H5v2H3V6a1 1 0 0 1 1-1h4v3zm8 0h3v2h2V6a1 1 0 0 0-1-1h-4v3zm0 8h3v-2h2v4a1 1 0 0 1-1 1h-4v-3zM8 16H5v-2H3v4a1 1 0 0 0 1 1h4v-3z"/></svg>
|
||||||
<span data-i18n="player.fullscreen.exit_short">Exit</span>
|
<span data-i18n="player.fullscreen.exit_short">Exit</span>
|
||||||
<kbd class="fs-chrome-kbd">ESC</kbd>
|
<kbd class="fs-chrome-kbd">ESC</kbd>
|
||||||
@@ -267,13 +267,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn-trans" onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
|
<button class="btn-trans" data-onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
|
||||||
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-trans primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
|
<button class="btn-trans primary" data-onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
|
||||||
<svg viewBox="0 0 24 24" id="play-pause-icon"><path d="M8 5v14l11-7z"/></svg>
|
<svg viewBox="0 0 24 24" id="play-pause-icon"><path d="M8 5v14l11-7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-trans" onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
|
<button class="btn-trans" data-onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
|
||||||
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Volume control: mute + slim slider, integrated -->
|
<!-- Volume control: mute + slim slider, integrated -->
|
||||||
<div class="vu-volume">
|
<div class="vu-volume">
|
||||||
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
|
<button class="mute-btn" data-onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
|
||||||
<svg viewBox="0 0 24 24" id="mute-icon">
|
<svg viewBox="0 0 24 24" id="mute-icon">
|
||||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
<!-- Hidden but functional: legacy display + visualizer toggle. -->
|
<!-- Hidden but functional: legacy display + visualizer toggle. -->
|
||||||
<div class="visually-hidden">
|
<div class="visually-hidden">
|
||||||
<div id="volume-display">50%</div>
|
<div id="volume-display">50%</div>
|
||||||
<button onclick="toggleVisualizer()" id="visualizerToggle" data-i18n-title="player.visualizer" title="Audio visualizer" style="display:none">
|
<button data-onclick="toggleVisualizer()" id="visualizerToggle" data-i18n-title="player.visualizer" title="Audio visualizer" style="display:none">
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 18h2v-8H3v8zm4 0h2V6H7v12zm4 0h2V2h-2v16zm4 0h2v-6h-2v6zm4 0h2V9h-2v9z"/></svg>
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 18h2v-8H3v8zm4 0h2V6H7v12zm4 0h2V2h-2v16zm4 0h2v-6h-2v6zm4 0h2V9h-2v9z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -318,34 +318,34 @@
|
|||||||
<div class="browser-toolbar" id="browserToolbar">
|
<div class="browser-toolbar" id="browserToolbar">
|
||||||
<div class="browser-toolbar-left">
|
<div class="browser-toolbar-left">
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button class="view-toggle-btn active" id="viewGridBtn" onclick="setViewMode('grid')" data-i18n-title="browser.view_grid" title="Grid view">
|
<button class="view-toggle-btn active" id="viewGridBtn" data-onclick="setViewMode('grid')" data-i18n-title="browser.view_grid" title="Grid view">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 3h8v8H3V3zm0 10h8v8H3v-8zm10-10h8v8h-8V3zm0 10h8v8h-8v-8z"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 3h8v8H3V3zm0 10h8v8H3v-8zm10-10h8v8h-8V3zm0 10h8v8h-8v-8z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="view-toggle-btn" id="viewCompactBtn" onclick="setViewMode('compact')" data-i18n-title="browser.view_compact" title="Compact view">
|
<button class="view-toggle-btn" id="viewCompactBtn" data-onclick="setViewMode('compact')" data-i18n-title="browser.view_compact" title="Compact view">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M2 2h5v5H2V2zm0 8h5v5H2v-5zm0 8h5v5H2v-5zm7-16h5v5H9V2zm0 8h5v5H9v-5zm0 8h5v5H9v-5zm7-16h5v5h-5V2zm0 8h5v5h-5v-5zm0 8h5v5h-5v-5z"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M2 2h5v5H2V2zm0 8h5v5H2v-5zm0 8h5v5H2v-5zm7-16h5v5H9V2zm0 8h5v5H9v-5zm0 8h5v5H9v-5zm7-16h5v5h-5V2zm0 8h5v5h-5v-5zm0 8h5v5h-5v-5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="view-toggle-btn" id="viewListBtn" onclick="setViewMode('list')" data-i18n-title="browser.view_list" title="List view">
|
<button class="view-toggle-btn" id="viewListBtn" data-onclick="setViewMode('list')" data-i18n-title="browser.view_list" title="List view">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="view-toggle-btn browser-refresh-btn" id="refreshBtn" onclick="refreshBrowser()" data-i18n-title="browser.refresh" title="Refresh">
|
<button class="view-toggle-btn browser-refresh-btn" id="refreshBtn" data-onclick="refreshBrowser()" data-i18n-title="browser.refresh" title="Refresh">
|
||||||
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
|
<button class="browser-play-all-btn" id="playAllBtn" data-onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;">
|
<div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;">
|
||||||
<svg class="browser-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
<svg class="browser-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||||
<input type="text" id="browserSearchInput" class="browser-search-input" data-i18n-placeholder="browser.search" placeholder="Search..." oninput="onBrowserSearch()">
|
<input type="text" id="browserSearchInput" class="browser-search-input" data-i18n-placeholder="browser.search" placeholder="Search..." data-oninput="onBrowserSearch()">
|
||||||
<button class="browser-search-clear" id="browserSearchClear" onclick="clearBrowserSearch()" style="display: none;">
|
<button class="browser-search-clear" id="browserSearchClear" data-onclick="clearBrowserSearch()" style="display: none;">
|
||||||
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="browser-toolbar-right">
|
<div class="browser-toolbar-right">
|
||||||
<label class="items-per-page-label">
|
<label class="items-per-page-label">
|
||||||
<span data-i18n="browser.items_per_page">Items per page:</span>
|
<span data-i18n="browser.items_per_page">Items per page:</span>
|
||||||
<select id="itemsPerPageSelect" onchange="onItemsPerPageChanged()">
|
<select id="itemsPerPageSelect" data-onchange="onItemsPerPageChanged()">
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100" selected>100</option>
|
<option value="100" selected>100</option>
|
||||||
@@ -366,13 +366,13 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" id="browserPagination" style="display: none;">
|
<div class="pagination" id="browserPagination" style="display: none;">
|
||||||
<button id="prevPage" onclick="previousPage()" data-i18n="browser.previous">Previous</button>
|
<button id="prevPage" data-onclick="previousPage()" data-i18n="browser.previous">Previous</button>
|
||||||
<div class="pagination-center">
|
<div class="pagination-center">
|
||||||
<span data-i18n="browser.page">Page</span>
|
<span data-i18n="browser.page">Page</span>
|
||||||
<input type="number" id="pageInput" class="page-input" min="1" value="1" onchange="goToPage()">
|
<input type="number" id="pageInput" class="page-input" min="1" value="1" data-onchange="goToPage()">
|
||||||
<span id="pageTotal">/ 1</span>
|
<span id="pageTotal">/ 1</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="nextPage" onclick="nextPage()" data-i18n="browser.next">Next</button>
|
<button id="nextPage" data-onclick="nextPage()" data-i18n="browser.next">Next</button>
|
||||||
<span class="pagination-showing" id="paginationShowing"></span>
|
<span class="pagination-showing" id="paginationShowing"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
<div class="audio-device-selector">
|
<div class="audio-device-selector">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n="settings.audio.device">Loopback Device</span>
|
<span data-i18n="settings.audio.device">Loopback Device</span>
|
||||||
<select id="audioDeviceSelect" onchange="onAudioDeviceChanged()">
|
<select id="audioDeviceSelect" data-onchange="onAudioDeviceChanged()">
|
||||||
<option value="" data-i18n="settings.audio.auto">Auto-detect</option>
|
<option value="" data-i18n="settings.audio.auto">Auto-detect</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="add-card" onclick="showAddFolderDialog()">
|
<div class="add-card" data-onclick="showAddFolderDialog()">
|
||||||
<span class="add-card-icon">+</span>
|
<span class="add-card-icon">+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -464,7 +464,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="add-card" onclick="showAddScriptDialog()">
|
<div class="add-card" data-onclick="showAddScriptDialog()">
|
||||||
<span class="add-card-icon">+</span>
|
<span class="add-card-icon">+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="add-card" onclick="showAddLinkDialog()">
|
<div class="add-card" data-onclick="showAddLinkDialog()">
|
||||||
<span class="add-card-icon">+</span>
|
<span class="add-card-icon">+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -528,7 +528,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="add-card" onclick="showAddCallbackDialog()">
|
<div class="add-card" data-onclick="showAddCallbackDialog()">
|
||||||
<span class="add-card-icon">+</span>
|
<span class="add-card-icon">+</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -551,7 +551,7 @@
|
|||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="dialogTitle" data-i18n="scripts.dialog.add">Add Script</h3>
|
<h3 id="dialogTitle" data-i18n="scripts.dialog.add">Add Script</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="scriptForm" onsubmit="saveScript(event)">
|
<form id="scriptForm" data-onsubmit="saveScript(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<input type="hidden" id="scriptOriginalName">
|
<input type="hidden" id="scriptOriginalName">
|
||||||
<input type="hidden" id="scriptIsEdit">
|
<input type="hidden" id="scriptIsEdit">
|
||||||
@@ -593,13 +593,13 @@
|
|||||||
<div class="params-section">
|
<div class="params-section">
|
||||||
<div class="params-header">
|
<div class="params-header">
|
||||||
<span data-i18n="scripts.field.parameters">Parameters</span>
|
<span data-i18n="scripts.field.parameters">Parameters</span>
|
||||||
<button type="button" class="btn-small" onclick="addParameterRow()" data-i18n="scripts.params.add">+ Add</button>
|
<button type="button" class="btn-small" data-onclick="addParameterRow()" data-i18n="scripts.params.add">+ Add</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="scriptParamsContainer"></div>
|
<div id="scriptParamsContainer"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeScriptDialog()" data-i18n="scripts.button.cancel">Cancel</button>
|
<button type="button" class="btn-secondary" data-onclick="closeScriptDialog()" data-i18n="scripts.button.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary" data-i18n="scripts.button.save">Save</button>
|
<button type="submit" class="btn-primary" data-i18n="scripts.button.save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -610,12 +610,12 @@
|
|||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="scriptParamsDialogTitle" data-i18n="scripts.params.execute">Execute Script</h3>
|
<h3 id="scriptParamsDialogTitle" data-i18n="scripts.params.execute">Execute Script</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="scriptParamsForm" onsubmit="submitScriptWithParams(event)">
|
<form id="scriptParamsForm" data-onsubmit="submitScriptWithParams(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<div id="scriptParamsInputs"></div>
|
<div id="scriptParamsInputs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeScriptParamsDialog()" data-i18n="scripts.button.cancel">Cancel</button>
|
<button type="button" class="btn-secondary" data-onclick="closeScriptParamsDialog()" data-i18n="scripts.button.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary" data-i18n="scripts.params.execute">Execute</button>
|
<button type="submit" class="btn-primary" data-i18n="scripts.params.execute">Execute</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -626,7 +626,7 @@
|
|||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="callbackDialogTitle" data-i18n="callbacks.dialog.add">Add Callback</h3>
|
<h3 id="callbackDialogTitle" data-i18n="callbacks.dialog.add">Add Callback</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="callbackForm" onsubmit="saveCallback(event)">
|
<form id="callbackForm" data-onsubmit="saveCallback(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<input type="hidden" id="callbackIsEdit">
|
<input type="hidden" id="callbackIsEdit">
|
||||||
|
|
||||||
@@ -664,7 +664,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeCallbackDialog()" data-i18n="callbacks.button.cancel">Cancel</button>
|
<button type="button" class="btn-secondary" data-onclick="closeCallbackDialog()" data-i18n="callbacks.button.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary" data-i18n="callbacks.button.save">Save</button>
|
<button type="submit" class="btn-primary" data-i18n="callbacks.button.save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -675,7 +675,7 @@
|
|||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="linkDialogTitle" data-i18n="links.dialog.add">Add Link</h3>
|
<h3 id="linkDialogTitle" data-i18n="links.dialog.add">Add Link</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="linkForm" onsubmit="saveLink(event)">
|
<form id="linkForm" data-onsubmit="saveLink(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<input type="hidden" id="linkOriginalName">
|
<input type="hidden" id="linkOriginalName">
|
||||||
<input type="hidden" id="linkIsEdit">
|
<input type="hidden" id="linkIsEdit">
|
||||||
@@ -710,7 +710,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeLinkDialog()" data-i18n="links.button.cancel">Cancel</button>
|
<button type="button" class="btn-secondary" data-onclick="closeLinkDialog()" data-i18n="links.button.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary" data-i18n="links.button.save">Save</button>
|
<button type="submit" class="btn-primary" data-i18n="links.button.save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -737,7 +737,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeExecutionDialog()" data-i18n="scripts.execution.close">Close</button>
|
<button type="button" class="btn-secondary" data-onclick="closeExecutionDialog()" data-i18n="scripts.execution.close">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
@@ -746,7 +746,7 @@
|
|||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<h3 id="folderDialogTitle" data-i18n="browser.folder_dialog.title_add">Add Media Folder</h3>
|
<h3 id="folderDialogTitle" data-i18n="browser.folder_dialog.title_add">Add Media Folder</h3>
|
||||||
</div>
|
</div>
|
||||||
<form id="folderForm" onsubmit="saveFolder(event)">
|
<form id="folderForm" data-onsubmit="saveFolder(event)">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<input type="hidden" id="folderIsEdit">
|
<input type="hidden" id="folderIsEdit">
|
||||||
<input type="hidden" id="folderOriginalId">
|
<input type="hidden" id="folderOriginalId">
|
||||||
@@ -773,7 +773,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeFolderDialog()" data-i18n="browser.folder_dialog.cancel">Cancel</button>
|
<button type="button" class="btn-secondary" data-onclick="closeFolderDialog()" data-i18n="browser.folder_dialog.cancel">Cancel</button>
|
||||||
<button type="submit" class="btn-primary" data-i18n="browser.folder_dialog.save">Save</button>
|
<button type="submit" class="btn-primary" data-i18n="browser.folder_dialog.save">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -813,7 +813,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeAboutDialog()" data-i18n="dialog.close">Close</button>
|
<button type="button" class="btn-secondary" data-onclick="closeAboutDialog()" data-i18n="dialog.close">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -159,10 +159,72 @@ HTMLDialogElement.prototype.showModal = function (...args) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CSP-safe replacement for inline on* handlers. HTML uses data-onclick,
|
||||||
|
// data-onchange, data-oninput, data-onsubmit with simple call expressions
|
||||||
|
// like "fn()", "fn('arg')", "fn(event)". We parse those at startup and
|
||||||
|
// attach proper addEventListener calls so script-src 'self' stays strict.
|
||||||
|
const INLINE_HANDLER_EVENTS = {
|
||||||
|
'data-onclick': 'click',
|
||||||
|
'data-onchange': 'change',
|
||||||
|
'data-oninput': 'input',
|
||||||
|
'data-onsubmit': 'submit',
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseInlineHandlerArg(token) {
|
||||||
|
const t = token.trim();
|
||||||
|
if (t === '') return { kind: 'empty' };
|
||||||
|
if (t === 'event') return { kind: 'event' };
|
||||||
|
if (t === 'true') return { kind: 'literal', value: true };
|
||||||
|
if (t === 'false') return { kind: 'literal', value: false };
|
||||||
|
if (t === 'null') return { kind: 'literal', value: null };
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(t)) return { kind: 'literal', value: Number(t) };
|
||||||
|
if ((t.startsWith("'") && t.endsWith("'")) || (t.startsWith('"') && t.endsWith('"'))) {
|
||||||
|
return { kind: 'literal', value: t.slice(1, -1) };
|
||||||
|
}
|
||||||
|
console.warn('inline-handler: unsupported arg token', token);
|
||||||
|
return { kind: 'literal', value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileInlineHandler(expr) {
|
||||||
|
const m = expr.match(/^\s*([A-Za-z_$][\w$]*)\s*\((.*)\)\s*;?\s*$/s);
|
||||||
|
if (!m) {
|
||||||
|
console.warn('inline-handler: unparsable expression', expr);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fnName = m[1];
|
||||||
|
const argsRaw = m[2].trim();
|
||||||
|
const argTokens = argsRaw === '' ? [] : argsRaw.split(',').map(s => s.trim());
|
||||||
|
const parsedArgs = argTokens.map(parseInlineHandlerArg);
|
||||||
|
return function (event) {
|
||||||
|
const fn = window[fnName];
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
console.error('inline-handler: missing global function', fnName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const args = parsedArgs.map(a => a.kind === 'event' ? event : a.value);
|
||||||
|
return fn.apply(this, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireInlineHandlers(root) {
|
||||||
|
for (const [attr, eventName] of Object.entries(INLINE_HANDLER_EVENTS)) {
|
||||||
|
const nodes = root.querySelectorAll(`[${attr}]`);
|
||||||
|
for (const el of nodes) {
|
||||||
|
const expr = el.getAttribute(attr);
|
||||||
|
const handler = compileInlineHandler(expr);
|
||||||
|
if (handler) el.addEventListener(eventName, handler);
|
||||||
|
el.removeAttribute(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
// Cache DOM references
|
// Cache DOM references
|
||||||
cacheDom();
|
cacheDom();
|
||||||
|
|
||||||
|
// Wire CSP-safe inline-handler stand-ins from index.html
|
||||||
|
wireInlineHandlers(document);
|
||||||
|
|
||||||
// Initialize theme and accent color
|
// Initialize theme and accent color
|
||||||
initTheme();
|
initTheme();
|
||||||
initAccentColor();
|
initAccentColor();
|
||||||
|
|||||||
Reference in New Issue
Block a user