mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Merge 8a1591d9be
into 39ee27eea6
This commit is contained in:
commit
f4c67e1b3e
2 changed files with 223 additions and 47 deletions
|
@ -1,15 +1,18 @@
|
|||
button {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: var(--button-size);
|
||||
min-height: var(--button-size);
|
||||
padding: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: var(--button-size, 32px);
|
||||
min-height: var(--button-size, 32px);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
margin: 4px;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.root {
|
||||
|
@ -26,6 +29,9 @@ button {
|
|||
height: 40px;
|
||||
background-color: rgba(26,26,26,.8);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
@ -33,11 +39,12 @@ button {
|
|||
}
|
||||
|
||||
.playing {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAALElEQVR4AWMYQeD///8KQOwAxQq4xQkb1PAfARpwiQ9Kg0YNGjWI8iwycgAA8BVdyMCA4SwAAAAASUVORK5CYII=") no-repeat;
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAALElEQVR4AWMYQeD///8KQOwAxQq4xQkb1PAfARpwiQ9Kg0YNGjWI8iwycgAA8BVdyMCA4SwAAAAASUVORK5CYII=") no-repeat center center;
|
||||
|
||||
}
|
||||
|
||||
.paused {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAATklEQVR4AWMYgeD///8C1DLoARA7UG4QAqynyHX/UcF7II6n3CAE2A/ECpQbhHBdPTUM+gDEDZQadIB8ryFckUBpYG8ARf/AJ0i4K0YBAOu3zFnxHnpDAAAAAElFTkSuQmCC") no-repeat;
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAATklEQVR4AWMYgeD///8C1DLoARA7UG4QAqynyHX/UcF7II6n3CAE2A/ECpQbhHBdPTUM+gDEDZQadIB8ryFckUBpYG8ARf/AJ0i4K0YBAOu3zFnxHnpDAAAAAElFTkSuQmCC") no-repeat center center;
|
||||
}
|
||||
|
||||
.ended {
|
||||
|
@ -58,4 +65,4 @@ button {
|
|||
|
||||
.fullscreen.fullscreen-active {
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAbtJREFUSA3tlj0vBFEUhnd8hFhrE4mPbSREovIfdFQ6hcJ/EJVColP6GRoqhZbEFqKmET2xDSpWYjxncu915u6ZzWS20DjJ6577no/3zL1mqNWwNE1HwKb4g5rrdcjayHo54oxVbG8QAepl0FNphLVBQ6bflZ2ySiLU6+a+3bY/Hq/qA0EEog4m4ieDmwOzwrNazUOPwgQKpfkVWDAEVuHvQAsUDhjqSLKmeIQXKxKQ2Jv8UPY7eejuHJIsEantJ6B6l/glIXsKvOgq/DICT+Rld6IHH9IbEursz8GM5kv68+RdxiKJLibYYr+sOeffJknyqXlyZbhJzTn/g9yuwf9Tf3QC8SUPM8eoMUuXi/vWPJcsuSuac36H3I7BZ98UeQduQGwbcQEJ03ES+3vQ8y7kakkQkQegrYzAKwXyLuQs96K5yDrrYi6r3KZJ2jEicnS2EdwCX8Dbu3P6PcGzT3brCWuvCGTc/BpOPsVyJ0UC2ZkT3wfa8iJErObZZ4BYEyzFzww3DsKF4tsiBNaAPhaZ3PrGxBo9e0PkQP6ajYELIFa5uVejh3+SNn74z0JEjkClyX1zv9Jnxzf/AaCJ561FockLAAAAAElFTkSuQmCC') no-repeat;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,3 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
|
@ -62,6 +58,7 @@
|
|||
|
||||
function formatTime(time, showHours = false) {
|
||||
// Format the duration as "h:mm:ss" or "m:ss"
|
||||
if (isNaN(time) || !isFinite(time)) return "00:00";
|
||||
time = Math.round(time / 1000);
|
||||
|
||||
const hours = Math.floor(time / 3600);
|
||||
|
@ -76,6 +73,172 @@
|
|||
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
class CustomRangeInput {
|
||||
constructor(originalInput) {
|
||||
this.originalInput = originalInput;
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'custom-range-container';
|
||||
this.container.style.position = 'relative';
|
||||
this.container.style.height = '20px';
|
||||
this.container.style.width = '150px';
|
||||
|
||||
this.track = document.createElement('div');
|
||||
this.track.className = 'custom-range-track';
|
||||
this.track.style.position = 'absolute';
|
||||
this.track.style.top = '50%';
|
||||
this.track.style.transform = 'translateY(-50%)';
|
||||
this.track.style.width = '100%';
|
||||
this.track.style.height = '4px';
|
||||
this.track.style.backgroundColor = '#d3d3d3';
|
||||
this.track.style.borderRadius = '2px';
|
||||
|
||||
this.progress = document.createElement('div');
|
||||
this.progress.className = 'custom-range-progress';
|
||||
this.progress.style.position = 'absolute';
|
||||
this.progress.style.top = '50%';
|
||||
this.progress.style.transform = 'translateY(-50%)';
|
||||
this.progress.style.width = '0%';
|
||||
this.progress.style.height = '4px';
|
||||
this.progress.style.backgroundColor = '#4c8bf5';
|
||||
this.progress.style.borderRadius = '2px';
|
||||
|
||||
this.thumb = document.createElement('div');
|
||||
this.thumb.className = 'custom-range-thumb';
|
||||
this.thumb.style.position = 'absolute';
|
||||
this.thumb.style.top = '50%';
|
||||
this.thumb.style.transform = 'translate(-50%, -50%)';
|
||||
this.thumb.style.width = '16px';
|
||||
this.thumb.style.height = '16px';
|
||||
this.thumb.style.backgroundColor = '#4c8bf5';
|
||||
this.thumb.style.borderRadius = '50%';
|
||||
this.thumb.style.cursor = 'pointer';
|
||||
this.thumb.style.zIndex = '1';
|
||||
|
||||
this.originalInput.style.display = 'none';
|
||||
|
||||
// Assemble component
|
||||
this.container.appendChild(this.track);
|
||||
this.container.appendChild(this.progress);
|
||||
this.container.appendChild(this.thumb);
|
||||
this.originalInput.parentNode.insertBefore(this.container, this.originalInput.nextSibling);
|
||||
|
||||
this.updateThumbPosition();
|
||||
|
||||
// Bind event handlers
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
updateThumbPosition() {
|
||||
const min = parseFloat(this.originalInput.min) || 0;
|
||||
const max = parseFloat(this.originalInput.max) || 100;
|
||||
const value = parseFloat(this.originalInput.value) || min;
|
||||
|
||||
// Calculate percentage
|
||||
const percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
// Update thumb and progress position
|
||||
this.thumb.style.left = `${percentage}%`;
|
||||
this.progress.style.width = `${percentage}%`;
|
||||
}
|
||||
|
||||
setValue(clientX) {
|
||||
const rect = this.track.getBoundingClientRect();
|
||||
const min = parseFloat(this.originalInput.min) || 0;
|
||||
const max = parseFloat(this.originalInput.max) || 100;
|
||||
const step = parseFloat(this.originalInput.step) || 1;
|
||||
|
||||
// Calculate percentage of position within track
|
||||
let percentage = (clientX - rect.left) / rect.width;
|
||||
|
||||
// Clamp percentage to 0-1 range
|
||||
percentage = Math.max(0, Math.min(1, percentage));
|
||||
|
||||
// Calculate value based on percentage
|
||||
let value = min + percentage * (max - min);
|
||||
|
||||
// Apply step if specified
|
||||
if (step > 0) {
|
||||
value = Math.round(value / step) * step;
|
||||
}
|
||||
|
||||
// Ensure value is within min/max bounds
|
||||
value = Math.max(min, Math.min(max, value));
|
||||
|
||||
// Update original input value
|
||||
this.originalInput.value = value;
|
||||
|
||||
// Dispatch input and change events
|
||||
const inputEvent = new Event('input', { bubbles: true });
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
this.originalInput.dispatchEvent(inputEvent);
|
||||
this.originalInput.dispatchEvent(changeEvent);
|
||||
|
||||
// Update thumb position
|
||||
this.updateThumbPosition();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Store bound functions so we can remove them later
|
||||
this.mouseMoveHandler = (e) => {
|
||||
if (this.isDragging) {
|
||||
this.setValue(e.clientX);
|
||||
}
|
||||
};
|
||||
|
||||
this.mouseUpHandler = () => {
|
||||
this.isDragging = false;
|
||||
};
|
||||
|
||||
this.touchMoveHandler = (e) => {
|
||||
if (this.isDragging) {
|
||||
this.setValue(e.touches[0].clientX);
|
||||
}
|
||||
};
|
||||
|
||||
this.touchEndHandler = () => {
|
||||
this.isDragging = false;
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
this.container.addEventListener('mousedown', (e) => {
|
||||
this.isDragging = true;
|
||||
this.setValue(e.clientX);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', this.mouseMoveHandler);
|
||||
document.addEventListener('mouseup', this.mouseUpHandler);
|
||||
|
||||
// Touch events
|
||||
this.container.addEventListener('touchstart', (e) => {
|
||||
this.isDragging = true;
|
||||
this.setValue(e.touches[0].clientX);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('touchmove', this.touchMoveHandler);
|
||||
document.addEventListener('touchend', this.touchEndHandler);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Remove all event listeners
|
||||
document.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||
document.removeEventListener('mouseup', this.mouseUpHandler);
|
||||
document.removeEventListener('touchmove', this.touchMoveHandler);
|
||||
document.removeEventListener('touchend', this.touchEndHandler);
|
||||
|
||||
// Remove DOM elements
|
||||
if (this.container && this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
|
||||
// Restore original input
|
||||
if (this.originalInput) {
|
||||
this.originalInput.style.display = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MediaControls {
|
||||
constructor() {
|
||||
this.nonce = Date.now();
|
||||
|
@ -100,7 +263,6 @@
|
|||
this.root.innerHTML = generateMarkup(this.isAudioOnly);
|
||||
this.controls.appendChild(this.root);
|
||||
|
||||
|
||||
const elementNames = [
|
||||
"duration",
|
||||
"play-pause-button",
|
||||
|
@ -121,6 +283,10 @@
|
|||
this.elements[camelCase(id)] = this.controls.getElementById(id);
|
||||
});
|
||||
|
||||
// Replace standard range inputs with custom ones
|
||||
this.customProgress = new CustomRangeInput(this.elements.progress);
|
||||
this.customVolume = new CustomRangeInput(this.elements.volumeLevel);
|
||||
|
||||
// Init position duration box.
|
||||
const positionTextNode = this.elements.positionText;
|
||||
const durationSpan = this.elements.duration;
|
||||
|
@ -204,10 +370,6 @@
|
|||
});
|
||||
|
||||
// Create state transitions.
|
||||
//
|
||||
// It exposes one method per transition. i.e. this.pause(), this.play(), etc.
|
||||
// For each transition, we check that the transition is possible and call
|
||||
// the `onStateChange` handler.
|
||||
for (let name in TRANSITIONS) {
|
||||
if (!TRANSITIONS.hasOwnProperty(name)) {
|
||||
continue;
|
||||
|
@ -215,7 +377,6 @@
|
|||
this[name] = () => {
|
||||
const from = this.state;
|
||||
|
||||
// Checks if the transition is valid in the current state.
|
||||
if (!TRANSITIONS[name][from]) {
|
||||
const error = `Transition "${name}" invalid for the current state "${from}"`;
|
||||
console.error(error);
|
||||
|
@ -228,7 +389,6 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Transition to the next state.
|
||||
this.state = to;
|
||||
this.onStateChange(from);
|
||||
};
|
||||
|
@ -240,37 +400,48 @@
|
|||
}
|
||||
|
||||
cleanup() {
|
||||
// Remove mutation observer
|
||||
this.mutationObserver.disconnect();
|
||||
|
||||
// Remove media event listeners
|
||||
this.mediaEvents.forEach(event => {
|
||||
this.media.removeEventListener(event, this);
|
||||
});
|
||||
|
||||
// Remove control event listeners
|
||||
this.controlEvents.forEach(({ el, type }) => {
|
||||
el.removeEventListener(type, this);
|
||||
});
|
||||
|
||||
// Clean up custom range inputs
|
||||
if (this.customProgress) {
|
||||
this.customProgress.destroy();
|
||||
}
|
||||
if (this.customVolume) {
|
||||
this.customVolume.destroy();
|
||||
}
|
||||
|
||||
// Remove root element
|
||||
if (this.root && this.root.parentNode) {
|
||||
this.root.parentNode.removeChild(this.root);
|
||||
}
|
||||
}
|
||||
|
||||
// State change handler
|
||||
onStateChange(from) {
|
||||
this.render(from);
|
||||
}
|
||||
|
||||
render(from = this.state) {
|
||||
if (!this.isAudioOnly) {
|
||||
// XXX This should ideally use clientHeight/clientWidth,
|
||||
// but for some reason I couldn't figure out yet,
|
||||
// using it breaks layout.
|
||||
this.root.style.height = this.media.videoHeight;
|
||||
this.root.style.width = this.media.videoWidth;
|
||||
}
|
||||
|
||||
// Error
|
||||
if (this.state == ERRORED) {
|
||||
//XXX render errored state
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state != from) {
|
||||
// Play/Pause button.
|
||||
const playPauseButton = this.elements.playPauseButton;
|
||||
playPauseButton.classList.remove(from);
|
||||
playPauseButton.classList.add(this.state);
|
||||
|
@ -281,8 +452,10 @@
|
|||
(this.media.currentTime / this.media.duration) * 100;
|
||||
if (Number.isFinite(positionPercent)) {
|
||||
this.elements.progress.value = positionPercent;
|
||||
this.customProgress.updateThumbPosition();
|
||||
} else {
|
||||
this.elements.progress.value = 0;
|
||||
this.customProgress.updateThumbPosition();
|
||||
}
|
||||
|
||||
// Current time and duration.
|
||||
|
@ -302,6 +475,7 @@
|
|||
: Math.round(this.media.volume * 100);
|
||||
if (this.elements.volumeLevel.value != volumeLevelValue) {
|
||||
this.elements.volumeLevel.value = volumeLevelValue;
|
||||
this.customVolume.updateThumbPosition();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,8 +503,8 @@
|
|||
this.toggleMuted();
|
||||
break;
|
||||
case this.elements.fullscreenSwitch:
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "input":
|
||||
|
@ -345,7 +519,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// HTMLMediaElement event handler
|
||||
onMediaEvent(event) {
|
||||
switch (event.type) {
|
||||
case "ended":
|
||||
|
@ -353,7 +526,6 @@
|
|||
break;
|
||||
case "play":
|
||||
case "pause":
|
||||
// Transition to PLAYING or PAUSED state.
|
||||
this[event.type]();
|
||||
break;
|
||||
case "volumechange":
|
||||
|
@ -367,7 +539,6 @@
|
|||
}
|
||||
|
||||
/* Media actions */
|
||||
|
||||
playOrPause() {
|
||||
switch (this.state) {
|
||||
case PLAYING:
|
||||
|
@ -388,19 +559,18 @@
|
|||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
const { fullscreenEnabled, fullscreenElement } = document;
|
||||
const { fullscreenEnabled, fullscreenElement } = document;
|
||||
const isElementFullscreen = fullscreenElement && fullscreenElement === this.media;
|
||||
|
||||
const isElementFullscreen = fullscreenElement && fullscreenElement === this.media;
|
||||
|
||||
if (fullscreenEnabled && isElementFullscreen) {
|
||||
document.exitFullscreen().then(() => {
|
||||
this.elements.fullscreenSwitch.classList.remove("fullscreen-active");
|
||||
});
|
||||
} else {
|
||||
this.media.requestFullscreen().then(() => {
|
||||
this.elements.fullscreenSwitch.classList.add("fullscreen-active");
|
||||
});
|
||||
}
|
||||
if (fullscreenEnabled && isElementFullscreen) {
|
||||
document.exitFullscreen().then(() => {
|
||||
this.elements.fullscreenSwitch.classList.remove("fullscreen-active");
|
||||
});
|
||||
} else {
|
||||
this.media.requestFullscreen().then(() => {
|
||||
this.elements.fullscreenSwitch.classList.add("fullscreen-active");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
changeVolume() {
|
||||
|
@ -412,5 +582,4 @@
|
|||
}
|
||||
|
||||
new MediaControls();
|
||||
})();
|
||||
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue