200 lines
5.9 KiB
Svelte
200 lines
5.9 KiB
Svelte
<!--
|
|
Copyright: Ankitects Pty Ltd and contributors
|
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
-->
|
|
<script lang="typescript">
|
|
import { createEventDispatcher, onDestroy } from "svelte";
|
|
import { nodeIsElement } from "lib/dom";
|
|
|
|
export let activeImage: HTMLImageElement | null;
|
|
export let container: HTMLElement;
|
|
export let sheet: CSSStyleSheet;
|
|
|
|
let active: boolean = false;
|
|
|
|
$: {
|
|
const index = images.indexOf(activeImage!);
|
|
|
|
if (index >= 0) {
|
|
const rule = sheet.cssRules[index] as CSSStyleRule;
|
|
active = rule.cssText.endsWith("{ }");
|
|
} else {
|
|
activeImage = null;
|
|
}
|
|
}
|
|
|
|
export let maxWidth = 250;
|
|
export let maxHeight = 125;
|
|
|
|
$: restrictionAspectRatio = maxWidth / maxHeight;
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
function createPathRecursive(tokens: string[], element: Element): string[] {
|
|
const tagName = element.tagName.toLowerCase();
|
|
|
|
if (!element.parentElement) {
|
|
const nth =
|
|
Array.prototype.indexOf.call(
|
|
(element.parentNode! as Document | ShadowRoot).children,
|
|
element
|
|
) + 1;
|
|
return [`${tagName}:nth-child(${nth})`, ...tokens];
|
|
} else {
|
|
const nth =
|
|
Array.prototype.indexOf.call(element.parentElement.children, element) +
|
|
1;
|
|
return createPathRecursive(
|
|
[`${tagName}:nth-child(${nth})`, ...tokens],
|
|
element.parentElement
|
|
);
|
|
}
|
|
}
|
|
|
|
function createPath(element: Element): string {
|
|
return createPathRecursive([], element).join(" > ");
|
|
}
|
|
|
|
export const images: HTMLImageElement[] = [];
|
|
|
|
$: for (const [index, image] of images.entries()) {
|
|
const rule = sheet.cssRules[index] as CSSStyleRule;
|
|
rule.selectorText = createPath(image);
|
|
}
|
|
|
|
function filterImages(nodes: HTMLCollection | Node[]): HTMLImageElement[] {
|
|
const result: HTMLImageElement[] = [];
|
|
|
|
for (const node of nodes) {
|
|
if (!nodeIsElement(node)) {
|
|
continue;
|
|
}
|
|
|
|
if (node.tagName === "IMG" && !(node as HTMLElement).dataset.anki) {
|
|
result.push(node as HTMLImageElement);
|
|
} else {
|
|
result.push(...filterImages(node.children));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function setImageRule(image: HTMLImageElement, rule: CSSStyleRule): void {
|
|
const aspectRatio = image.naturalWidth / image.naturalHeight;
|
|
|
|
if (restrictionAspectRatio - aspectRatio > 1) {
|
|
// restricted by height
|
|
rule.style.setProperty("width", "auto", "important");
|
|
|
|
const width = Number(image.getAttribute("width")) || image.width;
|
|
const height = Number(image.getAttribute("height")) || width / aspectRatio;
|
|
rule.style.setProperty(
|
|
"height",
|
|
height < maxHeight ? `${height}px` : "auto",
|
|
"important"
|
|
);
|
|
} else {
|
|
// square or restricted by width
|
|
const width = Number(image.getAttribute("width")) || image.width;
|
|
rule.style.setProperty(
|
|
"width",
|
|
width < maxWidth ? `${width}px` : "auto",
|
|
"important"
|
|
);
|
|
|
|
rule.style.setProperty("height", "auto", "important");
|
|
}
|
|
|
|
rule.style.setProperty("max-width", `min(${maxWidth}px, 100%)`, "important");
|
|
rule.style.setProperty("max-height", `${maxHeight}px`, "important");
|
|
}
|
|
|
|
function resetImageRule(rule: CSSStyleRule): void {
|
|
rule.style.removeProperty("width");
|
|
rule.style.removeProperty("height");
|
|
rule.style.removeProperty("max-width");
|
|
rule.style.removeProperty("max-height");
|
|
}
|
|
|
|
function addImage(image: HTMLImageElement): void {
|
|
if (!container.contains(image)) {
|
|
return;
|
|
}
|
|
|
|
images.push(image);
|
|
const index = sheet.insertRule(
|
|
`${createPath(image)} {}`,
|
|
sheet.cssRules.length
|
|
);
|
|
const rule = sheet.cssRules[index] as CSSStyleRule;
|
|
setImageRule(image, rule);
|
|
}
|
|
|
|
function addImageOnLoad(image: HTMLImageElement): void {
|
|
if (image.complete && image.naturalWidth !== 0 && image.naturalHeight !== 0) {
|
|
addImage(image);
|
|
} else {
|
|
image.addEventListener("load", () => {
|
|
addImage(image);
|
|
});
|
|
}
|
|
}
|
|
|
|
function removeImage(image: HTMLImageElement): void {
|
|
const index = images.indexOf(image);
|
|
if (index < 0) {
|
|
return;
|
|
}
|
|
|
|
images.splice(index, 1);
|
|
sheet.deleteRule(index);
|
|
}
|
|
|
|
const mutationObserver = new MutationObserver((mutations) => {
|
|
const addedImages = mutations.flatMap((mutation) =>
|
|
filterImages([...mutation.addedNodes])
|
|
);
|
|
|
|
for (const image of addedImages) {
|
|
addImageOnLoad(image);
|
|
}
|
|
|
|
const removedImages = mutations.flatMap((mutation) =>
|
|
filterImages([...mutation.removedNodes])
|
|
);
|
|
|
|
for (const image of removedImages) {
|
|
removeImage(image);
|
|
}
|
|
});
|
|
|
|
$: if (container) {
|
|
mutationObserver.observe(container, { childList: true, subtree: true });
|
|
}
|
|
|
|
export function toggleActualSize() {
|
|
const index = images.indexOf(activeImage!);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
|
|
const rule = sheet.cssRules[index] as CSSStyleRule;
|
|
active = !active;
|
|
|
|
if (active) {
|
|
resetImageRule(rule);
|
|
} else {
|
|
setImageRule(activeImage!, rule);
|
|
}
|
|
|
|
dispatch("update", active);
|
|
}
|
|
|
|
onDestroy(() => mutationObserver.disconnect());
|
|
</script>
|
|
|
|
{#if activeImage}
|
|
<slot {toggleActualSize} {active} />
|
|
{/if}
|