Change how resizable images work (#1859)

* Add ResizableImage.svelte in ts/editable

* Set image constrained via attributes instead of managed style sheet

* Implement new constrained size method

* Turn WithImageConstrained.svelte into normal ts file

* Introduce removeStyleProperties

Removes "style" attribute if all style properties were cleared

* Avoid --editor-width and use one variable set on container

* Disable shrinking if already smaller than shrunken size

* Add button to restore image to original size

* Don't allow restoring original size if no custom width set

* Bottom-center HandleLabel

* Satisfy svelte-check
This commit is contained in:
Henrik Giesel 2022-05-13 04:57:07 +02:00 committed by GitHub
parent fb5521eeed
commit de2cc20c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 225 additions and 349 deletions

View File

@ -15,6 +15,7 @@ editing-customize-card-templates = Customize Card Templates
editing-customize-fields = Customize Fields editing-customize-fields = Customize Fields
editing-cut = Cut editing-cut = Cut
editing-double-click-image = double-click image editing-double-click-image = double-click image
editing-double-click-to-expand = (double-click to expand)
editing-edit-current = Edit Current editing-edit-current = Edit Current
editing-edit-html = Edit HTML editing-edit-html = Edit HTML
editing-fields = Fields editing-fields = Fields
@ -38,6 +39,7 @@ editing-outdent = Decrease indent
editing-paste = Paste editing-paste = Paste
editing-record-audio = Record audio editing-record-audio = Record audio
editing-remove-formatting = Remove formatting editing-remove-formatting = Remove formatting
editing-restore-original-size = Restore original size
editing-select-remove-formatting = Select formatting to remove editing-select-remove-formatting = Select formatting to remove
editing-show-duplicates = Show Duplicates editing-show-duplicates = Show Duplicates
editing-subscript = Subscript editing-subscript = Subscript

View File

@ -0,0 +1,11 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<style lang="scss">
:global([data-editor-shrink]:not([data-editor-shrink="false"])) {
max-width: var(--editor-shrink-max-width);
max-height: var(--editor-shrink-max-height);
width: auto;
}
</style>

View File

@ -5,3 +5,4 @@ import "./editable-base.css";
/* only imported for the CSS */ /* only imported for the CSS */
import "./ContentEditable.svelte"; import "./ContentEditable.svelte";
import "./Mathjax.svelte"; import "./Mathjax.svelte";
import "./ResizableImage.svelte";

View File

@ -4,53 +4,32 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import { getContext } from "svelte"; import { getContext } from "svelte";
import { afterUpdate, createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import type { Readable } from "svelte/store"; import type { Readable } from "svelte/store";
import { directionKey } from "../lib/context-keys"; import { directionKey } from "../lib/context-keys";
let dimensions: HTMLDivElement;
let overflowFix = 0;
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey); const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
function updateOverflow(dimensions: HTMLDivElement): void {
const boundingClientRect = dimensions.getBoundingClientRect();
const overflow =
$direction === "ltr"
? boundingClientRect.x
: window.innerWidth - boundingClientRect.x - boundingClientRect.width;
overflowFix = Math.min(0, overflowFix + overflow, overflow);
}
afterUpdate(() => updateOverflow(dimensions));
function updateOverflowAsync(dimensions: HTMLDivElement): void {
setTimeout(() => updateOverflow(dimensions));
}
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
onMount(() => dispatch("mount")); onMount(() => dispatch("mount"));
</script> </script>
<div <div class="image-handle-dimensions" class:is-rtl={$direction === "rtl"}>
bind:this={dimensions}
class="image-handle-dimensions"
class:is-rtl={$direction === "rtl"}
style="--overflow-fix: {overflowFix}px"
use:updateOverflowAsync
>
<slot /> <slot />
</div> </div>
<style lang="scss"> <style lang="scss">
div { div {
position: absolute; position: absolute;
width: fit-content;
pointer-events: none; left: 0;
user-select: none; right: 0;
bottom: 3px;
margin-left: auto;
margin-right: auto;
font-size: 13px; font-size: 13px;
color: white; color: white;
@ -59,16 +38,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
border-radius: 5px; border-radius: 5px;
padding: 0 5px; padding: 0 5px;
bottom: 3px; pointer-events: none;
right: 3px; user-select: none;
margin-left: 3px;
margin-right: var(--overflow-fix, 0);
&.is-rtl {
right: auto;
left: 3px;
margin-right: 3px;
margin-left: var(--overflow-fix, 0);
}
} }
</style> </style>

View File

@ -339,7 +339,7 @@ the AddCards dialog) should be implemented in the user of this component.
}} }}
bind:this={richTextInputs[index]} bind:this={richTextInputs[index]}
> >
<ImageHandle /> <ImageHandle maxWidth={250} maxHeight={125} />
<MathjaxHandle /> <MathjaxHandle />
</RichTextInput> </RichTextInput>

