mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
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:
parent
336ad05693
commit
3969487e77
7 changed files with 142 additions and 136 deletions
|
@ -23,7 +23,8 @@ html {
|
|||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}×{actualHeight}</span>
|
||||
{#if customDimensions}
|
||||
<span>(Original: {naturalWidth}×{naturalHeight})</span>
|
||||
<HandleLabel on:mount={updateDimensions}>
|
||||
{#if isSizeConstrained}
|
||||
<span>{tr.editingDoubleClickToExpand()}</span>
|
||||
{:else}
|
||||
<span>{actualWidth}×{actualHeight}</span>
|
||||
{#if customDimensions}
|
||||
<span>(Original: {naturalWidth}×{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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 />
|
|
@ -18,6 +18,7 @@ type AnkiPackages =
|
|||
| "anki/NoteEditor"
|
||||
| "anki/EditorField"
|
||||
| "anki/PlainTextInput"
|
||||
| "anki/RichTextInput"
|
||||
| "anki/TemplateButtons"
|
||||
| "anki/packages"
|
||||
| "anki/bridgecommand"
|
||||
|
|
Loading…
Reference in a new issue