1// javascript/chives-image.js
4// ChivesImage provides a <chives-image> component that renders
5// an ImageEntity on a web page.
7// The image is lazy-loaded and wrapped in a link pointing to a URL
12// const elem = document.createElement("chives-image");
14// elem.href = props.path;
15// elem.imageEntity = props;
16// elem.variant = "thumbnail";
18class ChivesImage extends HTMLElement {
19 static get observedAttributes() {
21 // Where this image should link to, if anywhere.
24 // Either "full" or "thumbnail". If you select "thumbnail" but the
25 // image doesn't have a thumbnail_path set, the full image is used.
26 // Defaults to "full".
29 // Fields from ImageEntity
40 // href allows you to set the target href directly from JavaScript.
44 // const elem = document.createElement("chives-image");
45 // elem.href = "https://example.com";
48 this.setAttribute("href", url);
51 // variant allows you to set the variant directly from JavaScript.
55 // const elem = document.createElement("chives-image");
56 // elem.variant = "thumbnail";
59 if (v !== "full" && v !== "thumbnail") {
60 console.warn(`Unrecognised variant: ${v}, want 'full' or 'thumbnail'`);
62 this.setAttribute("variant", v);
65 // imageEntity allows you to pass an ImageEntity directly from JavaScript.
69 // const elem = document.createElement("chives-image");
70 // elem.imageEntity = imageEntity;
72 set imageEntity(entity) {
73 this.setAttribute("path", entity.path);
74 this.setAttribute("width", entity.width);
75 this.setAttribute("height", entity.height);
76 this.setAttribute("tint-colour", entity.tint_colour);
78 if (typeof entity.thumbnail_path === "string") {
79 this.setAttribute("thumbnail-path", entity.thumbnail_path);
82 if (typeof entity.alt_text === "string") {
83 this.setAttribute("alt-text", entity.alt_text);
86 if (entity.has_transparency) {
87 this.setAttribute("has-transparency", "");
89 this.removeAttribute("has-transparency");
93 // thumbnailPath allows you to set a custom thumbnail path.
95 // If you set a custom thumbnail path, the `variant` is automatically
96 // set to `thumbnail` on the assumption you want to render the thumbnail.
97 set thumbnailPath(path) {
98 this.setAttribute("thumbnail-path", path);
99 this.setAttribute("variant", "thumbnail");
102 // Rather than re-rendering the component on every attribute change
103 // (which will fire seven times when we set imageEntity), batch render
104 // changes together into the next event loop tick.
105 #renderTimeout = null;
107 connectedCallback() {
108 this.scheduleRender();
111 attributeChangedCallback() {
112 this.scheduleRender();
116 if (this.#renderTimeout) {
117 cancelAnimationFrame(this.#renderTimeout);
119 this.#renderTimeout = requestAnimationFrame(() => {
124 // Render the component.
126 // Gather properties from attributes
127 const href = this.getAttribute("href") || "";
129 const variant = this.getAttribute("variant") || "full";
131 const path = this.getAttribute("path");
132 const thumbnailPath = this.getAttribute("thumbnail-path");
133 const width = this.getAttribute("width");
134 const height = this.getAttribute("height");
135 const tintColour = this.getAttribute("tint-colour");
136 const altText = this.getAttribute("alt-text") || "";
137 const hasTransparency = this.hasAttribute("has-transparency");
139 // Decide which variant of the image will be displayed.
143 : typeof thumbnailPath === 'string'
147 // Choose which wrapper element to use. Use a link if there's
148 // an href, otherwise a wrapper div.
150 ? document.createElement("a")
151 : document.createElement("div");
157 // Add CSS classes and properties to the wrapper element.
158 wrapper.classList.add("chives_image");
159 if (hasTransparency) {
160 wrapper.classList.add("has_transparency");
163 wrapper.style.setProperty("--width", width);
164 wrapper.style.setProperty("--height", height);
165 wrapper.style.setProperty("--tint-colour", tintColour);
167 // Add a data-orientation property to the wrapper element.
168 const widthNum = parseFloat(width);
169 const heightNum = parseFloat(height);
171 if (!isNaN(widthNum) && !isNaN(heightNum)) {
172 let orientation = "square";
173 if (widthNum > heightNum) {
174 orientation = "landscape";
175 } else if (widthNum < heightNum) {
176 orientation = "portrait";
178 wrapper.setAttribute("data-orientation", orientation);
181 // Build the appropriate media element.
182 if (displayPath.endsWith(".mp4")) {
183 const video = document.createElement("video");
184 video.src = displayPath;
185 video.loading = "lazy";
186 video.autoplay = true;
189 video.playsInline = true;
190 wrapper.appendChild(video);
192 const img = document.createElement("img");
193 img.src = displayPath;
194 img.loading = "lazy";
200 wrapper.appendChild(img);
203 this.replaceChildren(wrapper);
207// Register global styles for the <chives-image> element.
208if (typeof document !== "undefined" &&
209 !document.getElementById("chives-image-styles")) {
210 const style = document.createElement("style");
211 style.id = "chives-image-styles";
212 style.textContent = `
215 aspect-ratio: var(--width) / var(--height);
216 background: var(--tint-colour);
230 document.head.appendChild(style);
233// Register the custom HTML tag <chives-image>
234if (!customElements.get("chives-image")) {
235 customElements.define("chives-image", ChivesImage);