View File

@ -9,9 +9,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { MatchType } from "../../domlib/surround"; import type { MatchType } from "../../domlib/surround";
import * as tr from "../../lib/ftl"; import * as tr from "../../lib/ftl";
import { getPlatformString } from "../../lib/shortcuts"; import { getPlatformString } from "../../lib/shortcuts";
import { removeStyleProperties } from "../../lib/styling";
import { context as noteEditorContext } from "../NoteEditor.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input"; import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround"; import { Surrounder } from "../surround";
import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { boldIcon } from "./icons"; import { boldIcon } from "./icons";
@ -25,9 +26,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const fontWeight = element.style.fontWeight; const fontWeight = element.style.fontWeight;
if (fontWeight === "bold" || Number(fontWeight) >= 400) { if (fontWeight === "bold" || Number(fontWeight) >= 400) {
return match.clear((): void => { return match.clear((): void => {
element.style.removeProperty("font-weight"); if (
removeStyleProperties(element, "font-weight") &&
if (removeEmptyStyle(element) && element.className.length === 0) { element.className.length === 0
) {
match.remove(); match.remove();
} }
}); });

View File

@ -11,9 +11,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} from "../../domlib/surround"; } from "../../domlib/surround";
import { bridgeCommand } from "../../lib/bridgecommand"; import { bridgeCommand } from "../../lib/bridgecommand";
import * as tr from "../../lib/ftl"; import * as tr from "../../lib/ftl";
import { removeStyleProperties } from "../../lib/styling";
import { context as noteEditorContext } from "../NoteEditor.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input"; import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround"; import { Surrounder } from "../surround";
import ColorPicker from "./ColorPicker.svelte"; import ColorPicker from "./ColorPicker.svelte";
import type { RemoveFormat } from "./EditorToolbar.svelte"; import type { RemoveFormat } from "./EditorToolbar.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte";
@ -45,9 +46,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
match.setCache(value); match.setCache(value);
match.clear((): void => { match.clear((): void => {
element.style.removeProperty("background-color"); if (
removeStyleProperties(element, "background-color") &&
if (removeEmptyStyle(element) && element.className.length === 0) { element.className.length === 0
) {
match.remove(); match.remove();
} }
}); });

View File

