Skip to main content

javascript/chives-image.js

1// javascript/chives-image.js
2// chives version: 52
4// ChivesImage provides a <chives-image> component that renders
5// an ImageEntity on a web page.
6//
7// The image is lazy-loaded and wrapped in a link pointing to a URL
8// of your choice.
9//
10// Example usage:
11//
12// const elem = document.createElement("chives-image");
13//
14// elem.href = props.path;
15// elem.imageEntity = props;
16// elem.variant = "thumbnail";
17//
18class ChivesImage extends HTMLElement {
19 static get observedAttributes() {
20 return [
21 // Where this image should link to, if anywhere.
22 "href",
23 //
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".
27 "variant",
28 //
29 // Fields from ImageEntity
30 "path",
31 "thumbnail-path",
32 "width",
33 "height",
34 "tint-colour",
35 "alt-text",
36 "has-transparency",
37 ];
38 }
40 // href allows you to set the target href directly from JavaScript.
41 //
42 // For example:
43 //
44 // const elem = document.createElement("chives-image");
45 // elem.href = "https://example.com";
46 //
47 set href(url) {
48 this.setAttribute("href", url);
49 }
51 // variant allows you to set the variant directly from JavaScript.
52 //
53 // For example:
54 //
55 // const elem = document.createElement("chives-image");
56 // elem.variant = "thumbnail";
57 //
58 set variant(v) {
59 if (v !== "full" && v !== "thumbnail") {
60 console.warn(`Unrecognised variant: ${v}, want 'full' or 'thumbnail'`);
61 }
62 this.setAttribute("variant", v);
63 }
65 // imageEntity allows you to pass an ImageEntity directly from JavaScript.
66 //
67 // For example:
68 //
69 // const elem = document.createElement("chives-image");
70 // elem.imageEntity = imageEntity;
71 //
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);
80 }
82 if (typeof entity.alt_text === "string") {
83 this.setAttribute("alt-text", entity.alt_text);
84 }
86 if (entity.has_transparency) {
87 this.setAttribute("has-transparency", "");
88 } else {
89 this.removeAttribute("has-transparency");
90 }
91 }
93 // thumbnailPath allows you to set a custom thumbnail path.
94 //
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");
100 }
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();
109 }
111 attributeChangedCallback() {
112 this.scheduleRender();
113 }
115 scheduleRender() {
116 if (this.#renderTimeout) {
117 cancelAnimationFrame(this.#renderTimeout);
118 }
119 this.#renderTimeout = requestAnimationFrame(() => {
120 this.render();
121 });
122 }
124 // Render the component.
125 render() {
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.
140 const displayPath =
141 variant === "full"
142 ? path
143 : typeof thumbnailPath === 'string'
144 ? thumbnailPath
145 : path;
147 // Choose which wrapper element to use. Use a link if there's
148 // an href, otherwise a wrapper div.
149 const wrapper = href
150 ? document.createElement("a")
151 : document.createElement("div");
153 if (href) {
154 wrapper.href = href;
155 }
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");
161 }
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";
177 }
178 wrapper.setAttribute("data-orientation", orientation);
179 }
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;
187 video.loop = true;
188 video.muted = true;
189 video.playsInline = true;
190 wrapper.appendChild(video);
191 } else {
192 const img = document.createElement("img");
193 img.src = displayPath;
194 img.loading = "lazy";
196 if (altText) {
197 img.alt = altText;
198 }
200 wrapper.appendChild(img);
201 }
203 this.replaceChildren(wrapper);
204 }
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 = `
213 .chives_image {
214 display: block;
215 aspect-ratio: var(--width) / var(--height);
216 background: var(--tint-colour);
218 &.has_transparency {
219 background: none;
220 }
222 img, video {
223 width: 100%;
224 height: 100%;
225 object-fit: cover;
226 display: block;
227 }
228 }
229 `;
230 document.head.appendChild(style);
233// Register the custom HTML tag <chives-image>
234if (!customElements.get("chives-image")) {
235 customElements.define("chives-image", ChivesImage);