anki/ts/editor/image-overlay/WithImageConstrained.svelte

203 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="ts">
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: number;
export let maxHeight: number;
$: 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(" > ");
}
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);
}
});
mutationObserver.observe(container, {
childList: true,
subtree: true,
});
for (const image of filterImages([...container.childNodes])) {
addImageOnLoad(image);
}
onDestroy(() => mutationObserver.disconnect());
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);
}
</script>
{#if activeImage}
<slot {toggleActualSize} {active} />
{/if}