mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Introduce WithImageConstrained
This commit is contained in:
parent
017b6f9ff1
commit
3579b6a3b6
7 changed files with 302 additions and 306 deletions
|
@ -28,7 +28,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<div class="dropdown">
|
||||
<slot {createDropdown} />
|
||||
<slot {createDropdown} dropdownObject={dropdown} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -2,7 +2,16 @@
|
|||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<div on:mousedown|preventDefault on:click on:dblclick />
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
|
||||
let background: HTMLDivElement;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => dispatch("mount", { background }));
|
||||
</script>
|
||||
|
||||
<div bind:this={background} on:mousedown|preventDefault on:click on:dblclick />
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
|
|
|
@ -3,47 +3,40 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
export let container: HTMLElement;
|
||||
export let activeImage: HTMLImageElement | null;
|
||||
export let image: HTMLImageElement;
|
||||
|
||||
export let offsetX = 0;
|
||||
export let offsetY = 0;
|
||||
|
||||
$: if (activeImage) {
|
||||
updateSelection();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
|
||||
let left: number;
|
||||
let top: number;
|
||||
let width: number;
|
||||
let height: number;
|
||||
|
||||
export function updateSelection() {
|
||||
export function updateSelection(_div: HTMLDivElement): void {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const imageRect = activeImage!.getBoundingClientRect();
|
||||
const imageRect = image!.getBoundingClientRect();
|
||||
|
||||
const containerLeft = containerRect.left;
|
||||
const containerTop = containerRect.top;
|
||||
|
||||
left = imageRect!.left - containerLeft;
|
||||
top = imageRect!.top - containerTop;
|
||||
width = activeImage!.clientWidth;
|
||||
height = activeImage!.clientHeight;
|
||||
width = image!.clientWidth;
|
||||
height = image!.clientHeight;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
activeImage = null;
|
||||
const dispatch = createEventDispatcher();
|
||||
let selection: HTMLDivElement;
|
||||
|
||||
left = 0;
|
||||
top = 0;
|
||||
width = 0;
|
||||
height = 0;
|
||||
}
|
||||
onMount(() => dispatch("mount", { selection }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={selection}
|
||||
use:updateSelection
|
||||
style="--left: {left}px; --top: {top}px; --width: {width}px; --height: {height}px; --offsetX: {offsetX}px; --offsetY: {offsetY}px;"
|
||||
>
|
||||
<slot />
|
||||
|
|
|
@ -3,6 +3,11 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import WithDropdown from "components/WithDropdown.svelte";
|
||||
import ButtonDropdown from "components/ButtonDropdown.svelte";
|
||||
import Item from "components/Item.svelte";
|
||||
|
||||
import WithImageConstrained from "./WithImageConstrained.svelte";
|
||||
import HandleBackground from "./HandleBackground.svelte";
|
||||
import HandleSelection from "./HandleSelection.svelte";
|
||||
import HandleControl from "./HandleControl.svelte";
|
||||
|
@ -20,7 +25,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
$: naturalWidth = activeImage?.naturalWidth;
|
||||
$: naturalHeight = activeImage?.naturalHeight;
|
||||
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
||||
$: showFloat = activeImage ? Number(activeImage!.width) >= 100 : false;
|
||||
|
||||
let customDimensions: boolean = false;
|
||||
let actualWidth = "";
|
||||
|
@ -79,7 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let getDragHeight: (event: PointerEvent) => number;
|
||||
|
||||
function setPointerCapture({ detail }: CustomEvent): void {
|
||||
if (!active || detail.originalEvent.pointerId !== 1) {
|
||||
if (detail.originalEvent.pointerId !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -128,89 +132,73 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
width = Math.trunc(naturalWidth! * (height / naturalHeight!));
|
||||
}
|
||||
|
||||
showFloat = width >= 100;
|
||||
activeImage!.width = width;
|
||||
|
||||
await updateSizesWithDimensions();
|
||||
}
|
||||
|
||||
let toggleActualSize: () => void;
|
||||
let active = false;
|
||||
|
||||
onDestroy(() => resizeObserver.disconnect());
|
||||
</script>
|
||||
|
||||
<HandleSelection bind:updateSelection {container} {activeImage}>
|
||||
{#if activeImage}
|
||||
<HandleBackground on:dblclick={toggleActualSize} />
|
||||
{/if}
|
||||
{#if sheet}
|
||||
<WithImageConstrained
|
||||
{sheet}
|
||||
{container}
|
||||
{activeImage}
|
||||
on:update={updateSizesWithDimensions}
|
||||
let:toggleActualSize
|
||||
let:active
|
||||
>
|
||||
{#if activeImage}
|
||||
<WithDropdown let:createDropdown>
|
||||
<HandleSelection
|
||||
bind:updateSelection
|
||||
{container}
|
||||
image={activeImage}
|
||||
on:mount={(event) => createDropdown(event.detail.selection)}
|
||||
>
|
||||
<HandleBackground
|
||||
on:dblclick={toggleActualSize}
|
||||
on:mount={(event) => createDropdown(event.detail.background)}
|
||||
/>
|
||||
|
||||
{#if sheet}
|
||||
<div class="image-handle-size-select" class:is-rtl={isRtl}>
|
||||
<ImageHandleSizeSelect
|
||||
bind:toggleActualSize
|
||||
bind:active
|
||||
{container}
|
||||
{sheet}
|
||||
{activeImage}
|
||||
{isRtl}
|
||||
on:update={updateSizesWithDimensions}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<HandleLabel {isRtl} on:mount={updateDimensions}>
|
||||
<span>{actualWidth}×{actualHeight}</span>
|
||||
{#if customDimensions}
|
||||
<span>(Original: {naturalWidth}×{naturalHeight})</span
|
||||
>
|
||||
{/if}
|
||||
</HandleLabel>
|
||||
|
||||
{#if activeImage}
|
||||
{#if showFloat}
|
||||
<div
|
||||
class="image-handle-float"
|
||||
class:is-rtl={isRtl}
|
||||
on:click={updateSizesWithDimensions}
|
||||
>
|
||||
<ImageHandleFloat {activeImage} {isRtl} />
|
||||
</div>
|
||||
<HandleControl
|
||||
{active}
|
||||
activeSize={7}
|
||||
offsetX={5}
|
||||
offsetY={5}
|
||||
on:pointerclick={(event) => {
|
||||
if (active) {
|
||||
setPointerCapture(event);
|
||||
}
|
||||
}}
|
||||
on:pointerup={startObserving}
|
||||
on:pointermove={resize}
|
||||
/>
|
||||
</HandleSelection>
|
||||
<ButtonDropdown>
|
||||
<div on:click={updateSizesWithDimensions}>
|
||||
<Item>
|
||||
<ImageHandleFloat image={activeImage} {isRtl} />
|
||||
</Item>
|
||||
<Item>
|
||||
<ImageHandleSizeSelect
|
||||
{active}
|
||||
{isRtl}
|
||||
on:click={toggleActualSize}
|
||||
/>
|
||||
</Item>
|
||||
</div>
|
||||
</ButtonDropdown>
|
||||
</WithDropdown>
|
||||
{/if}
|
||||
|
||||
<HandleLabel {isRtl} on:mount={updateDimensions}>
|
||||
<span>{actualWidth}×{actualHeight}</span>
|
||||
{#if customDimensions}
|
||||
<span>(Original: {naturalWidth}×{naturalHeight})</span>
|
||||
{/if}
|
||||
</HandleLabel>
|
||||
|
||||
<HandleControl
|
||||
{active}
|
||||
activeSize={7}
|
||||
offsetX={5}
|
||||
offsetY={5}
|
||||
on:pointerclick={setPointerCapture}
|
||||
on:pointerup={startObserving}
|
||||
on:pointermove={resize}
|
||||
/>
|
||||
{/if}
|
||||
</HandleSelection>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.image-handle-float {
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
|
||||
&.is-rtl {
|
||||
left: auto;
|
||||
right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-handle-size-select {
|
||||
right: 3px;
|
||||
top: 3px;
|
||||
|
||||
&.is-rtl {
|
||||
right: auto;
|
||||
left: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</WithImageConstrained>
|
||||
{/if}
|
||||
|
|
|
@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import { floatNoneIcon, floatLeftIcon, floatRightIcon } from "./icons";
|
||||
|
||||
export let activeImage: HTMLImageElement;
|
||||
export let image: HTMLImageElement;
|
||||
export let isRtl: boolean;
|
||||
|
||||
const leftValues = {
|
||||
|
@ -33,20 +33,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatNone()}
|
||||
active={activeImage.style.float === "" ||
|
||||
activeImage.style.float === "none"}
|
||||
active={image.style.float === "" || image.style.float === "none"}
|
||||
flipX={isRtl}
|
||||
on:click={() => (activeImage.style.float = "")}
|
||||
>{@html floatNoneIcon}</IconButton
|
||||
on:click={() => (image.style.float = "")}>{@html floatNoneIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={inlineStart.label}
|
||||
active={activeImage.style.float === inlineStart.position}
|
||||
active={image.style.float === inlineStart.position}
|
||||
flipX={isRtl}
|
||||
on:click={() => (activeImage.style.float = inlineStart.position)}
|
||||
on:click={() => (image.style.float = inlineStart.position)}
|
||||
>{@html floatLeftIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
@ -54,9 +52,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={inlineEnd.label}
|
||||
active={activeImage.style.float === inlineEnd.position}
|
||||
active={image.style.float === inlineEnd.position}
|
||||
flipX={isRtl}
|
||||
on:click={() => (activeImage.style.float = inlineEnd.position)}
|
||||
on:click={() => (image.style.float = inlineEnd.position)}
|
||||
>{@html floatRightIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
|
|
@ -9,209 +9,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import { sizeActual, sizeMinimized } from "./icons";
|
||||
import { nodeIsElement } from "./helpers";
|
||||
|
||||
export let container: HTMLElement;
|
||||
export let sheet: CSSStyleSheet;
|
||||
|
||||
export let activeImage: HTMLImageElement | null;
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
$: icon = active ? sizeActual : sizeMinimized;
|
||||
|
||||
export let active: boolean;
|
||||
export let isRtl: boolean;
|
||||
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") {
|
||||
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());
|
||||
$: icon = active ? sizeActual : sizeMinimized;
|
||||
</script>
|
||||
|
||||
{#if activeImage}
|
||||
<ButtonGroup size={1.6}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
{active}
|
||||
flipX={isRtl}
|
||||
tooltip={tr.editingActualSize()}
|
||||
on:click={toggleActualSize}>{@html icon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
{/if}
|
||||
<ButtonGroup size={1.6}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton {active} flipX={isRtl} tooltip={tr.editingActualSize()} on:click
|
||||
>{@html icon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
|
|
199
ts/editor/WithImageConstrained.svelte
Normal file
199
ts/editor/WithImageConstrained.svelte
Normal file
|
@ -0,0 +1,199 @@
|
|||
<!--
|
||||
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 "./helpers";
|
||||
|
||||
export let container: HTMLElement;
|
||||
export let sheet: CSSStyleSheet;
|
||||
|
||||
export let activeImage: HTMLImageElement | null;
|
||||
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") {
|
||||
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}
|
Loading…
Reference in a new issue