@webwriter/interactive-video
Version:
(WIP) Enhance learning by adding interactive content in popups to videos for an engaging, interactive experience.
257 lines (205 loc) • 7.83 kB
text/typescript
import { html, css, LitElement, PropertyValues } from "lit";
import { LitElementWw } from "@webwriter/lit";
import { customElement, property, query } from "lit/decorators.js";
import "@shoelace-style/shoelace/dist/themes/light.css";
import { SlIcon } from "@shoelace-style/shoelace";
import styles from "./webwriter-video-interaction.styles";
import { InteractiveVideoOptions } from "../../components/options-panel/interactive-video-options";
/**
* `webwriter-video-interaction` is a custom element that represents an interaction in a `replace` interaction.
* It extends `LitElementWw` and provides a slot for content insertion.
*/
import radiusBottomRight from "@tabler/icons/outline/radius-bottom-right.svg";
import gripHorizontal from "@tabler/icons/outline/grip-horizontal.svg";
export class WwVideoInteraction extends LitElementWw {
/**
* The styles for the webwriter-interactive-video component.
*/
static styles = [styles];
accessor tabIndex = -1;
accessor id;
accessor startTime;
accessor endTime;
accessor initialPause = "false";
accessor bottomRight: SlIcon;
accessor dragIcon: SlIcon;
// Create an observer instance linked to the callback function
private mutationObserver: MutationObserver;
//
//
//
static get scopedElements() {
return {
"sl-icon": SlIcon,
"interactive-video-options": InteractiveVideoOptions,
};
}
/*
*/
constructor() {
super();
this.mutationObserver = new MutationObserver(this.mutationCallback);
}
/*
*/
protected firstUpdated(_changedProperties: PropertyValues): void {
// Options for the observer (which mutations to observe)
const config = {
attributes: true,
childList: true,
subtree: true,
characterData: true,
};
// Start observing the target node for configured mutations
this.mutationObserver.observe(this, config);
//create an empty p element if container has no children
const slot = this.shadowRoot.querySelector("slot");
const assignedElements = slot.assignedElements();
if (assignedElements.length == 0) {
const par = document.createElement("p");
par.textContent = "Add something here...";
this.appendChild(par);
}
}
/**
* Renders the component's template.
* Provides a slot for inserting custom content.
*
* @returns The HTML template for the component.
*/
render() {
return html`
<div
id="popup"
style="overflow: scroll; height: 100%; display: flex; flex-direction: column; align-items: center; justify-items: center; "
=${() =>
this.dispatchEvent(
new CustomEvent("interactionClicked", {
detail: { id: this.id },
bubbles: true,
composed: true,
})
)}
>
${this.isContentEditable
? html`<sl-icon
id="dragIcon"
style="position: sticky; top: 0; /* Keeps it at the top */"
src=${gripHorizontal}
="${this.startDragging}"
>
</sl-icon>`
: null}
<slot class="page"></slot>
${this.isContentEditable
? html` <sl-icon
id="bottomRight"
style="position: absolute; bottom: 5px; right: 5px; "
src=${radiusBottomRight}
=${this.startResizing}
>
</sl-icon>`
: null}
</div>
<interactive-video-options
style="outline: none"
contenteditable=${this.isContentEditable}
part="options"
class="author-only"
></interactive-video-options>
`;
}
//
//
//
private mutationCallback = (mutationList: MutationRecord[]) => {
mutationList.forEach(
({ type, removedNodes, addedNodes, attributeName, target }) => {
//
if (type === "childList") {
// Check if there is at least one paragraph <p> element in the container
const paragraphs = this.querySelectorAll("p");
if (paragraphs.length === 0) {
const par = document.createElement("p");
par.textContent = "Add something here...";
this.appendChild(par);
}
}
}
);
};
//
//
//
startResizing(e: PointerEvent) {
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startY = e.clientY;
const startWidth = this.offsetWidth;
const startHeight = this.offsetHeight;
const parent = this.parentElement?.shadowRoot?.querySelector("#video");
if (!parent) return;
const parentRect = parent.getBoundingClientRect();
const rect = this.getBoundingClientRect();
this.bottomRight.setPointerCapture(e.pointerId);
const onPointerMove = (moveEvent: PointerEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
const maxWidth = parentRect.width - (rect.left - parentRect.left);
const maxHeight = parentRect.height - (rect.top - parentRect.top);
const newWidth = Math.max(50, Math.min(startWidth + deltaX, maxWidth));
const newHeight = Math.max(50, Math.min(startHeight + deltaY, maxHeight));
this.style.width = `${newWidth}px`;
this.style.height = `${newHeight}px`;
};
const onPointerUp = () => {
this.bottomRight.releasePointerCapture(e.pointerId);
this.bottomRight.removeEventListener("pointermove", onPointerMove);
this.bottomRight.removeEventListener("pointerup", onPointerUp);
};
this.bottomRight.addEventListener("pointermove", onPointerMove);
this.bottomRight.addEventListener("pointerup", onPointerUp);
}
//
//
//
startDragging(e: PointerEvent) {
if (e.target === this.bottomRight || e.target !== this.dragIcon) return;
e.preventDefault();
const startX = e.clientX;
const startY = e.clientY;
const rect = this.getBoundingClientRect();
const parent = this.parentElement?.shadowRoot?.querySelector("#video");
if (!parent) return;
const parentRect = parent.getBoundingClientRect();
const offsetX = rect.left - parentRect.left;
const offsetY = rect.top - parentRect.top;
this.dragIcon.setPointerCapture(e.pointerId);
const onPointerMove = (moveEvent: PointerEvent) => {
const newX = offsetX + (moveEvent.clientX - startX);
const newY = offsetY + (moveEvent.clientY - startY);
const maxX = parentRect.width - rect.width;
const maxY = parentRect.height - rect.height;
const clampedX = Math.max(0, Math.min(newX, maxX));
const clampedY = Math.max(0, Math.min(newY, maxY));
this.style.position = "absolute";
this.style.left = `${clampedX}px`;
this.style.top = `${clampedY}px`;
};
const onPointerUp = () => {
this.dragIcon.releasePointerCapture(e.pointerId);
this.dragIcon.removeEventListener("pointermove", onPointerMove);
this.dragIcon.removeEventListener("pointerup", onPointerUp);
};
this.dragIcon.addEventListener("pointermove", onPointerMove);
this.dragIcon.addEventListener("pointerup", onPointerUp);
}
}