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:
parent
fb5521eeed
commit
de2cc20c59
@ -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
|
||||||
|
11
ts/editable/ResizableImage.svelte
Normal file
11
ts/editable/ResizableImage.svelte
Normal 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>
|
@ -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";
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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}×{actualHeight}</span>
|
{#if isSizeConstrained}
|
||||||
{#if customDimensions}
|
<span>{tr.editingDoubleClickToExpand()}</span>
|
||||||
<span>(Original: {naturalWidth}×{naturalHeight})</span
|
{:else}
|
||||||
>
|
<span>{actualWidth}×{actualHeight}</span>
|
||||||
{/if}
|
{#if customDimensions}
|
||||||
</HandleLabel>
|
<span>(Original: {naturalWidth}×{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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
|
@ -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";
|
||||||
|
@ -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
31
ts/lib/styling.ts
Normal 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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user