1// javascript/chives-video.js
4// ChivesVideo provides a <chives-video> component that renders
5// a VideoEntity on a web page.
9// const elem = document.createElement("chives-video");
11// elem.href = props.path;
12// elem.videoEntity = props;
14class ChivesVideo extends HTMLElement {
15 static get observedAttributes() {
18 // Fields from VideoEntity
28 // videoEntity allows you to pass a VideoEntity directly from JavaScript.
32 // const elem = document.createElement("chives-video");
33 // elem.videoEntity = videoEntity;
35 set videoEntity(entity) {
36 this.setAttribute("path", entity.path);
37 this.setAttribute("width", entity.width);
38 this.setAttribute("height", entity.height);
40 this.setAttribute("poster-path", entity.poster.path);
42 if (Array.isArray(entity.subtitles)) {
45 entity.subtitles.map(s => s.label).join(";"),
47 entity.subtitles.forEach(s =>
48 this.#setSubtitleAttribute(s.label, s.path)
52 if (entity.autoplay) {
53 this.setAttribute("autoplay", "");
55 this.removeAttribute("autoplay");
59 // Rather than re-rendering the component on every attribute change
60 // (which will fire seven times when we set imageEntity), batch render
61 // changes together into the next event loop tick.
62 #renderTimeout = null;
65 this.scheduleRender();
68 attributeChangedCallback() {
69 this.scheduleRender();
73 if (this.#renderTimeout) {
74 cancelAnimationFrame(this.#renderTimeout);
76 this.#renderTimeout = requestAnimationFrame(() => {
81 // Render the component.
83 // Gather properties from attributes
85 const path = this.getAttribute("path");
86 const width = this.getAttribute("width");
87 const height = this.getAttribute("height");
89 const posterPath = this.getAttribute("poster-path");
91 const subtitles = (this.getAttribute("subtitles") || "")
93 .filter(label => label !== "")
95 const path = this.#getSubtitleAttribute(label);
96 return { label, path };
99 const autoplay = this.hasAttribute("autoplay");
101 // Create a wrapper div.
102 const wrapper = document.createElement("div");
104 // Add CSS classes and properties to the wrapper element.
105 wrapper.classList.add("chives_video");
107 wrapper.style.setProperty("--width", width);
108 wrapper.style.setProperty("--height", height);
110 // Add a data-orientation property to the wrapper element.
111 const widthNum = parseFloat(width);
112 const heightNum = parseFloat(height);
114 if (!isNaN(widthNum) && !isNaN(heightNum)) {
115 let orientation = "square";
116 if (widthNum > heightNum) {
117 orientation = "landscape";
118 } else if (widthNum < heightNum) {
119 orientation = "portrait";
121 wrapper.setAttribute("data-orientation", orientation);
124 // Build the <video> element.
125 const video = document.createElement("video");
128 video.autoplay = true;
131 video.playsInline = true;
133 video.controls = true;
136 video.preload = "none";
138 video.poster = posterPath;
140 subtitles.forEach(s => {
141 const track = document.createElement("track");
142 track.kind = "subtitles";
143 track.label = s.label;
145 track.srclang = s.label.substring(0, 2).toLowerCase()
146 video.appendChild(track);
149 wrapper.appendChild(video);
150 this.replaceChildren(wrapper);
153 // We store subtitle paths in attributes like `subtitles-english-cc`.
155 // Because spaces and special characters break HTML attribute value parsing,
156 // tidy labels into lowercase, hyphen-separated strings.
157 #subtitleAttributeName(label) {
158 return "subtitles-" + label.replace(/[^a-zA-Z]+/g, "-").toLowerCase();
161 #setSubtitleAttribute(label, path) {
162 this.setAttribute(this.#subtitleAttributeName(label), path);
165 #getSubtitleAttribute(label) {
166 return this.getAttribute(this.#subtitleAttributeName(label));
170// Register global styles for the <chives-video> element.
171if (typeof document !== "undefined" &&
172 !document.getElementById("chives-video-styles")) {
173 const style = document.createElement("style");
174 style.id = "chives-video-styles";
175 style.textContent = `
178 aspect-ratio: var(--width) / var(--height);
189 document.head.appendChild(style);
192// Register the custom HTML tag <chives-video>
193if (!customElements.get("chives-video")) {
194 customElements.define("chives-video", ChivesVideo);