Join RichTextAPI and RichTextContextAPI + Expose anki/RichTextInput (#1918)

* Format scss correctly so it passes ts:format

* Use on and singleCallback in ImageHandle and MathjaxHandle

* Add a few comments

* Fix relict of partial commit

* Fix 'element not found' in ImageHandle

* Remove setting css on image handle twice

* Remove use of container in ImageHandle

* Remove use of container in MathjaxHandle

* Use unprefixed properties of RichTextInputAPI

* Inline api to get to RichTextInputAPI

* Join customStyles into RichTextInputAPI

* Export RichTextInput; Remove SetContext

* Address eslint and svelte_check
This commit is contained in:
Henrik Giesel 2022-06-20 08:11:27 +02:00 committed by GitHub
parent 336ad05693
commit 3969487e77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 136 deletions

View file

@ -23,7 +23,8 @@ html {
overflow-x: hidden;
}
html, body {
html,
body {
height: 100%;
}

View file

@ -3,11 +3,13 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onDestroy, tick } from "svelte";
import { onMount, tick } from "svelte";
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
import WithDropdown from "../../components/WithDropdown.svelte";
import { on } from "../../lib/events";
import * as tr from "../../lib/ftl";
import { singleCallback } from "../../lib/typing";
import HandleBackground from "../HandleBackground.svelte";
import HandleControl from "../HandleControl.svelte";
import HandleLabel from "../HandleLabel.svelte";
@ -19,12 +21,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let maxWidth: number;
export let maxHeight: number;
const { container } = context.get();
$: {
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
}
const { element } = context.get();
let activeImage: HTMLImageElement | null = null;
@ -59,11 +56,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
container.addEventListener("click", maybeShowHandle);
container.addEventListener("blur", resetHandle);
container.addEventListener("key", resetHandle);
container.addEventListener("paste", resetHandle);
$: naturalWidth = activeImage?.naturalWidth;
$: naturalHeight = activeImage?.naturalHeight;
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
@ -108,12 +100,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
});
$: observes = Boolean(activeImage);
$: if (observes) {
resizeObserver.observe(container);
} else {
resizeObserver.unobserve(container);
async function toggleResizeObserver(observes: boolean) {
const container = await element;
if (observes) {
resizeObserver.observe(container);
} else {
resizeObserver.unobserve(container);
}
}
$: toggleResizeObserver(observes);
/* memoized position of image on resize start
* prevents frantic behavior when image shift into the next/previous line */
let getDragWidth: (event: PointerEvent) => number;
@ -144,6 +143,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
target.setPointerCapture(pointerId);
}
let minResizeWidth: number;
let minResizeHeight: number;
$: [minResizeWidth, minResizeHeight] =
aspectRatio > 1 ? [5 * aspectRatio, 5] : [5, 5 / aspectRatio];
@ -198,12 +199,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
activeImage!.removeAttribute("width");
}
onDestroy(() => {
resizeObserver.disconnect();
container.removeEventListener("click", maybeShowHandle);
container.removeEventListener("blur", resetHandle);
container.removeEventListener("key", resetHandle);
container.removeEventListener("paste", resetHandle);
onMount(async () => {
const container = await element;
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
return singleCallback(
on(container, "click", maybeShowHandle),
on(container, "blur", resetHandle),
on(container, "key" as any, resetHandle),
on(container, "paste", resetHandle),
);
});
let shrinkingDisabled: boolean;
@ -234,51 +241,55 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:dropdownObject
>
{#if activeImage}
<HandleSelection
bind:updateSelection
{container}
image={activeImage}
on:mount={(event) => createDropdown(event.detail.selection)}
>
<HandleBackground
on:dblclick={() => {
if (shrinkingDisabled) {
return;
}
toggleActualSize();
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
{#await element then container}
<HandleSelection
bind:updateSelection
{container}
image={activeImage}
on:mount={(event) => createDropdown(event.detail.selection)}
>
<HandleBackground
on:dblclick={() => {
if (shrinkingDisabled) {
return;
}
toggleActualSize();
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
<HandleLabel on:mount={updateDimensions}>
{#if isSizeConstrained}
<span>{tr.editingDoubleClickToExpand()}</span>
{:else}
<span>{actualWidth}&times;{actualHeight}</span>
{#if customDimensions}
<span>(Original: {naturalWidth}&times;{naturalHeight})</span>
<HandleLabel on:mount={updateDimensions}>
{#if isSizeConstrained}
<span>{tr.editingDoubleClickToExpand()}</span>
{:else}
<span>{actualWidth}&times;{actualHeight}</span>
{#if customDimensions}
<span>(Original: {naturalWidth}&times;{naturalHeight})</span
>
{/if}
{/if}
{/if}
</HandleLabel>
</HandleLabel>
<HandleControl
active={!isSizeConstrained}
activeSize={8}
offsetX={5}
offsetY={5}
on:pointerclick={(event) => {
if (!isSizeConstrained) {
setPointerCapture(event);
}
}}
on:pointermove={(event) => {
resize(event);
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
</HandleSelection>
{/await}
<HandleControl
active={!isSizeConstrained}
activeSize={8}
offsetX={5}
offsetY={5}
on:pointerclick={(event) => {
if (!isSizeConstrained) {
setPointerCapture(event);
}
}}
on:pointermove={(event) => {
resize(event);
updateSizesWithDimensions();
dropdownObject.update();
}}
/>
</HandleSelection>
<ButtonDropdown on:click={updateSizesWithDimensions}>
<FloatButtons image={activeImage} on:update={dropdownObject.update} />
<SizeSelect

View file

@ -12,14 +12,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { Mathjax } from "../../editable/mathjax-element";
import { on } from "../../lib/events";
import { noop } from "../../lib/functional";
import { singleCallback } from "../../lib/typing";
import HandleBackground from "../HandleBackground.svelte";
import HandleControl from "../HandleControl.svelte";
import HandleSelection from "../HandleSelection.svelte";
import { context } from "../rich-text-input";
import MathjaxMenu from "./MathjaxMenu.svelte";
const { container, api } = context.get();
const { editable, preventResubscription } = api;
const { editable, element, preventResubscription } = context.get();
let activeImage: HTMLImageElement | null = null;
let mathjaxElement: HTMLElement | null = null;
@ -76,6 +76,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function maybeShowHandle({ target }: Event): Promise<void> {
await resetHandle();
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
showHandle(target);
}
@ -107,20 +108,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
showHandle(detail);
}
onMount(() => {
const removeClick = on(container, "click", maybeShowHandle);
const removeCaretAfter = on(
container,
"movecaretafter" as any,
showAutofocusHandle,
);
const removeSelectAll = on(container, "selectall" as any, showSelectAll);
onMount(async () => {
const container = await element;
return () => {
removeClick();
removeCaretAfter();
removeSelectAll();
};
return singleCallback(
on(container, "click", maybeShowHandle),
on(container, "movecaretafter" as any, showAutofocusHandle),
on(container, "selectall" as any, showSelectAll),
);
});
let updateSelection: () => Promise<void>;
@ -136,7 +131,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const resizeObserver = new ResizeObserver(onImageResize);
let clearResize = noop;
function handleImageResizing(activeImage: HTMLImageElement | null) {
async function handleImageResizing(activeImage: HTMLImageElement | null) {
const container = await element;
if (activeImage) {
resizeObserver.observe(container);
clearResize = on(activeImage, "resize", onImageResize);
@ -172,16 +169,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
resetHandle();
}}
>
<HandleSelection
image={activeImage}
{container}
bind:updateSelection
on:mount={(event) =>
(dropdownApi = createDropdown(event.detail.selection))}
>
<HandleBackground tooltip={errorMessage} />
<HandleControl offsetX={1} offsetY={1} />
</HandleSelection>
{#await element then container}
<HandleSelection
image={activeImage}
{container}
bind:updateSelection
on:mount={(event) =>
(dropdownApi = createDropdown(event.detail.selection))}
>
<HandleBackground tooltip={errorMessage} />
<HandleControl offsetX={1} offsetY={1} />
</HandleSelection>
{/await}
</MathjaxMenu>
{/if}
</WithDropdown>

View file

@ -4,23 +4,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script context="module" lang="ts">
import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
import { singleCallback } from "../../lib/typing";
import useContextProperty from "../../sveltelib/context-property";
import useDOMMirror from "../../sveltelib/dom-mirror";
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
import useInputHandler from "../../sveltelib/input-handler";
import { pageTheme } from "../../sveltelib/theme";
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
import type CustomStyles from "./CustomStyles.svelte";
export interface RichTextInputAPI extends EditingInputAPI {
name: "rich-text";
/** This is the contentEditable anki-editable element */
element: Promise<HTMLElement>;
moveCaretToEnd(): void;
toggle(): boolean;
preventResubscription(): () => void;
inputHandler: InputHandlerAPI;
/** The API exposed by the editable component */
editable: ContentEditableAPI;
customStyles: Promise<CustomStyles>;
}
export function editingInputIsRichText(
@ -29,16 +27,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return editingInput?.name === "rich-text";
}
export interface RichTextInputContextAPI {
styles: CustomStyles;
container: HTMLElement;
api: RichTextInputAPI;
}
import { registerPackage } from "../../lib/runtime-require";
import contextProperty from "../../sveltelib/context-property";
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
const key = Symbol("richText");
const [context, setContextProperty] =
useContextProperty<RichTextInputContextAPI>(key);
const [context, setContextProperty] = contextProperty<RichTextInputAPI>(key);
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
const [lifecycle, instances, setupLifecycleHooks] =
lifecycleHooks<RichTextInputAPI>();
registerPackage("anki/RichTextInput", {
context,
lifecycle,
instances,
});
export { context, globalInputHandler as inputHandler };
</script>
@ -48,12 +51,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { placeCaretAfterContent } from "../../domlib/place-caret";
import ContentEditable from "../../editable/ContentEditable.svelte";
import { promiseWithResolver } from "../../lib/promise";
import { singleCallback } from "../../lib/typing";
import useDOMMirror from "../../sveltelib/dom-mirror";
import useInputHandler from "../../sveltelib/input-handler";
import { pageTheme } from "../../sveltelib/theme";
import { context as editingAreaContext } from "../EditingArea.svelte";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import getNormalizingNodeStore from "./normalizing-node-store";
import useRichTextResolve from "./rich-text-resolve";
import RichTextStyles from "./RichTextStyles.svelte";
import SetContext from "./SetContext.svelte";
import { fragmentToStored, storedToFragment } from "./transform";
export let hidden: boolean;
@ -65,6 +72,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const [richTextPromise, resolve] = useRichTextResolve();
const { mirror, preventResubscription } = useDOMMirror();
const [inputHandler, setupInputHandler] = useInputHandler();
const [customStyles, stylesResolve] = promiseWithResolver<CustomStyles>();
export function attachShadow(element: Element): void {
element.attachShadow({ mode: "open" });
@ -123,6 +131,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
preventResubscription,
inputHandler,
editable: {} as ContentEditableAPI,
customStyles,
};
const allContexts = getAllContexts();
@ -165,13 +174,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
),
);
});
setContextProperty(api);
setupLifecycleHooks(api);
</script>
<div class="rich-text-input" on:focusin={() => ($focusedInput = api)}>
<RichTextStyles
color={$pageTheme.isDark ? "white" : "black"}
callback={stylesResolve}
let:attachToShadow={attachStyles}
let:promise={stylesPromise}
let:stylesDidLoad
>
<div
@ -186,16 +198,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
on:focusout
/>
<div class="rich-text-widgets">
{#await Promise.all( [richTextPromise, stylesPromise], ) then [container, styles]}
<SetContext
setter={setContextProperty}
value={{ container, styles, api }}
>
<slot />
</SetContext>
{/await}
</div>
{#await Promise.all([richTextPromise, stylesDidLoad]) then _}
<div class="rich-text-widgets">
<slot />
</div>
{/await}
</RichTextStyles>
</div>

View file

@ -11,15 +11,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { StyleLinkType, StyleObject } from "./CustomStyles.svelte";
import CustomStyles from "./CustomStyles.svelte";
const [promise, customStylesResolve] = promiseWithResolver<CustomStyles>();
export let callback: (styles: CustomStyles) => void;
const [userBaseStyle, userBaseResolve] = promiseWithResolver<StyleObject>();
const [userBaseRule, userBaseRuleResolve] = promiseWithResolver<CSSStyleRule>();
const stylesDidLoad: Promise<unknown> = Promise.all([
promise,
userBaseStyle,
userBaseRule,
]);
const stylesDidLoad: Promise<unknown> = Promise.all([userBaseStyle, userBaseRule]);
userBaseStyle.then((baseStyle: StyleObject) => {
const sheet = baseStyle.element.sheet as CSSStyleSheet;
@ -56,9 +53,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
props: { styles },
});
customStyles.addStyleTag("userBase").then(userBaseResolve);
customStylesResolve(customStyles);
customStyles.addStyleTag("userBase").then((styleTag) => {
userBaseResolve(styleTag);
callback(customStyles);
});
}
</script>
<slot {attachToShadow} {promise} {stylesDidLoad} />
<slot {attachToShadow} {stylesDidLoad} />

View file

@ -1,12 +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">
export let setter: (value: any) => void;
export let value: any;
setter(value);
</script>
<slot />

View file

@ -18,6 +18,7 @@ type AnkiPackages =
| "anki/NoteEditor"
| "anki/EditorField"
| "anki/PlainTextInput"
| "anki/RichTextInput"
| "anki/TemplateButtons"
| "anki/packages"
| "anki/bridgecommand"