From 47e19c1bf2f72121ad825385987c05f6b11f5245 Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Wed, 5 Apr 2023 02:26:54 +0200 Subject: [PATCH] Transform more towards HTML5+CSS+JS --- _includes/blocks/media.liquid | 13 - _sass/_block-figure-player.scss | 186 --------- _sass/_block-figure.scss | 32 +- _sass/_block-table.scss | 52 +++ _sass/_blocks.scss | 8 +- _sass/_content.scss | 1 + _sass/_player.scss | 195 +++++++++ assets/site.js | 707 +++++++++++++++++++------------- 8 files changed, 680 insertions(+), 514 deletions(-) delete mode 100644 _sass/_block-figure-player.scss create mode 100644 _sass/_block-table.scss create mode 100644 _sass/_player.scss diff --git a/_includes/blocks/media.liquid b/_includes/blocks/media.liquid index 3d9318b..ed8ebe1 100644 --- a/_includes/blocks/media.liquid +++ b/_includes/blocks/media.liquid @@ -1,18 +1,5 @@ {% assign file_info = site.static_files | where: "path", include.url %}
- {% if include.player %} -
- -
-
- - 0:00 / 0:00 - - - - -
- {% endif %} {% if include.link %}{% endif %} {% if include.type == "image" %} .top, -figure.block-media > .bottom { - display: block; - margin: 0; - padding: 0.2rem 0.5rem; - z-index: 1; -} - -figure.block-media > .top { - position: absolute; - top: 0; - left: 0; - right: 0; - - display: flex; - padding-bottom: 1rem; - flex-flow: row nowrap; - justify-content: space-between; - align-items: stretch; - align-content: stretch; - gap: 0.5rem; - - background: linear-gradient(to top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 80%); - transition: top ease-in-out 100ms, opacity ease-in-out 100s; - opacity: 1.0; - - overflow: hidden; - user-select: none; -} - -figure.block-media.hide > .top { - top: -100%; - opacity: 0.0; - transition: top ease-in-out 500ms, opacity ease-in-out 500s; -} - -figure.block-media > .top > .variant { - flex-grow: 1; -} - -figure.block-media > .bottom { - position: absolute; - bottom: 0; - left: 0; - right: 0; - - display: flex; - padding-top: 1rem; - flex-flow: row nowrap; - justify-content: space-between; - align-items: stretch; - align-content: stretch; - gap: 0.5rem; - - background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 80%); - transition: top ease-in-out 100ms, opacity ease-in-out 100s; - opacity: 1.0; - - overflow: hidden; - user-select: none; -} - -figure.block-media.hide > .bottom { - bottom: -100%; - opacity: 0.0; - transition: top ease-in-out 500ms, opacity ease-in-out 500s; -} - -figure.block-media > .bottom > .play { - display: block; - flex-grow: 0; - margin: 0; - padding: 0; - width: $media-player-button-size; - height: $media-player-button-size; - - font-size: $media-player-button-size; - line-height: $media-player-button-size; - text-align: center; - user-select: none; -} -figure.block-media > .bottom > .play.playing { -} - -figure.block-media > .bottom > .time { - display: block; - margin: 0; - padding: 0; - width: auto; - height: $media-player-button-size; - flex-grow: 0; - flex-shrink: 0; - flex-basis: auto; - - text-align: right; - line-height: $media-player-button-size; - user-select: auto; -} - -figure.block-media > .bottom > .progress { - margin: 0; - padding: 0; - width: auto; - min-width: 5rem; - height: 100%; - flex-grow: 5; - flex-shrink: 1; - - display: block; - user-select: none; -} -figure.block-media > .bottom > .mute { - display: block; - margin: 0; - padding: 0; - width: $media-player-button-size; - height: $media-player-button-size; - flex-grow: 0; - flex-shrink: 0; - flex-basis: auto; - - font-size: $media-player-button-size; - line-height: $media-player-button-size; - text-align: center; - user-select: none; -} -figure.block-media > .bottom > .mute.muted { -} - -figure.block-media > .bottom > .volume { - display: block; - margin: 0; - padding: 0; - width: auto; - min-width: 3rem; - max-width: 6rem; - height: 100%; - flex-grow: 1; - flex-shrink: 5; - flex-basis: auto; - - user-select: none; -} - -figure.block-media > .bottom > .fullscreen { - display: block; - margin: 0; - padding: 0; - width: $media-player-button-size; - height: $media-player-button-size; - flex-grow: 0; - flex-shrink: 0; - flex-basis: auto; - - font-size: $media-player-button-size; - line-height: $media-player-button-size; - text-align: center; - user-select: none; -} - -@media (max-width: 500px) { - figure.block-media > .bottom > .time { - display: none; - } -} -@media (max-width: 350px) { - figure.block-media > .bottom > .volume { - display: none; - } -} -@media (max-width: 250px) { - figure.block-media > .bottom > .play, - figure.block-media > .bottom > .mute, - figure.block-media > .bottom > .fullscreen { - display: none; - } -} diff --git a/_sass/_block-figure.scss b/_sass/_block-figure.scss index 415ce50..e65b1a0 100644 --- a/_sass/_block-figure.scss +++ b/_sass/_block-figure.scss @@ -7,7 +7,7 @@ figure.block { overflow: hidden; } -figure.block>figcaption { +figure.block figcaption { display: block; margin: 0; padding: .25em .5em; @@ -19,10 +19,10 @@ figure.block>figcaption { // -------------------------------------------------------------------------------- // Block: Figure > Media // -------------------------------------------------------------------------------- -figure.block>picture, -figure.block>img, -figure.block>video, -figure.block>audio { +figure.block picture, +figure.block img, +figure.block video, +figure.block audio { display: block; width: 100%; height: auto; @@ -33,12 +33,10 @@ figure.block>audio { background: rgba(0, 0, 0, 0.5); } -@import "_block-figure-player.scss"; - // -------------------------------------------------------------------------------- // Block: Figure > Table // -------------------------------------------------------------------------------- -figure.block>table { +figure.block table { margin: 0; padding: 0; display: table; @@ -47,27 +45,27 @@ figure.block>table { border-collapse: collapse; } -figure.block>table>thead>tr, -figure.block>table>tbody>tr { +figure.block table>thead>tr, +figure.block table>tbody>tr { margin: 0; padding: 0; vertical-align: top; } -figure.block>table>thead>tr { +figure.block table>thead>tr { background: $theme-article-table-head-background; } -figure.block>table>tbody>tr { +figure.block table>tbody>tr { background: $theme-article-table-body-background; } -figure.block>table>tbody>tr:nth-child(2n) { +figure.block table>tbody>tr:nth-child(2n) { background: $theme-article-table-body-background-alt; } -figure.block>table>thead>tr>th, -figure.block>table>tbody>tr>th { +figure.block table>thead>tr>th, +figure.block table>tbody>tr>th { margin: 0; padding: 0.75rem 1.5rem; font-weight: bold; @@ -79,8 +77,8 @@ figure.block>table>tbody>tr>th { border-bottom: none; } -figure.block>table>thead>tr>td, -figure.block>table>tbody>tr>td { +figure.block table>thead>tr>td, +figure.block table>tbody>tr>td { margin: 0; padding: 0.5rem 0.75rem; border: 1px solid rgba(0, 0, 0, 0.5); diff --git a/_sass/_block-table.scss b/_sass/_block-table.scss new file mode 100644 index 0000000..31fdf1c --- /dev/null +++ b/_sass/_block-table.scss @@ -0,0 +1,52 @@ +// -------------------------------------------------------------------------------- +// Block: Table +// -------------------------------------------------------------------------------- +table.block { + margin: 0; + padding: 0; + display: table; + width: 100%; + table-layout: auto; + border-collapse: collapse; +} + +table.block>thead>tr, +table.block>tbody>tr { + margin: 0; + padding: 0; + vertical-align: top; +} + +table.block>thead>tr { + background: $theme-article-table-head-background; +} + +table.block>tbody>tr { + background: $theme-article-table-body-background; +} + +table.block>tbody>tr:nth-child(2n) { + background: $theme-article-table-body-background-alt; +} + +table.block>thead>tr>th, +table.block>tbody>tr>th { + margin: 0; + padding: 0.75rem 1.5rem; + font-weight: bold; + font-variant: small-caps; + text-align: start; + vertical-align: top; + border: 1px solid rgba(0, 0, 0, 0.5); + border-top: none; + border-bottom: none; +} + +table.block>thead>tr>td, +table.block>tbody>tr>td { + margin: 0; + padding: 0.5rem 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.5); + border-top: none; + border-bottom: none; +} diff --git a/_sass/_blocks.scss b/_sass/_blocks.scss index 80a4fc4..846f578 100644 --- a/_sass/_blocks.scss +++ b/_sass/_blocks.scss @@ -3,7 +3,7 @@ // -------------------------------------------------------------------------------- .block { padding: 0; - margin: 0; + margin: 0 auto; overflow: auto; text-align: start; } @@ -12,11 +12,6 @@ margin-top: 1.25em; } -.block > .content { - padding: 0; - margin: 0; -} - // -------------------------------------------------------------------------------- // Float // -------------------------------------------------------------------------------- @@ -84,3 +79,4 @@ @import "_block-figure.scss"; @import "_block-heading.scss"; @import "_block-list.scss"; +@import "_block-table.scss"; diff --git a/_sass/_content.scss b/_sass/_content.scss index 0e7bfdf..4671ecf 100644 --- a/_sass/_content.scss +++ b/_sass/_content.scss @@ -8,6 +8,7 @@ body > .content { @import "_breadcrumbs.scss"; @import "_article.scss"; +@import "_player.scss"; @import "_comments.scss"; @import "_index.scss"; @import "_pagination.scss"; diff --git a/_sass/_player.scss b/_sass/_player.scss new file mode 100644 index 0000000..94da97a --- /dev/null +++ b/_sass/_player.scss @@ -0,0 +1,195 @@ +// -------------------------------------------------------------------------------- +// Media Player +// -------------------------------------------------------------------------------- +$media-player-button-size: 1.5rem; + +player { + overlay { + display: block; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + z-index: 1; + + background: transparent; + + controls { + transition: bottom ease-in-out 100ms, opacity ease-in-out 100ms; + display: flex; + margin: 0; + padding: 0.5rem 0.5rem; + padding-top: 1rem; + flex-flow: row nowrap; + justify-content: space-between; + align-items: stretch; + align-content: stretch; + gap: 0.5rem; + + position: absolute; + bottom: 0; + left: 0; + right: 0; + + background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 80%); + opacity: 1.0; + + overflow: hidden; + user-select: none; + + .play { + display: block; + flex-grow: 0; + margin: 0; + padding: 0; + width: $media-player-button-size; + height: $media-player-button-size; + + font-size: $media-player-button-size; + line-height: $media-player-button-size; + text-align: center; + user-select: none; + } + + .time { + display: block; + margin: 0; + padding: 0; + width: auto; + height: $media-player-button-size; + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + + text-align: right; + line-height: $media-player-button-size; + user-select: auto; + } + + .progress { + margin: 0; + padding: 0; + width: auto; + min-width: 5rem; + height: 100%; + flex-grow: 5; + flex-shrink: 1; + + display: block; + user-select: none; + } + + .mute { + display: block; + margin: 0; + padding: 0; + width: $media-player-button-size; + height: $media-player-button-size; + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + + font-size: $media-player-button-size; + line-height: $media-player-button-size; + text-align: center; + user-select: none; + } + + .volume { + display: block; + margin: 0; + padding: 0; + width: auto; + min-width: 3rem; + max-width: 6rem; + height: 100%; + flex-grow: 1; + flex-shrink: 5; + flex-basis: auto; + + user-select: none; + } + + .fullscreen { + display: block; + margin: 0; + padding: 0; + width: $media-player-button-size; + height: $media-player-button-size; + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + + font-size: $media-player-button-size; + line-height: $media-player-button-size; + text-align: center; + user-select: none; + } + } + } +} + +player.hide { + cursor: none; + + overlay { + controls { + bottom: -100%; + opacity: 0.0; + transition: bottom ease-in-out 500ms, opacity ease-in-out 500ms; + } + } +} + +player>.top { + position: absolute; + top: 0; + left: 0; + right: 0; + + display: flex; + padding-bottom: 1rem; + flex-flow: row nowrap; + justify-content: space-between; + align-items: stretch; + align-content: stretch; + gap: 0.5rem; + + background: linear-gradient(to top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 80%); + transition: top ease-in-out 100ms, opacity ease-in-out 100s; + opacity: 1.0; + + overflow: hidden; + user-select: none; +} + +player overlay.hide>.top { + top: -100%; + opacity: 0.0; + transition: top ease-in-out 500ms, opacity ease-in-out 500s; +} + +player overlay .top>.variant { + flex-grow: 1; +} + +@media (max-width: 500px) { + player overlay controls .time { + display: none; + } +} + +@media (max-width: 350px) { + player overlay controls .volume { + display: none; + } +} + +@media (max-width: 250px) { + + player overlay controls .play, + player overlay controls .mute, + player overlay controls .fullscreen { + display: none; + } +} diff --git a/assets/site.js b/assets/site.js index 36a60b0..d0e2608 100644 --- a/assets/site.js +++ b/assets/site.js @@ -11,7 +11,7 @@ async function xmr_media_lazyload_perform(element) { async function xmr_media_lazyload_initialize() { // Figure out what to load. - let elements = document.querySelectorAll(".block-media > [data-lazyload]"); + let elements = document.querySelectorAll(".block-this._media > [data-lazyload]"); let stack = Array.from(elements); console.debug("Lazily loading " + stack.length + " elements..."); @@ -21,57 +21,60 @@ async function xmr_media_lazyload_initialize() { } } -async function xmr_initialize_player(el) { - let media = el.querySelector("video"); - if (!media) - el.querySelector("audio"); - - let play_pause = el.querySelector(".play"); - { // Play/Pause - play_pause.update = function() { - if (media.paused) { - play_pause.classList.remove("playing"); - play_pause.dataset.symbol = "⏵"; - play_pause.ariaLabel = "Play"; - } else if (media.error || media.ended) { - play_pause.classList.remove("playing"); - play_pause.dataset.symbol = "⏹"; - play_pause.ariaLabel = "Stopped"; +class XMediaPlayer { + constructor(element) { + this._media = element; + this._variants = new Map(); + for (let source of this._media.querySelectorAll("source")) { + if (source.title) { + this._variants.set(source.url, source.title); } else { - play_pause.classList.add("playing"); - play_pause.dataset.symbol = "⏸"; - play_pause.ariaLabel = "Pause"; + this._variants.set(source.url, (new URL(source.url)).pathname); } - play_pause.innerText = play_pause.dataset.symbol; } - play_pause.addEventListener("click", () => { - if (media.paused) { - media.play(); - } else { - media.pause(); - } - }); - play_pause.addEventListener("keydown", (ev) => { - if (ev.isComposing || ev.keyCode === 229) { - // Ignore IME compositing. - return; - } - if (["Space", "Enter"].includes(ev.code)) { - play_pause.click(); - ev.preventDefault(); - ev.stopPropagation(); - } - }); - media.addEventListener("play", () => { play_pause.update(); }); - media.addEventListener("pause", () => { play_pause.update(); }); - media.addEventListener("ended", () => { play_pause.update(); }); - media.addEventListener("error", () => { play_pause.update(); }); - play_pause.update(); - } - let playback_time = el.querySelector(".time"); - { // Time - playback_time.update = function() { + // Replace the original video element with a player. + this._container = document.createElement("player"); + this._media.parentElement.replaceChild(this._container, this._media); + + // Insert the original video element. This must be done here, else we have Z-fighting on mobile! + this._container.appendChild(this._media); + + // Insert the container for our overlays. + this._overlay = document.createElement("overlay"); + this._container.appendChild(this._overlay); + + // Build the control overlay. + this._controls = document.createElement("controls"); + this._overlay.appendChild(this._controls); + // - Play Button + this._controlPlay = document.createElement("span"); + this._controlPlay.classList.add("play", "symbol") + this._controlPlay.tabIndex = 0; + this._controlPlay.dataset.symbol = "⏵"; + this._controlPlay.ariaLabel = "Play"; + this._controlPlay.update = () => { + if (this._media.paused) { + this._controlPlay.classList.remove("playing"); + this._controlPlay.dataset.symbol = "⏵"; + this._controlPlay.ariaLabel = "Play"; + } else if (this._media.error || this._media.ended) { + this._controlPlay.classList.remove("playing"); + this._controlPlay.dataset.symbol = "⏹"; + this._controlPlay.ariaLabel = "Stopped"; + } else { + this._controlPlay.classList.add("playing"); + this._controlPlay.dataset.symbol = "⏸"; + this._controlPlay.ariaLabel = "Pause"; + } + this._controlPlay.innerText = this._controlPlay.dataset.symbol; + } + this._controls.appendChild(this._controlPlay); + // - Time Display + this._controlTime = document.createElement("span"); + this._controlTime.classList.add("time"); + this._controlTime.innerHTML = "00:00 / 00:00"; + this._controlTime.update = () => { function formatSeconds(seconds, with_ms, with_minutes, with_hour) { let result = []; if (isFinite(seconds) && !isNaN(seconds)) { @@ -97,170 +100,386 @@ async function xmr_initialize_player(el) { return result; } - let duration = formatSeconds(media.duration, true); - let current = formatSeconds(media.currentTime, true, duration.length >= 2, duration.length >= 3); + let duration = formatSeconds(this._media.duration, true); + let current = formatSeconds(this._media.currentTime, true, duration.length >= 2, duration.length >= 3); for (let idx in duration) { current[idx].padStart(duration[idx].length, '0'); } if (duration.length > 0) { - playback_time.innerText = `${current.join(':')} / ${duration.join(':')}`; + this._controlTime.innerText = `${current.join(':')} / ${duration.join(':')}`; } else { - playback_time.innerText = `${current.join(':')}`; + this._controlTime.innerText = `${current.join(':')}`; } } - media.addEventListener("timeupdate", () => { playback_time.update(); }); - media.addEventListener("durationupdate", () => { playback_progress.update(); }); - media.addEventListener("loadedmetadata", () => { playback_time.update(); }); - playback_time.update(); - } - - let playback_progress = el.querySelector(".progress"); - { // Progress - playback_progress.update = function() { - playback_progress.disabled = !media.seekable; - playback_progress.min = 0; - playback_progress.max = media.duration * 100; - playback_progress.value = media.currentTime * 100; + this._controls.appendChild(this._controlTime); + // - Progress Bar + this._controlProgress = document.createElement("input"); + this._controlProgress.classList.add("progress"); + this._controlProgress.type = "range"; + this._controlProgress.tabIndex = 0; + this._controlProgress.min = -2147483648; + this._controlProgress.max = 2147483647; + this._controlProgress.update = () => { + let min = parseInt(this._controlVolume.min, 10); + let max = parseInt(this._controlVolume.max, 10); + let dlt = max - min; + this._controlProgress.disabled = !this._media.seekable; + this._controlProgress.value = (this._media.currentTime / this._media.duration) * dlt + min; } - playback_progress.addEventListener("input", () => { - if (isFinite(media.duration)) { - media.currentTime = parseInt(playback_progress.value, 10) / 100; - } - }); - media.addEventListener("timeupdate", () => { playback_progress.update(); }); - media.addEventListener("durationupdate", () => { playback_progress.update(); }); - media.addEventListener("loadeddata", () => { playback_progress.update(); }); - media.addEventListener("loadedmetadata", () => { playback_progress.update(); }); - playback_progress.update(); - } - - let audio_mute = el.querySelector(".mute"); - { // Mute - audio_mute.update = function() { - if (media.muted) { - audio_mute.classList.add("muted"); - audio_mute.dataset.symbol = "🔇"; - audio_mute.ariaLabel = "Unmute"; + this._controls.appendChild(this._controlProgress); + // - Mute Button + this._controlMute = document.createElement("span"); + this._controlMute.classList.add("mute", "symbol"); + this._controlMute.tabIndex = 0; + this._controlMute.ariaLabel = "Mute"; + this._controlMute.dataset.symbol = "🔊"; + this._controlMute.update = () => { + if (this._media.muted) { + this._controlMute.classList.add("muted"); + this._controlMute.dataset.symbol = "🔇"; + this._controlMute.ariaLabel = "Unmute"; } else { - audio_mute.classList.remove("muted"); - if (media.volume > 0.5) { - audio_mute.dataset.symbol = "🔊"; - } else if (media.volume > 0.25) { - audio_mute.dataset.symbol = "🔉"; + this._controlMute.classList.remove("muted"); + if (this._media.volume > 0.5) { + this._controlMute.dataset.symbol = "🔊"; + } else if (this._media.volume > 0.25) { + this._controlMute.dataset.symbol = "🔉"; } else { - audio_mute.dataset.symbol = "🔈"; + this._controlMute.dataset.symbol = "🔈"; } - audio_mute.ariaLabel = `Volume: ${(media.volume * 100).toString(10)}%`; + this._controlMute.ariaLabel = `Volume: ${(this._media.volume * 100).toString(10)}%`; } - audio_mute.innerText = audio_mute.dataset.symbol; + this._controlMute.innerText = this._controlMute.dataset.symbol; } - audio_mute.addEventListener("click", () => { - media.muted = !media.muted; - }); - audio_mute.addEventListener("keydown", (ev) => { - if (ev.isComposing || ev.keyCode === 229) { - // Ignore IME compositing. - return; - } - if (["Space", "Enter"].includes(ev.code)) { - audio_mute.click(); - ev.preventDefault(); - ev.stopPropagation(); - } - }); - media.addEventListener("volumechange", () => { - audio_mute.update(); - }); - audio_mute.update(); - } - - let audio_volume = el.querySelector(".volume"); - { // Volume - audio_volume.min = 0; - audio_volume.max = 2147483647; - audio_volume.update = function() { - let min = parseInt(audio_volume.min, 10); - let max = parseInt(audio_volume.max, 10); + this._controls.appendChild(this._controlMute); + // - Volume Controls + this._controlVolume = document.createElement("input"); + this._controlVolume.classList.add("volume"); + this._controlVolume.type = "range"; + this._controlVolume.tabIndex = 0; + this._controlVolume.min = -2147483648; + this._controlVolume.max = 2147483647; + this._controlVolume.value = this._controlVolume.max; + this._controlVolume.update = () => { + let min = parseInt(this._controlVolume.min, 10); + let max = parseInt(this._controlVolume.max, 10); let dlt = max - min; - audio_volume.value = (media.logVolume * dlt) + min; - audio_volume.ariaLabel = `Volume: ${(media.volume * 100).toString(10)}%`; - if (media.muted) { - audio_volume.classList.add("muted"); + this._controlVolume.value = (this._media.volume * dlt) + min; + this._controlVolume.ariaLabel = `Volume: ${(this._media.volume * 100).toString(10)}%`; + if (this._media.muted) { + this._controlVolume.classList.add("muted"); } else { - audio_volume.classList.remove("muted"); + this._controlVolume.classList.remove("muted"); } } - audio_volume.addEventListener("input", () => { - let min = parseInt(audio_volume.min, 10); - let max = parseInt(audio_volume.max, 10); - let val = parseInt(audio_volume.value, 10); - let dlt = max - min; - let vol = (val + min) / dlt; - media.logVolume = vol; - media.muted = (media.volume < 0.01); - }); - media.addEventListener("volumechange", () => { audio_volume.update(); }); - audio_volume.update(); - } - - let fullscreen = el.querySelector(".fullscreen"); - { // Fullscreen - fullscreen.update = function() { + this._controls.appendChild(this._controlVolume); + // - Fullscreen Button + this._controlFullscreen = document.createElement("span"); + this._controlFullscreen.classList.add("fullscreen", "symbol"); + this._controlFullscreen.tabIndex = 0; + this._controlFullscreen.ariaLabel = "Toggle Fullscreen"; + this._controlFullscreen.dataset.symbol = "⛶"; + this._controlFullscreen.update = () => { if (document.fullscreenElement) { - fullscreen.dataset.symbol = "\u200F⛶"; + this._controlFullscreen.dataset.symbol = "\u200F⛶"; } else { - fullscreen.dataset.symbol = "⛶"; + this._controlFullscreen.dataset.symbol = "⛶"; } - fullscreen.innerText = fullscreen.dataset.symbol; + this._controlFullscreen.innerText = this._controlFullscreen.dataset.symbol; }; - fullscreen.addEventListener("click", () => { - if (document.fullscreenElement) { - document.exitFullscreen(); - } else { - el.requestFullscreen(); - } - }); - fullscreen.addEventListener("keydown", (ev) => { - if (ev.isComposing || ev.keyCode === 229) { - // Ignore IME compositing. - return; - } - if (["Space", "Enter"].includes(ev.code)) { - fullscreen.click(); - ev.preventDefault(); - } - }) - el.addEventListener("fullscreenchange", () => { - fullscreen.update(); - }); - fullscreen.update(); + this._controls.appendChild(this._controlFullscreen); + + // Now begin listening to all events that are relevant. + for (let event of ["Abort", "CanPlay", "CanPlayThrough", "DurationChange", "Emptied", "Encrypted", "Ended", "Error", "LoadedData", "LoadedMetaData", "LoadStart", "Pause", "Play", "Playing", "Progress", "RateChange", "Seeked", "Seeking", "Stalled", "Suspend", "TimeUpdate", "VolumeChange", "Waiting", "DblClick", "Click", "KeyDown"]) { + this._media.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["FullscreenChange", "FullscreenError", "KeyDown", "MouseEnter", "MouseLeave", "MouseMove", "Wheel"]) { + this._container.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["Click", "KeyDown"]) { + this._controlPlay.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["Input", "KeyDown", "Wheel"]) { + this._controlProgress.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["Click", "KeyDown"]) { + this._controlMute.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["Input", "KeyDown", "Wheel"]) { + this._controlVolume.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + for (let event of ["Click", "KeyDown"]) { + this._controlFullscreen.addEventListener(event.toLowerCase(), (ev) => { this[`_on${event}`](ev); }); + } + + // And update all controls once. + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + this._controlMute.update(); + this._controlVolume.update(); + this._controlFullscreen.update(); + + // And tell the media element to load. + this._media.load(); } + _hideAudioControlWithoutAudio() { + let hasAudio = Boolean(this._media.mozHasAudio) + || Boolean(this._media.webkitAudioDecodedByteCount) + || Boolean(this._media.audioTracks && this._media.audioTracks.length); + this._controlMute.style.display = hasAudio ? "" : "none"; + this._controlVolume.style.display = hasAudio ? "" : "none"; + } + + // Media Playback Status + _onAbort(ev) { + this.showOverlay(); + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onEnded(ev) { + this.showOverlay(); + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onError(ev) { + this.showOverlay(); + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onPlay(ev) { + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onPlaying(ev) { + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onPause(ev) { + this.showOverlay(); + this._controlPlay.update(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onProgress(ev) { + this._controlTime.update(); + this._controlProgress.update(); + } + _onRateChange(ev) { + this._controlPlay.update(); + } + _onTimeUpdate(ev) { + this._controlTime.update(); + this._controlProgress.update(); + } + _onVolumeChange(ev) { + this._controlMute.update(); + this._controlVolume.update(); + } + + // Media Stream Status + _onCanPlay(ev) { } + _onCanPlayThrough(ev) { } + _onDurationChange(ev) { + this._controlTime.update(); + this._controlProgress.update(); + } + _onEmptied(ev) { } + _onEncrypted(ev) { } + _onLoadedData(ev) { + this._hideAudioControlWithoutAudio(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onLoadedMetaData(ev) { + this._hideAudioControlWithoutAudio(); + this._controlTime.update(); + this._controlProgress.update(); + } + _onLoadStart(ev) { } + _onSeeked(ev) { + this._controlTime.update(); + this._controlProgress.update(); + } + _onSeeking(ev) { } + _onSuspend(ev) { } + _onWaiting(ev) { } + + // User Input + _onClick(ev) { + if ((ev.target == this._controlPlay) || (ev.target == this._media)) { + this.togglePlayPause(); + } else if (ev.target == this._controlMute) { + this.toggleMute(); + } else if (ev.target == this._controlFullscreen) { + this.toggleFullscreen(); + } else { + // If we can't handle this, let it fall to the next handler. + return + } + + // Prevent default behavior and stop propagation. + this.showOverlay(); + ev.preventDefault(); + ev.stopPropagation(); + } + _onDblClick(ev) { + this._controlFullscreen.click(); + this.showOverlay(); + ev.preventDefault(); + ev.stopPropagation(); + } + _onMouseEnter(ev) { } + _onMouseMove(ev) { + this.showOverlay(); + } + _onMouseLeave(ev) { } + _onKeyDown(ev) { + // Ignore IME compositing. + if (ev.isComposing || ev.keyCode === 229) { + return; + } + + if (["Space", "Enter"].includes(ev.code)) { + // Most likely an attempt to simulate a click. + ev.target.click(); + } else if (["f", "F"].includes(ev.key)) { + this._controlFullscreen.click(); + } else if (["m", "M"].includes(ev.key)) { + this._controlMute.click(); + } else if (ev.code === "Home") { + this._media.currentTime = 0; + } else if (ev.code === "ArrowLeft") { + this._media.currentTime -= 5; + } else if (ev.code === "Comma") { + this._media.currentTime -= .1; + } else if (ev.code === "Period") { + this._media.currentTime += .1; + } else if (ev.code === "ArrowRight") { + this._media.currentTime += 5; + } else if (ev.code === "End") { + this._media.currentTime = this._media.duration; + } else if (ev.code === "ArrowUp") { + this._media.volume = Math.max(0, Math.min(this._media.volume + 0.05, 1.0)); + } else if (ev.code === "ArrowDown") { + this._media.volume = Math.max(0, Math.min(this._media.volume - 0.05, 1.0)); + } else if (["p", "P"].includes(ev.key)) { + this._media.preservesPitch = !this._media.preservesPitch; + } else if (ev.code === "PageDown") { + this._media.playbackRate = Math.max(0.25, Math.min(this._media.playbackRate - 0.05, 4.0)); + } else if (ev.code === "PageUp") { + this._media.playbackRate = Math.max(0.25, Math.min(this._media.playbackRate + 0.05, 4.0)); + } else if (["r", "R"].includes(ev.key)) { + this._media.playbackRate = 1.0; + } else { + // If we can't handle this, let it fall to the next handler. + return; + } + + // Prevent default behavior and stop propagation. + this.showOverlay(); + ev.preventDefault(); + ev.stopPropagation(); + } + _onInput(ev) { + if (ev.target == this._controlProgress) { + ev.preventDefault(); + if (isFinite(this._media.duration)) { + let min = parseInt(this._controlProgress.min, 10); + let max = parseInt(this._controlProgress.max, 10); + let val = parseInt(this._controlProgress.value, 10); + let dlt = max - min; + let time = (val - min) / dlt; + this._media.currentTime = time * this._media.duration; + } + } else if (ev.target == this._controlVolume) { + let min = parseInt(this._controlVolume.min, 10); + let max = parseInt(this._controlVolume.max, 10); + let val = parseInt(this._controlVolume.value, 10); + let dlt = max - min; + let vol = (val - min) / dlt; + this._media.volume = vol; + this._media.muted = (this._media.volume < 0.01); + } + } + _onChange(ev) { } + + // Control Functions + togglePlayPause() { + if (this._media.paused) { + this._media.play(); + } else { + this._media.pause(); + } + } + + toggleMute() { + this._media.muted = !this._media.muted; + } + + toggleFullscreen() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + this._container.requestFullscreen(); + } + } + + showOverlay() { + if (this._overlayTimeout) { + clearTimeout(this._overlayTimeout); + } + + this._container.classList.remove("hide"); + this._overlayTimeout = setTimeout(() => { this.hideOverlay(); }, 2500); + } + + hideOverlay() { + if (this._overlayTimeout) { + clearTimeout(this._overlayTimeout); + } + + if (!this._media.paused && !this._media.ended && !this._media.error) { + this._container.classList.add("hide"); + } + } + +} + +/* +async function xmr_initialize_player(el) { + let variant = el.querySelector(".variant"); { // Variants - variant.update = function() { + variant.update = function () { // Store current state and pause. - let paused = media.paused; - let muted = media.muted; - let volume = media.volume; - let time = media.currentTime; - media.pause(); + let paused = this._media.paused; + let muted = this._media.muted; + let volume = this._media.volume; + let time = this._media.currentTime; + this._media.pause(); play_pause.update(); - // Tell the media source to begin loading. - media.src = variant.value; - media.load(); - media.currentTime = time; + // Tell the this._media source to begin loading. + this._media.src = variant.value; + this._media.load(); + this._media.currentTime = time; - media.addEventListener("canplay", () => { - media.currentTime = time; - media.volume = volume; - media.muted = muted; + this._media.addEventListener("canplay", () => { + this._media.currentTime = time; + this._media.volume = volume; + this._media.muted = muted; if (paused) { - media.pause(); + this._media.pause(); } else { - media.play(); + this._media.play(); } play_pause.update(); @@ -279,110 +498,28 @@ async function xmr_initialize_player(el) { option.textContent = k.title; variant.appendChild(option); } - variant.value = media.currentSrc; - } - - { // Keyboard/Mouse Controls - el.addEventListener("keydown", (ev) => { - if (ev.isComposing || ev.keyCode === 229) { - // Ignore IME compositing. - return; - } - - if (["f", "F"].includes(ev.key)) { - fullscreen.click(); - ev.preventDefault(); - ev.stopPropagation(); - } else if (["m", "M"].includes(ev.key)) { - audio_mute.click(); - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "Space") { - play_pause.click(); - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "Home") { - media.currentTime = 0; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "ArrowLeft") { - media.currentTime -= 5; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "Comma") { - media.currentTime -= .1; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "Period") { - media.currentTime += .1; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "ArrowRight") { - media.currentTime += 5; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "End") { - media.currentTime = media.duration; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "ArrowUp") { - media.logVolume = Math.max(0, Math.min(media.logVolume + 0.05, 1.0)); - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "ArrowDown") { - media.logVolume = Math.max(0, Math.min(media.logVolume - 0.05, 1.0)); - ev.preventDefault(); - ev.stopPropagation(); - } else if (["p", "P"].includes(ev.key)) { - media.preservesPitch = !media.preservesPitch; - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "PageDown") { - media.playbackRate = Math.max(0.25, Math.min(media.playbackRate - 0.05, 4.0)); - ev.preventDefault(); - ev.stopPropagation(); - } else if (ev.code === "PageUp") { - media.playbackRate = Math.max(0.25, Math.min(media.playbackRate + 0.05, 4.0)); - ev.preventDefault(); - ev.stopPropagation(); - } else if (["r", "R"].includes(ev.key)) { - media.playbackRate = 1.0; - ev.preventDefault(); - ev.stopPropagation(); - } - }); - - media.addEventListener("click", () => { - play_pause.click(); - }); - - media.addEventListener("dblclick", () => { - fullscreen.click(); - }) + variant.value = this._media.currentSrc; } { // Hide/Show controls - el.showOverlay = function() { + el.showOverlay = function () { el.cancelHideOverlay(); el.classList.remove("hide"); } - el.hideOverlay = function() { - if (!media.paused && !media.ended && !media.error) { - el.classList.add("hide"); - } + el.hideOverlay = function () { } - el.delayHideOverlay = function() { + el.delayHideOverlay = function () { el.cancelHideOverlay(); el.timer = setTimeout(() => { el.hideOverlay(); }, 2500); } - el.cancelHideOverlay = function() { + el.cancelHideOverlay = function () { if (el.timer) { clearTimeout(el.timer); } } el.addEventListener("mousemove", () => { el.showOverlay(); - if (!media.paused && !media.ended && !media.error) { + if (!this._media.paused && !this._media.ended && !this._media.error) { el.delayHideOverlay(); } }); @@ -391,34 +528,18 @@ async function xmr_initialize_player(el) { { // Show/Hide Audio controls without audio. function checkAudioPresence() { - let hasAudio = Boolean(media.mozHasAudio) - || Boolean(media.webkitAudioDecodedByteCount) - || Boolean(media.audioTracks && media.audioTracks.length); - audio_mute.style.display = hasAudio ? "" : "none"; - audio_volume.style.display = hasAudio ? "" : "none"; } - media.addEventListener("loadedmetadata", () => { checkAudioPresence() }); - media.addEventListener("loadeddata", () => { checkAudioPresence() }); + this._media.addEventListener("loadedmetadata", () => { checkAudioPresence() }); + this._media.addEventListener("loadeddata", () => { checkAudioPresence() }); checkAudioPresence(); } // Signal the browser to try and load some information. - media.load(); + this._media.load(); } +*/ -async function xmr_initialize_players() { - // Figure out what to initialize. - let elements = document.querySelectorAll("media.player"); - let stack = Array.from(elements); - console.debug("Initializing " + stack.length + " players..."); - - // Add a lazyloading handler to all entries. - for (let el of stack) { - xmr_initialize_player(el); - } -} - -(function() { +(function () { 'use strict'; // Support for Mobile Devices @@ -426,9 +547,11 @@ async function xmr_initialize_players() { document.querySelector("#header #navigation").classList.toggle("open"); }); - // Lazily load Media + // Lazily load this._media xmr_media_lazyload_initialize(); - // Initialize media player. - xmr_initialize_players(); + // Initialize this._media player. + for (let el of document.querySelectorAll("video")) { + el.player = new XMediaPlayer(el); + } })();