Introduce WithImageConstrained

This commit is contained in:
Henrik Giesel 2021-08-06 02:19:36 +02:00 committed by Damien Elmes
parent 017b6f9ff1
commit 3579b6a3b6
7 changed files with 302 additions and 306 deletions

View file

@ -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">

View file

@ -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 {

View file

@ -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 />

View file

@ -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}&times;{actualHeight}</span>
{#if customDimensions}
<span>(Original: {naturalWidth}&times;{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}&times;{actualHeight}</span>
{#if customDimensions}
<span>(Original: {naturalWidth}&times;{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}

View file

@ -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>

View file

@ -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>

View 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}