mirror of
https://github.com/ankitects/anki.git
synced 2025-11-15 09:07:11 -05: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>
|
</script>
|
||||||
|
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<slot {createDropdown} />
|
<slot {createDropdown} dropdownObject={dropdown} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,16 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
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">
|
<style lang="scss">
|
||||||
div {
|
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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
export let container: HTMLElement;
|
export let container: HTMLElement;
|
||||||
export let activeImage: HTMLImageElement | null;
|
export let image: HTMLImageElement;
|
||||||
|
|
||||||
export let offsetX = 0;
|
export let offsetX = 0;
|
||||||
export let offsetY = 0;
|
export let offsetY = 0;
|
||||||
|
|
||||||
$: if (activeImage) {
|
|
||||||
updateSelection();
|
|
||||||
} else {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
let left: number;
|
let left: number;
|
||||||
let top: number;
|
let top: number;
|
||||||
let width: number;
|
let width: number;
|
||||||
let height: number;
|
let height: number;
|
||||||
|
|
||||||
export function updateSelection() {
|
export function updateSelection(_div: HTMLDivElement): void {
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const imageRect = activeImage!.getBoundingClientRect();
|
const imageRect = image!.getBoundingClientRect();
|
||||||
|
|
||||||
const containerLeft = containerRect.left;
|
const containerLeft = containerRect.left;
|
||||||
const containerTop = containerRect.top;
|
const containerTop = containerRect.top;
|
||||||
|
|
||||||
left = imageRect!.left - containerLeft;
|
left = imageRect!.left - containerLeft;
|
||||||
top = imageRect!.top - containerTop;
|
top = imageRect!.top - containerTop;
|
||||||
width = activeImage!.clientWidth;
|
width = image!.clientWidth;
|
||||||
height = activeImage!.clientHeight;
|
height = image!.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
const dispatch = createEventDispatcher();
|
||||||
activeImage = null;
|
let selection: HTMLDivElement;
|
||||||
|
|
||||||
left = 0;
|
onMount(() => dispatch("mount", { selection }));
|
||||||
top = 0;
|
|
||||||
width = 0;
|
|
||||||
height = 0;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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;"
|
style="--left: {left}px; --top: {top}px; --width: {width}px; --height: {height}px; --offsetX: {offsetX}px; --offsetY: {offsetY}px;"
|
||||||
>
|
>
|
||||||
<slot />
|
<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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<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 HandleBackground from "./HandleBackground.svelte";
|
||||||
import HandleSelection from "./HandleSelection.svelte";
|
import HandleSelection from "./HandleSelection.svelte";
|
||||||
import HandleControl from "./HandleControl.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;
|
$: naturalWidth = activeImage?.naturalWidth;
|
||||||
$: naturalHeight = activeImage?.naturalHeight;
|
$: naturalHeight = activeImage?.naturalHeight;
|
||||||
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
||||||
$: showFloat = activeImage ? Number(activeImage!.width) >= 100 : false;
|
|
||||||
|
|
||||||
let customDimensions: boolean = false;
|
let customDimensions: boolean = false;
|
||||||
let actualWidth = "";
|
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;
|
let getDragHeight: (event: PointerEvent) => number;
|
||||||
|
|
||||||
function setPointerCapture({ detail }: CustomEvent): void {
|
function setPointerCapture({ detail }: CustomEvent): void {
|
||||||
if (!active || detail.originalEvent.pointerId !== 1) {
|
if (detail.originalEvent.pointerId !== 1) {
|
||||||
return;
|
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!));
|
width = Math.trunc(naturalWidth! * (height / naturalHeight!));
|
||||||
}
|
}
|
||||||
|
|
||||||
showFloat = width >= 100;
|
|
||||||
activeImage!.width = width;
|
activeImage!.width = width;
|
||||||
|
|
||||||
await updateSizesWithDimensions();
|
await updateSizesWithDimensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
let toggleActualSize: () => void;
|
|
||||||
let active = false;
|
|
||||||
|
|
||||||
onDestroy(() => resizeObserver.disconnect());
|
onDestroy(() => resizeObserver.disconnect());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<HandleSelection bind:updateSelection {container} {activeImage}>
|
{#if sheet}
|
||||||
{#if activeImage}
|
<WithImageConstrained
|
||||||
<HandleBackground on:dblclick={toggleActualSize} />
|
{sheet}
|
||||||
{/if}
|
{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}
|
<HandleLabel {isRtl} on:mount={updateDimensions}>
|
||||||
<div class="image-handle-size-select" class:is-rtl={isRtl}>
|
<span>{actualWidth}×{actualHeight}</span>
|
||||||
<ImageHandleSizeSelect
|
{#if customDimensions}
|
||||||
bind:toggleActualSize
|
<span>(Original: {naturalWidth}×{naturalHeight})</span
|
||||||
bind:active
|
>
|
||||||
{container}
|
{/if}
|
||||||
{sheet}
|
</HandleLabel>
|
||||||
{activeImage}
|
|
||||||
{isRtl}
|
|
||||||
on:update={updateSizesWithDimensions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if activeImage}
|
<HandleControl
|
||||||
{#if showFloat}
|
{active}
|
||||||
<div
|
activeSize={7}
|
||||||
class="image-handle-float"
|
offsetX={5}
|
||||||
class:is-rtl={isRtl}
|
offsetY={5}
|
||||||
on:click={updateSizesWithDimensions}
|
on:pointerclick={(event) => {
|
||||||
>
|
if (active) {
|
||||||
<ImageHandleFloat {activeImage} {isRtl} />
|
setPointerCapture(event);
|
||||||
</div>
|
}
|
||||||
|
}}
|
||||||
|
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}
|
{/if}
|
||||||
|
</WithImageConstrained>
|
||||||
<HandleLabel {isRtl} on:mount={updateDimensions}>
|
{/if}
|
||||||
<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>
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { floatNoneIcon, floatLeftIcon, floatRightIcon } from "./icons";
|
import { floatNoneIcon, floatLeftIcon, floatRightIcon } from "./icons";
|
||||||
|
|
||||||
export let activeImage: HTMLImageElement;
|
export let image: HTMLImageElement;
|
||||||
export let isRtl: boolean;
|
export let isRtl: boolean;
|
||||||
|
|
||||||
const leftValues = {
|
const leftValues = {
|
||||||
|
|
@ -33,20 +33,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={tr.editingFloatNone()}
|
tooltip={tr.editingFloatNone()}
|
||||||
active={activeImage.style.float === "" ||
|
active={image.style.float === "" || image.style.float === "none"}
|
||||||
activeImage.style.float === "none"}
|
|
||||||
flipX={isRtl}
|
flipX={isRtl}
|
||||||
on:click={() => (activeImage.style.float = "")}
|
on:click={() => (image.style.float = "")}>{@html floatNoneIcon}</IconButton
|
||||||
>{@html floatNoneIcon}</IconButton
|
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={inlineStart.label}
|
tooltip={inlineStart.label}
|
||||||
active={activeImage.style.float === inlineStart.position}
|
active={image.style.float === inlineStart.position}
|
||||||
flipX={isRtl}
|
flipX={isRtl}
|
||||||
on:click={() => (activeImage.style.float = inlineStart.position)}
|
on:click={() => (image.style.float = inlineStart.position)}
|
||||||
>{@html floatLeftIcon}</IconButton
|
>{@html floatLeftIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
@ -54,9 +52,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={inlineEnd.label}
|
tooltip={inlineEnd.label}
|
||||||
active={activeImage.style.float === inlineEnd.position}
|
active={image.style.float === inlineEnd.position}
|
||||||
flipX={isRtl}
|
flipX={isRtl}
|
||||||
on:click={() => (activeImage.style.float = inlineEnd.position)}
|
on:click={() => (image.style.float = inlineEnd.position)}
|
||||||
>{@html floatRightIcon}</IconButton
|
>{@html floatRightIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</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 ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||||
import IconButton from "components/IconButton.svelte";
|
import IconButton from "components/IconButton.svelte";
|
||||||
|
|
||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
|
||||||
import { sizeActual, sizeMinimized } from "./icons";
|
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 isRtl: boolean;
|
||||||
export let maxWidth = 250;
|
|
||||||
export let maxHeight = 125;
|
|
||||||
|
|
||||||
$: restrictionAspectRatio = maxWidth / maxHeight;
|
$: icon = active ? sizeActual : sizeMinimized;
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
{#if activeImage}
|
<ButtonGroup size={1.6}>
|
||||||
<ButtonGroup size={1.6}>
|
<ButtonGroupItem>
|
||||||
<ButtonGroupItem>
|
<IconButton {active} flipX={isRtl} tooltip={tr.editingActualSize()} on:click
|
||||||
<IconButton
|
>{@html icon}</IconButton
|
||||||
{active}
|
>
|
||||||
flipX={isRtl}
|
</ButtonGroupItem>
|
||||||
tooltip={tr.editingActualSize()}
|
</ButtonGroup>
|
||||||
on:click={toggleActualSize}>{@html icon}</IconButton
|
|
||||||
>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
</ButtonGroup>
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
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