@ -9,9 +9,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { MatchType } from "../../domlib/surround"; import type { MatchType } from "../../domlib/surround";
import * as tr from "../../lib/ftl"; import * as tr from "../../lib/ftl";
import { getPlatformString } from "../../lib/shortcuts"; import { getPlatformString } from "../../lib/shortcuts";
import { removeStyleProperties } from "../../lib/styling";
import { context as noteEditorContext } from "../NoteEditor.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input"; import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround"; import { Surrounder } from "../surround";
import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { italicIcon } from "./icons"; import { italicIcon } from "./icons";
@ -24,9 +25,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (["italic", "oblique"].includes(element.style.fontStyle)) { if (["italic", "oblique"].includes(element.style.fontStyle)) {
return match.clear((): void => { return match.clear((): void => {
element.style.removeProperty("font-style"); if (
removeStyleProperties(element, "font-style") &&
if (removeEmptyStyle(element) && element.className.length === 0) { element.className.length === 0
) {
return match.remove(); return match.remove();
} }
}); });

View File

@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { MatchType } from "../../domlib/surround"; import type { MatchType } from "../../domlib/surround";
import { removeEmptyStyle } from "../surround"; import { removeStyleProperties } from "../../lib/styling";
const surroundElement = document.createElement("sub"); const surroundElement = document.createElement("sub");
@ -15,9 +15,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (element.style.verticalAlign === "sub") { if (element.style.verticalAlign === "sub") {
return match.clear((): void => { return match.clear((): void => {
element.style.removeProperty("vertical-align"); if (
removeStyleProperties(element, "vertical-align") &&
if (removeEmptyStyle(element) && element.className.length === 0) { element.className.length === 0
) {
return match.remove(); return match.remove();
} }
}); });

View File

@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script context="module" lang="ts"> <script context="module" lang="ts">
import type { MatchType } from "../../domlib/surround"; import type { MatchType } from "../../domlib/surround";
import { removeEmptyStyle } from "../surround"; import { removeStyleProperties } from "../../lib/styling";
const surroundElement = document.createElement("sup"); const surroundElement = document.createElement("sup");
@ -15,9 +15,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (element.style.verticalAlign === "super") { if (element.style.verticalAlign === "super") {
return match.clear((): void => { return match.clear((): void => {
element.style.removeProperty("vertical-align"); if (
removeStyleProperties(element, "vertical-align") &&
if (removeEmptyStyle(element) && element.className.length === 0) { element.className.length === 0
) {
return match.remove(); return match.remove();
} }
}); });

View File

@ -13,10 +13,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { bridgeCommand } from "../../lib/bridgecommand"; import { bridgeCommand } from "../../lib/bridgecommand";
import * as tr from "../../lib/ftl"; import * as tr from "../../lib/ftl";
import { getPlatformString } from "../../lib/shortcuts"; import { getPlatformString } from "../../lib/shortcuts";
import { removeStyleProperties } from "../../lib/styling";
import { withFontColor } from "../helpers"; import { withFontColor } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte";
import { editingInputIsRichText } from "../rich-text-input"; import { editingInputIsRichText } from "../rich-text-input";
import { removeEmptyStyle, Surrounder } from "../surround"; import { Surrounder } from "../surround";
import ColorPicker from "./ColorPicker.svelte"; import ColorPicker from "./ColorPicker.svelte";
import type { RemoveFormat } from "./EditorToolbar.svelte"; import type { RemoveFormat } from "./EditorToolbar.svelte";
import { context as editorToolbarContext } from "./EditorToolbar.svelte"; import { context as editorToolbarContext } from "./EditorToolbar.svelte";
@ -59,9 +60,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
match.setCache(value); match.setCache(value);
match.clear((): void => { match.clear((): void => {
element.style.removeProperty("color"); if (
removeStyleProperties(element, "color") &&
if (removeEmptyStyle(element) && element.className.length === 0) { element.className.length === 0
) {
match.remove(); match.remove();
} }
}); });

View File

@ -7,6 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonDropdown from "../../components/ButtonDropdown.svelte"; import ButtonDropdown from "../../components/ButtonDropdown.svelte";
import WithDropdown from "../../components/WithDropdown.svelte"; import WithDropdown from "../../components/WithDropdown.svelte";
import * as tr from "../../lib/ftl";
import HandleBackground from "../HandleBackground.svelte"; import HandleBackground from "../HandleBackground.svelte";
import HandleControl from "../HandleControl.svelte"; import HandleControl from "../HandleControl.svelte";
import HandleLabel from "../HandleLabel.svelte"; import HandleLabel from "../HandleLabel.svelte";
@ -14,16 +15,33 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { context } from "../rich-text-input"; import { context } from "../rich-text-input";
import FloatButtons from "./FloatButtons.svelte"; import FloatButtons from "./FloatButtons.svelte";
import SizeSelect from "./SizeSelect.svelte"; import SizeSelect from "./SizeSelect.svelte";
import WithImageConstrained from "./WithImageConstrained.svelte";
const { container, styles } = context.get(); export let maxWidth: number;
export let maxHeight: number;
const sheetPromise = styles const { container } = context.get();
.addStyleTag("imageOverlay")
.then((styleObject) => styleObject.element.sheet!); $: {
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
}
let activeImage: HTMLImageElement | null = null; let activeImage: HTMLImageElement | null = null;
/**
* For element dataset attributes which work like the contenteditable attribute
*/
function isDatasetAttributeFlagSet(
element: HTMLElement | SVGElement,
attribute: string,
): boolean {
return attribute in element.dataset && element.dataset[attribute] !== "false";
}
$: isSizeConstrained = activeImage
? isDatasetAttributeFlagSet(activeImage, "editorShrink")
: false;
async function resetHandle(): Promise<void> { async function resetHandle(): Promise<void> {
activeImage = null; activeImage = null;
await tick(); await tick();
@ -166,6 +184,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
activeImage!.width = width; activeImage!.width = width;
} }
function toggleActualSize(): void {
if (isSizeConstrained) {
delete activeImage!.dataset.editorShrink;
} else {
activeImage!.dataset.editorShrink = "true";
}
isSizeConstrained = !isSizeConstrained;
}
function clearActualSize(): void {
activeImage!.removeAttribute("width");
}
onDestroy(() => { onDestroy(() => {
resizeObserver.disconnect(); resizeObserver.disconnect();
container.removeEventListener("click", maybeShowHandle); container.removeEventListener("click", maybeShowHandle);
@ -173,6 +205,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
container.removeEventListener("key", resetHandle); container.removeEventListener("key", resetHandle);
container.removeEventListener("paste", resetHandle); container.removeEventListener("paste", resetHandle);
}); });
let shrinkingDisabled: boolean;
$: shrinkingDisabled =
Number(actualWidth) <= maxWidth && Number(actualHeight) <= maxHeight;
let restoringDisabled: boolean;
$: restoringDisabled = !activeImage?.hasAttribute("width") ?? true;
const widthObserver = new MutationObserver(
() => (restoringDisabled = !activeImage!.hasAttribute("width")),
);
$: activeImage
? widthObserver.observe(activeImage, {
attributes: true,
attributeFilter: ["width"],
})
: widthObserver.disconnect();
</script> </script>
<WithDropdown <WithDropdown
@ -183,62 +233,69 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:createDropdown let:createDropdown
let:dropdownObject let:dropdownObject
> >
{#await sheetPromise then sheet} {#if activeImage}
<WithImageConstrained <HandleSelection
{sheet} bind:updateSelection
{container} {container}
{activeImage} image={activeImage}
maxWidth={250} on:mount={(event) => createDropdown(event.detail.selection)}
maxHeight={125}
on:update={() => {
updateSizesWithDimensions();
dropdownObject.update();
}}
let:toggleActualSize
let:active
> >
{#if activeImage} <HandleBackground
<HandleSelection on:dblclick={() => {
bind:updateSelection if (shrinkingDisabled) {
{container} return;
image={activeImage} }
on:mount={(event) => createDropdown(event.detail.selection)} toggleActualSize();
> updateSizesWithDimensions();
<HandleBackground on:dblclick={toggleActualSize} /> dropdownObject.update();
}}
/>
<HandleLabel on:mount={updateDimensions}> <HandleLabel on:mount={updateDimensions}>
<span>{actualWidth}&times;{actualHeight}</span> {#if isSizeConstrained}
{#if customDimensions} <span>{tr.editingDoubleClickToExpand()}</span>
<span>(Original: {naturalWidth}&times;{naturalHeight})</span {:else}
> <span>{actualWidth}&times;{actualHeight}</span>
{/if} {#if customDimensions}
</HandleLabel> <span>(Original: {naturalWidth}&times;{naturalHeight})</span>
{/if}
{/if}
</HandleLabel>
<HandleControl <HandleControl
{active} active={!isSizeConstrained}
activeSize={8} activeSize={8}
offsetX={5} offsetX={5}
offsetY={5} offsetY={5}
on:pointerclick={(event) => { on:pointerclick={(event) => {
if (active) { if (!isSizeConstrained) {
setPointerCapture(event); setPointerCapture(event);
} }
}} }}
on:pointermove={(event) => { on:pointermove={(event) => {
resize(event); resize(event);
updateSizesWithDimensions(); updateSizesWithDimensions();
dropdownObject.update(); dropdownObject.update();
}} }}
/> />
</HandleSelection> </HandleSelection>
<ButtonDropdown on:click={updateSizesWithDimensions}> <ButtonDropdown on:click={updateSizesWithDimensions}>
<FloatButtons <FloatButtons image={activeImage} on:update={dropdownObject.update} />
image={activeImage} <SizeSelect
on:update={dropdownObject.update} {shrinkingDisabled}
/> {restoringDisabled}
<SizeSelect {active} on:click={toggleActualSize} /> {isSizeConstrained}
</ButtonDropdown> on:imagetoggle={() => {
{/if} toggleActualSize();
</WithImageConstrained> updateSizesWithDimensions();
{/await} dropdownObject.update();
}}
on:imageclear={() => {
clearActualSize();
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
</ButtonDropdown>
{/if}
</WithDropdown> </WithDropdown>

View File

@ -3,29 +3,38 @@ 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 { getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import type { Readable } from "svelte/store"; import type { Readable } from "svelte/store";
import ButtonGroup from "../../components/ButtonGroup.svelte"; import ButtonGroup from "../../components/ButtonGroup.svelte";
import IconButton from "../../components/IconButton.svelte"; import IconButton from "../../components/IconButton.svelte";
import { directionKey } from "../../lib/context-keys"; import { directionKey } from "../../lib/context-keys";
import * as tr from "../../lib/ftl"; import * as tr from "../../lib/ftl";
import { sizeActual, sizeMinimized } from "./icons"; import { sizeActual, sizeClear, sizeMinimized } from "./icons";
export let active: boolean; export let isSizeConstrained: boolean;
export let shrinkingDisabled: boolean;
export let restoringDisabled: boolean;
$: icon = active ? sizeActual : sizeMinimized; $: icon = isSizeConstrained ? sizeMinimized : sizeActual;
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey); const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
const dispatch = createEventDispatcher();
</script> </script>
<ButtonGroup size={1.6}> <ButtonGroup size={1.6}>
<IconButton <IconButton
{active} disabled={shrinkingDisabled}
flipX={$direction === "rtl"} flipX={$direction === "rtl"}
tooltip="{tr.editingActualSize()} ({tr.editingDoubleClickImage()})" tooltip="{tr.editingActualSize()} ({tr.editingDoubleClickImage()})"
on:click on:click={() => dispatch("imagetoggle")}
--border-left-radius="5px" --border-left-radius="5px">{@html icon}</IconButton
--border-right-radius="5px">{@html icon}</IconButton >
<IconButton
disabled={restoringDisabled}
tooltip={tr.editingRestoreOriginalSize()}
on:click={() => dispatch("imageclear")}
--border-right-radius="5px">{@html sizeClear}</IconButton
> >
</ButtonGroup> </ButtonGroup>

View File

@ -1,203 +0,0 @@
<!--
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}

View File

@ -4,5 +4,6 @@
export { default as floatLeftIcon } from "@mdi/svg/svg/format-float-left.svg"; export { default as floatLeftIcon } from "@mdi/svg/svg/format-float-left.svg";
export { default as floatNoneIcon } from "@mdi/svg/svg/format-float-none.svg"; export { default as floatNoneIcon } from "@mdi/svg/svg/format-float-none.svg";
export { default as floatRightIcon } from "@mdi/svg/svg/format-float-right.svg"; export { default as floatRightIcon } from "@mdi/svg/svg/format-float-right.svg";
export { default as sizeClear } from "@mdi/svg/svg/image-remove.svg";
export { default as sizeActual } from "@mdi/svg/svg/image-size-select-actual.svg"; export { default as sizeActual } from "@mdi/svg/svg/image-size-select-actual.svg";
export { default as sizeMinimized } from "@mdi/svg/svg/image-size-select-large.svg"; export { default as sizeMinimized } from "@mdi/svg/svg/image-size-select-large.svg";

View File

@ -243,19 +243,6 @@ export class Surrounder {
} }
} }
/**
* @returns True, if element has no style attribute (anymore).
*/
export function removeEmptyStyle(element: HTMLElement | SVGElement): boolean {
if (element.style.cssText.length === 0) {
element.removeAttribute("style");
// Calling `.hasAttribute` right after `.removeAttribute` might return true.
return true;
}
return false;
}
registerPackage("anki/surround", { registerPackage("anki/surround", {
Surrounder, Surrounder,
}); });

31
ts/lib/styling.ts Normal file
View File

@ -0,0 +1,31 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/**
* @returns True, if element has no style attribute (anymore).
*/
function removeEmptyStyle(element: HTMLElement | SVGElement): boolean {
if (element.style.cssText.length === 0) {
element.removeAttribute("style");
// Calling `.hasAttribute` right after `.removeAttribute` might return true.
return true;
}
return false;
}
/**
* Will remove the style attribute, if all properties were removed.
*
* @returns True, if element has no style attributes anymore
*/
export function removeStyleProperties(
element: HTMLElement | SVGElement,
...props: string[]
): boolean {
for (const prop of props) {
element.style.removeProperty(prop);
}
return removeEmptyStyle(element);
}