400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
import { globalThis, document } from "../../utils/server-safe-globals.js";
|
|
import { MediaUIEvents, MediaUIAttributes } from "../../constants.js";
|
|
const template = document.createElement("template");
|
|
const HANDLE_W = 8;
|
|
const Z = {
|
|
100: 100,
|
|
200: 200,
|
|
300: 300
|
|
};
|
|
function lockBetweenZeroAndOne(num) {
|
|
return Math.max(0, Math.min(1, num));
|
|
}
|
|
template.innerHTML = `
|
|
<style>
|
|
#selectorContainer {
|
|
background-color: transparent;
|
|
height: 44px;
|
|
width: 100%;
|
|
display: flex;
|
|
position: relative;
|
|
}
|
|
|
|
#timeline {
|
|
width: 100%;
|
|
height: 10px;
|
|
background: #ccc;
|
|
position: absolute;
|
|
top: 16px;
|
|
z-index: ${Z["100"]};
|
|
}
|
|
|
|
#startHandle, #endHandle {
|
|
cursor: pointer;
|
|
height: 80%;
|
|
width: ${HANDLE_W}px;
|
|
border-radius: 4px;
|
|
background-color: royalblue;
|
|
}
|
|
|
|
#playhead {
|
|
height: 100%;
|
|
width: 3px;
|
|
background-color: #aaa;
|
|
position: absolute;
|
|
display: none;
|
|
z-index: ${Z["300"]};
|
|
}
|
|
|
|
#selection {
|
|
display: flex;
|
|
z-index: ${Z["200"]};
|
|
width: 25%;
|
|
height: 100%;
|
|
align-items: center;
|
|
}
|
|
|
|
#leftTrim {
|
|
width: 25%;
|
|
}
|
|
|
|
#spacer {
|
|
flex: 1;
|
|
background-color: cornflowerblue;
|
|
height: 40%;
|
|
}
|
|
|
|
#thumbnailContainer {
|
|
display: none;
|
|
position: absolute;
|
|
top: 0;
|
|
}
|
|
|
|
media-preview-thumbnail {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
border: 2px solid #fff;
|
|
border-radius: 2px;
|
|
background-color: #000;
|
|
width: 160px;
|
|
height: 90px;
|
|
|
|
/* Negative offset of half to center on the handle */
|
|
margin-left: -80px;
|
|
}
|
|
|
|
/* Can't get this working. Trying a downward triangle. */
|
|
/* media-preview-thumbnail::after {
|
|
content: "";
|
|
display: block;
|
|
width: 300px;
|
|
height: 300px;
|
|
margin: 100px;
|
|
background-color: #ff0;
|
|
} */
|
|
|
|
:host(:hover) #thumbnailContainer.enabled {
|
|
display: block;
|
|
animation: fadeIn ease 0.5s;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
0% {
|
|
/* transform-origin: bottom center; */
|
|
/* transform: scale(0.7); */
|
|
margin-top: 10px;
|
|
opacity: 0;
|
|
}
|
|
100% {
|
|
/* transform-origin: bottom center; */
|
|
/* transform: scale(1); */
|
|
margin-top: 0;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|
|
<div id="thumbnailContainer">
|
|
<media-preview-thumbnail></media-preview-thumbnail>
|
|
</div>
|
|
<div id="selectorContainer">
|
|
<div id="timeline"></div>
|
|
<div id="playhead"></div>
|
|
<div id="leftTrim"></div>
|
|
<div id="selection">
|
|
<div id="startHandle"></div>
|
|
<div id="spacer"></div>
|
|
<div id="endHandle"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
class MediaClipSelector extends globalThis.HTMLElement {
|
|
static get observedAttributes() {
|
|
return [
|
|
"thumbnails",
|
|
MediaUIAttributes.MEDIA_DURATION,
|
|
MediaUIAttributes.MEDIA_CURRENT_TIME
|
|
];
|
|
}
|
|
constructor() {
|
|
var _a, _b, _c;
|
|
super();
|
|
if (!this.shadowRoot) {
|
|
this.attachShadow({ mode: "open" });
|
|
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
}
|
|
this.draggingEl = null;
|
|
this.wrapper = this.shadowRoot.querySelector("#selectorContainer");
|
|
this.selection = this.shadowRoot.querySelector("#selection");
|
|
this.playhead = this.shadowRoot.querySelector("#playhead");
|
|
this.leftTrim = this.shadowRoot.querySelector("#leftTrim");
|
|
this.spacerFirst = this.shadowRoot.querySelector("#spacerFirst");
|
|
this.startHandle = this.shadowRoot.querySelector("#startHandle");
|
|
this.spacerMiddle = this.shadowRoot.querySelector("#spacerMiddle");
|
|
this.endHandle = this.shadowRoot.querySelector("#endHandle");
|
|
this.spacerLast = this.shadowRoot.querySelector("#spacerLast");
|
|
this._clickHandler = this.handleClick.bind(this);
|
|
this._dragStart = this.dragStart.bind(this);
|
|
this._dragEnd = this.dragEnd.bind(this);
|
|
this._drag = this.drag.bind(this);
|
|
this.wrapper.addEventListener("click", this._clickHandler, false);
|
|
this.wrapper.addEventListener("touchstart", this._dragStart, false);
|
|
(_a = globalThis.window) == null ? void 0 : _a.addEventListener("touchend", this._dragEnd, false);
|
|
this.wrapper.addEventListener("touchmove", this._drag, false);
|
|
this.wrapper.addEventListener("mousedown", this._dragStart, false);
|
|
(_b = globalThis.window) == null ? void 0 : _b.addEventListener("mouseup", this._dragEnd, false);
|
|
(_c = globalThis.window) == null ? void 0 : _c.addEventListener("mousemove", this._drag, false);
|
|
this.enableThumbnails();
|
|
}
|
|
get mediaDuration() {
|
|
return +this.getAttribute(MediaUIAttributes.MEDIA_DURATION);
|
|
}
|
|
get mediaCurrentTime() {
|
|
return +this.getAttribute(MediaUIAttributes.MEDIA_CURRENT_TIME);
|
|
}
|
|
/*
|
|
* pass in a mouse event (evt.clientX)
|
|
* calculates the percentage progress based on the bounding rectang
|
|
* converts the percentage progress into a duration in seconds
|
|
*/
|
|
getPlayheadBasedOnMouseEvent(evt) {
|
|
const duration = this.mediaDuration;
|
|
if (!duration)
|
|
return;
|
|
const mousePercent = lockBetweenZeroAndOne(this.getMousePercent(evt));
|
|
return mousePercent * duration;
|
|
}
|
|
getXPositionFromMouse(evt) {
|
|
let clientX;
|
|
if (["touchstart", "touchmove"].includes(evt.type)) {
|
|
clientX = evt.touches[0].clientX;
|
|
}
|
|
return clientX || evt.clientX;
|
|
}
|
|
getMousePercent(evt) {
|
|
const rangeRect = this.wrapper.getBoundingClientRect();
|
|
const mousePercent = (this.getXPositionFromMouse(evt) - rangeRect.left) / rangeRect.width;
|
|
return lockBetweenZeroAndOne(mousePercent);
|
|
}
|
|
dragStart(evt) {
|
|
if (evt.target === this.startHandle) {
|
|
this.draggingEl = this.startHandle;
|
|
}
|
|
if (evt.target === this.endHandle) {
|
|
this.draggingEl = this.endHandle;
|
|
}
|
|
this.initialX = this.getXPositionFromMouse(evt);
|
|
}
|
|
dragEnd() {
|
|
this.initialX = null;
|
|
this.draggingEl = null;
|
|
}
|
|
setSelectionWidth(selectionPercent, fullTimelineWidth) {
|
|
let percent = selectionPercent;
|
|
const minWidthPx = HANDLE_W * 3;
|
|
const minWidthPercent = lockBetweenZeroAndOne(
|
|
minWidthPx / fullTimelineWidth
|
|
);
|
|
if (percent < minWidthPercent) {
|
|
percent = minWidthPercent;
|
|
}
|
|
this.selection.style.width = `${percent * 100}%`;
|
|
}
|
|
drag(evt) {
|
|
if (!this.draggingEl) {
|
|
return;
|
|
}
|
|
evt.preventDefault();
|
|
const rangeRect = this.wrapper.getBoundingClientRect();
|
|
const fullTimelineWidth = rangeRect.width;
|
|
const endXPosition = this.getXPositionFromMouse(evt);
|
|
const xDelta = endXPosition - this.initialX;
|
|
const percent = this.getMousePercent(evt);
|
|
const selectionW = this.selection.getBoundingClientRect().width;
|
|
if (this.draggingEl === this.startHandle) {
|
|
this.initialX = this.getXPositionFromMouse(evt);
|
|
this.leftTrim.style.width = `${percent * 100}%`;
|
|
const selectionPercent = lockBetweenZeroAndOne(
|
|
(selectionW - xDelta) / fullTimelineWidth
|
|
);
|
|
this.setSelectionWidth(selectionPercent, fullTimelineWidth);
|
|
}
|
|
if (this.draggingEl === this.endHandle) {
|
|
this.initialX = this.getXPositionFromMouse(evt);
|
|
const selectionPercent = lockBetweenZeroAndOne(
|
|
(selectionW + xDelta) / fullTimelineWidth
|
|
);
|
|
this.setSelectionWidth(selectionPercent, fullTimelineWidth);
|
|
}
|
|
this.dispatchUpdate();
|
|
}
|
|
dispatchUpdate() {
|
|
const updateEvent = new CustomEvent("update", {
|
|
detail: this.getCurrentClipBounds()
|
|
});
|
|
this.dispatchEvent(updateEvent);
|
|
}
|
|
getCurrentClipBounds() {
|
|
const rangeRect = this.wrapper.getBoundingClientRect();
|
|
const leftTrimRect = this.leftTrim.getBoundingClientRect();
|
|
const selectionRect = this.selection.getBoundingClientRect();
|
|
const percentStart = lockBetweenZeroAndOne(
|
|
leftTrimRect.width / rangeRect.width
|
|
);
|
|
const percentEnd = lockBetweenZeroAndOne(
|
|
(leftTrimRect.width + selectionRect.width) / rangeRect.width
|
|
);
|
|
return {
|
|
startTime: Math.round(percentStart * this.mediaDuration),
|
|
endTime: Math.round(percentEnd * this.mediaDuration)
|
|
};
|
|
}
|
|
isTimestampInBounds(timestamp) {
|
|
const { startTime, endTime } = this.getCurrentClipBounds();
|
|
return startTime <= timestamp && endTime >= timestamp;
|
|
}
|
|
handleClick(evt) {
|
|
const mousePercent = this.getMousePercent(evt);
|
|
const timestampForClick = mousePercent * this.mediaDuration;
|
|
if (this.isTimestampInBounds(timestampForClick)) {
|
|
this.dispatchEvent(
|
|
new globalThis.CustomEvent(MediaUIEvents.MEDIA_SEEK_REQUEST, {
|
|
composed: true,
|
|
bubbles: true,
|
|
detail: timestampForClick
|
|
})
|
|
);
|
|
}
|
|
}
|
|
mediaCurrentTimeSet() {
|
|
const percentComplete = lockBetweenZeroAndOne(
|
|
this.mediaCurrentTime / this.mediaDuration
|
|
);
|
|
this.playhead.style.left = `${percentComplete * 100}%`;
|
|
this.playhead.style.display = "block";
|
|
if (!this.mediaPaused) {
|
|
const { startTime, endTime } = this.getCurrentClipBounds();
|
|
if (this.mediaCurrentTime < startTime || this.mediaCurrentTime > endTime) {
|
|
this.dispatchEvent(
|
|
new globalThis.CustomEvent(MediaUIEvents.MEDIA_SEEK_REQUEST, {
|
|
composed: true,
|
|
bubbles: true,
|
|
detail: startTime
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
mediaUnsetCallback(media) {
|
|
var _a, _b;
|
|
super.mediaUnsetCallback(media);
|
|
this.wrapper.removeEventListener("touchstart", this._dragStart);
|
|
this.wrapper.removeEventListener("touchend", this._dragEnd);
|
|
this.wrapper.removeEventListener("touchmove", this._drag);
|
|
this.wrapper.removeEventListener("mousedown", this._dragStart);
|
|
(_a = globalThis.window) == null ? void 0 : _a.removeEventListener("mouseup", this._dragEnd);
|
|
(_b = globalThis.window) == null ? void 0 : _b.removeEventListener("mousemove", this._drag);
|
|
}
|
|
/*
|
|
* This was copied over from media-time-range, we should have a way of making
|
|
* this code shared between the two components
|
|
*/
|
|
enableThumbnails() {
|
|
this.thumbnailPreview = this.shadowRoot.querySelector(
|
|
"media-preview-thumbnail"
|
|
);
|
|
const thumbnailContainer = this.shadowRoot.querySelector(
|
|
"#thumbnailContainer"
|
|
);
|
|
thumbnailContainer.classList.add("enabled");
|
|
let mouseMoveHandler;
|
|
const trackMouse = () => {
|
|
var _a;
|
|
mouseMoveHandler = (evt) => {
|
|
const duration = this.mediaDuration;
|
|
if (!duration)
|
|
return;
|
|
const rangeRect = this.wrapper.getBoundingClientRect();
|
|
const mousePercent = this.getMousePercent(evt);
|
|
const leftPadding = rangeRect.left - this.getBoundingClientRect().left;
|
|
const thumbnailLeft = leftPadding + mousePercent * rangeRect.width;
|
|
this.thumbnailPreview.style.left = `${thumbnailLeft}px`;
|
|
this.dispatchEvent(
|
|
new globalThis.CustomEvent(MediaUIEvents.MEDIA_PREVIEW_REQUEST, {
|
|
composed: true,
|
|
bubbles: true,
|
|
detail: mousePercent * duration
|
|
})
|
|
);
|
|
};
|
|
(_a = globalThis.window) == null ? void 0 : _a.addEventListener("mousemove", mouseMoveHandler, false);
|
|
};
|
|
const stopTrackingMouse = () => {
|
|
var _a;
|
|
(_a = globalThis.window) == null ? void 0 : _a.removeEventListener("mousemove", mouseMoveHandler);
|
|
};
|
|
let rangeEntered = false;
|
|
const rangeMouseMoveHander = () => {
|
|
var _a;
|
|
if (!rangeEntered && this.mediaDuration) {
|
|
rangeEntered = true;
|
|
this.thumbnailPreview.style.display = "block";
|
|
trackMouse();
|
|
const offRangeHandler = (evt) => {
|
|
var _a2;
|
|
if (evt.target != this && !this.contains(evt.target)) {
|
|
this.thumbnailPreview.style.display = "none";
|
|
(_a2 = globalThis.window) == null ? void 0 : _a2.removeEventListener(
|
|
"mousemove",
|
|
offRangeHandler
|
|
);
|
|
rangeEntered = false;
|
|
stopTrackingMouse();
|
|
}
|
|
};
|
|
(_a = globalThis.window) == null ? void 0 : _a.addEventListener(
|
|
"mousemove",
|
|
offRangeHandler,
|
|
false
|
|
);
|
|
}
|
|
if (!this.mediaDuration) {
|
|
this.thumbnailPreview.style.display = "none";
|
|
}
|
|
};
|
|
this.addEventListener("mousemove", rangeMouseMoveHander, false);
|
|
}
|
|
disableThumbnails() {
|
|
const thumbnailContainer = this.shadowRoot.querySelector(
|
|
"#thumbnailContainer"
|
|
);
|
|
thumbnailContainer.classList.remove("enabled");
|
|
}
|
|
}
|
|
if (!globalThis.customElements.get("media-clip-selector")) {
|
|
globalThis.customElements.define("media-clip-selector", MediaClipSelector);
|
|
}
|
|
var media_clip_selector_default = MediaClipSelector;
|
|
export {
|
|
media_clip_selector_default as default
|
|
};
|