Mathjax editor improvements (#1502)

* Remove unnecessary stopPropagation of mathjax-overlay events

* Use CodeMirror component for MathjaxHandle

* Refactor ResizeObserver code in MathjaxHandle

* Wrap setRange in CodeMirror in try/catch

* Add Mathjax Editor bottom margin

* Add custom Enter and Shift+Enter shortcuts for the MathjaxHandle

* Format

* Move placeCaretAfter to domlib

* Move focus back to field after editing Mathjax

* Put Cursor after Mathjax after accepting

* Add delete button for Mathjax

* Change border color of mathjax menu

* Refactor into MathjaxMenu

* Put caretKeyword in variable

* Use one ResizeObserver for all Mathjax images

* Add minmimum width for Mathjax editor

* is still smaller than minimal window width

* Add bazel directories to .prettierignore and format from root

* exclude ftl/usage (dae)

the json files that live there are output from our tooling, and
formatting them means an extra step each time we want to update them

also exclude .mypy_cache, which is output by scripts/mypy*

* minor ftl tweak: newline -> new line  (dae)
This commit is contained in:
Henrik Giesel 2021-11-23 01:27:32 +01:00 committed by GitHub
parent 3db3ae28af
commit 2778b9220c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 447 additions and 326 deletions

View file

@ -1,2 +1,6 @@
licenses.json
vendor
node_modules
bazel-*
ftl/usage
.mypy_cache

View file

@ -2,5 +2,5 @@
"trailingComma": "all",
"printWidth": 88,
"tabWidth": 4,
"semi": true,
"semi": true
}

View file

@ -30,6 +30,7 @@ editing-latex-math-env = LaTeX math env.
editing-mathjax-block = MathJax block
editing-mathjax-chemistry = MathJax chemistry
editing-mathjax-inline = MathJax inline
editing-mathjax-placeholder = Press { $accept } to accept, { $newline } for new line.
editing-media = Media
editing-ordered-list = Ordered list
editing-outdent = Decrease indent

View file

@ -1,7 +1,7 @@
.card {
font-family: arial;
font-size: 20px;
text-align: center;
color: black;
background-color: white;
font-family: arial;
font-size: 20px;
text-align: center;
color: black;
background-color: white;
}

View file

@ -3,14 +3,7 @@
@use "sass:list";
@use "sass:map";
$bps: (
"xs",
"sm",
"md",
"lg",
"xl",
"xxl",
);
$bps: ("xs", "sm", "md", "lg", "xl", "xxl");
$breakpoints: (
list.nth($bps, 2): 576px,
@ -28,10 +21,10 @@ $breakpoints: (
} @else {
@content;
}
};
}
@mixin with-breakpoints($prefix, $dict) {
@each $property, $values in $dict {
@each $property, $values in $dict {
@each $bp, $value in $values {
@if map.get($breakpoints, $bp) {
@media (min-width: map.get($breakpoints, $bp)) {
@ -46,7 +39,7 @@ $breakpoints: (
}
}
}
};
}
@function breakpoints-upto($upto) {
$result: ();
@ -66,14 +59,14 @@ $breakpoints: (
$result: ();
@each $bp in breakpoints-upto($upto) {
$result: list.append($result, ".#{$prefix}-#{$bp}", $separator: comma)
$result: list.append($result, ".#{$prefix}-#{$bp}", $separator: comma);
}
@return $result;
}
@mixin with-breakpoints-upto($prefix, $dict) {
@each $property, $values in $dict {
@each $property, $values in $dict {
@each $bp, $value in $values {
$selector: breakpoint-selector-upto($prefix, $bp);
@ -90,4 +83,4 @@ $breakpoints: (
}
}
}
};
} ;

14
ts/domlib/place-caret.ts Normal file
View file

@ -0,0 +1,14 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { getSelection } from "../lib/cross-browser";
export function placeCaretAfter(node: Node): void {
const range = new Range();
range.setStartAfter(node);
range.collapse(false);
const selection = getSelection(node)!;
selection.removeAllRanges();
selection.addRange(range);
}

View file

@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { Writable } from "svelte/store";
import { updateAllState } from "../components/WithState.svelte";
import { saveSelection, restoreSelection } from "../domlib/location";
import { on } from "../lib/events";
import { on, preventDefault } from "../lib/events";
import { registerShortcut } from "../lib/shortcuts";
export let nodes: Writable<DocumentFragment>;
@ -18,29 +18,31 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let inputManager: (editable: HTMLElement) => void;
let removeOnFocus: () => void;
let removeOnPointerdown: () => void;
function onBlur(): void {
const location = saveSelection(editable);
removeOnFocus = on(
editable,
"focus",
() => {
if (location) {
restoreSelection(editable, location);
}
},
{ once: true },
);
removeOnPointerdown = on(editable, "pointerdown", () => removeOnFocus?.(), {
once: true,
});
}
/* must execute before DOMMirror */
function saveLocation(editable: HTMLElement) {
let removeOnFocus: () => void;
let removeOnPointerdown: () => void;
const removeOnBlur = on(editable, "blur", () => {
const location = saveSelection(editable);
removeOnFocus = on(
editable,
"focus",
() => {
if (location) {
restoreSelection(editable, location);
}
},
{ once: true },
);
removeOnPointerdown = on(editable, "pointerdown", () => removeOnFocus?.(), {
once: true,
});
});
const removeOnBlur = on(editable, "blur", onBlur);
return {
destroy() {
@ -54,10 +56,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let editable: HTMLElement;
$: if (editable) {
registerShortcut((event) => event.preventDefault(), "Control+B", editable);
registerShortcut((event) => event.preventDefault(), "Control+U", editable);
registerShortcut((event) => event.preventDefault(), "Control+I", editable);
registerShortcut((event) => event.preventDefault(), "Control+R", editable);
for (const keyCombination of [
"Control+B",
"Control+U",
"Control+I",
"Control+R",
]) {
registerShortcut(preventDefault, keyCombination, editable);
}
}
</script>

View file

@ -2,67 +2,85 @@
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { onMount, onDestroy, getContext, tick } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { convertMathjax } from "./mathjax";
<script context="module" lang="ts">
import type { Writable } from "svelte/store";
export let mathjax: string;
export let block: boolean;
export let autofocus = false;
const imageToHeightMap = new Map<string, Writable<number>>();
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
for (const entry of entries) {
const image = entry.target as HTMLImageElement;
const store = imageToHeightMap.get(image.dataset.uuid!)!;
store.set(entry.contentRect.height);
/* have fixed fontSize for normal */
export const fontSize: number = 20;
const nightMode = getContext<boolean>(nightModeKey);
$: [converted, title] = convertMathjax(mathjax, nightMode, fontSize);
$: empty = title === "MathJax";
let encoded: string;
let imageHeight: number;
$: encoded = encodeURIComponent(converted);
let image: HTMLImageElement;
const observer = new ResizeObserver(async () => {
imageHeight = image.getBoundingClientRect().height;
await tick();
setTimeout(() => image.dispatchEvent(new Event("resize")));
});
onMount(() => {
observer.observe(image);
if (autofocus) {
// This should trigger a focusing of the Mathjax Handle
const focusEvent = new CustomEvent("focusmathjax", {
detail: image,
bubbles: true,
composed: true,
});
image.dispatchEvent(focusEvent);
setTimeout(() => entry.target.dispatchEvent(new Event("resize")));
}
});
onDestroy(() => {
observer.unobserve(image);
observer.disconnect();
});
</script>
<script lang="ts">
import { onDestroy, getContext } from "svelte";
import { nightModeKey } from "../components/context-keys";
import { convertMathjax } from "./mathjax";
import { randomUUID } from "../lib/uuid";
import { writable } from "svelte/store";
export let mathjax: string;
export let block: boolean;
export let autofocus = false;
export let fontSize = 20;
const nightMode = getContext<boolean>(nightModeKey);
$: [converted, title] = convertMathjax(mathjax, nightMode, fontSize);
$: empty = title === "MathJax";
$: encoded = encodeURIComponent(converted);
const uuid = randomUUID();
const imageHeight = writable(0);
imageToHeightMap.set(uuid, imageHeight);
$: verticalCenter = -$imageHeight / 2 + fontSize / 4;
function maybeAutofocus(image: Element): void {
if (!autofocus) {
return;
}
// This should trigger a focusing of the Mathjax Handle
const focusEvent = new CustomEvent("focusmathjax", {
detail: image,
bubbles: true,
composed: true,
});
image.dispatchEvent(focusEvent);
}
function observe(image: Element) {
observer.observe(image);
return {
destroy() {
observer.unobserve(image);
},
};
}
onDestroy(() => imageToHeightMap.delete(uuid));
</script>
<img
bind:this={image}
src="data:image/svg+xml,{encoded}"
class:block
class:empty
style="--vertical-center: {-imageHeight / 2 + fontSize / 4}px;"
style="--vertical-center: {verticalCenter}px;"
alt="Mathjax"
{title}
data-anki="mathjax"
data-uuid={uuid}
on:dragstart|preventDefault
use:maybeAutofocus
use:observe
/>
<style lang="scss">
@ -76,7 +94,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
.block {
display: block;
margin: auto;
margin: 1rem auto;
}
.empty {

View file

@ -9,6 +9,8 @@ import "mathjax/es5/tex-svg-full";
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
import { nodeIsElement } from "../lib/dom";
import { noop } from "../lib/functional";
import { placeCaretAfter } from "../domlib/place-caret";
import { nightModeKey } from "../components/context-keys";
import Mathjax_svelte from "./Mathjax.svelte";
@ -35,16 +37,6 @@ function moveNodeOutOfElement(
return referenceNode;
}
function placeCaretAfter(node: Node): void {
const range = new Range();
range.setStartAfter(node);
range.collapse(false);
const selection = document.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
}
function moveNodesInsertedOutside(element: Element, allowedChild: Node): () => void {
const observer = new MutationObserver(() => {
if (element.childNodes.length === 1) {
@ -123,9 +115,7 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
}
block = false;
disconnect: () => void = () => {
/* noop */
};
disconnect = noop;
component?: Mathjax_svelte;
static get observedAttributes(): string[] {

View file

@ -11,13 +11,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script>
<script lang="ts">
import { createEventDispatcher, getContext } from "svelte";
import { createEventDispatcher, getContext, onMount } from "svelte";
import type { Writable } from "svelte/store";
import storeSubscribe from "../sveltelib/store-subscribe";
import { directionKey } from "../lib/context-keys";
export let configuration: CodeMirror.EditorConfiguration;
export let code: Writable<string>;
export let autofocus = false;
const direction = getContext<Writable<"ltr" | "rtl">>(directionKey);
const defaultConfiguration = {
@ -52,13 +53,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
});
codeMirror.on("focus", () => {
if (ranges) {
codeMirror.setSelections(ranges);
try {
codeMirror.setSelections(ranges);
} catch {
ranges = null;
}
}
unsubscribe();
dispatch("focus");
});
codeMirror.on("blur", () => {
ranges = codeMirror.listSelections();
subscribe();
dispatch("blur");
});
subscribe();
@ -70,6 +77,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
editor: { get: () => codeMirror },
},
) as CodeMirrorAPI;
onMount(() => {
if (autofocus) {
codeMirror.focus();
codeMirror.setCursor(codeMirror.lineCount(), 0);
}
});
</script>
<div class="code-mirror">
@ -79,7 +93,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<style lang="scss">
.code-mirror :global(.CodeMirror) {
height: auto;
border-radius: 0 0 5px 5px;
padding: 6px 0;
}
</style>

View file

@ -16,7 +16,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="ts">
import CodeMirror from "./CodeMirror.svelte";
import type { CodeMirrorAPI } from "./CodeMirror.svelte";
import { tick, onMount } from "svelte";
import { writable } from "svelte/store";
import { getDecoratedElements } from "./DecoratedElements.svelte";
@ -143,7 +142,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
});
</script>
<div class:hidden on:focusin on:focusout>
<div class="plain-text-input" class:hidden on:focusin on:focusout>
<CodeMirror
{configuration}
{code}
@ -153,6 +152,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div>
<style lang="scss">
.plain-text-input :global(.CodeMirror) {
border-radius: 0 0 5px 5px;
}
.hidden {
display: none;
}

View file

@ -50,6 +50,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { promiseWithResolver } from "../lib/promise";
import { bridgeCommand } from "../lib/bridgecommand";
import { wrapInternal } from "../lib/wrap";
import { on } from "../lib/events";
import { nodeStore } from "../sveltelib/node-store";
import type { DecoratedElement } from "../editable/decorated";
import { nightModeKey } from "../components/context-keys";
@ -142,16 +143,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bridgeCommand("cutOrCopy");
}
richTextInput.addEventListener("paste", onPaste);
richTextInput.addEventListener("copy", onCutOrCopy);
richTextInput.addEventListener("cut", onCutOrCopy);
const removePaste = on(richTextInput, "paste", onPaste);
const removeCopy = on(richTextInput, "copy", onCutOrCopy);
const removeCut = on(richTextInput, "cut", onCutOrCopy);
richTextResolve(richTextInput);
return {
destroy() {
richTextInput.removeEventListener("paste", onPaste);
richTextInput.removeEventListener("copy", onCutOrCopy);
richTextInput.removeEventListener("cut", onCutOrCopy);
removePaste();
removeCopy();
removeCut();
},
};
}

View file

@ -11,8 +11,9 @@ import "codemirror/mode/stex/stex";
import "codemirror/addon/fold/foldcode";
import "codemirror/addon/fold/foldgutter";
import "codemirror/addon/fold/xml-fold";
import "codemirror/addon/edit/matchtags.js";
import "codemirror/addon/edit/closetag.js";
import "codemirror/addon/edit/matchtags";
import "codemirror/addon/edit/closetag";
import "codemirror/addon/display/placeholder";
export { CodeMirror };

View file

@ -1,81 +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 { onMount, createEventDispatcher } from "svelte";
import { ChangeTimer } from "../change-timer";
import { CodeMirror, latex, baseOptions } from "../code-mirror";
export let initialValue: string;
const codeMirrorOptions = {
mode: latex,
...baseOptions,
};
let codeMirror: CodeMirror.EditorFromTextArea;
const changeTimer = new ChangeTimer();
const dispatch = createEventDispatcher();
function onInput() {
dispatch("update", { mathjax: codeMirror.getValue() });
/* changeTimer.schedule( */
/* () => dispatch("update", { mathjax: codeMirror.getValue() }), */
/* 400 */
/* ); */
}
function onBlur() {
changeTimer.fireImmediately();
dispatch("codemirrorblur");
}
function openCodemirror(textarea: HTMLTextAreaElement): void {
codeMirror = CodeMirror.fromTextArea(textarea, codeMirrorOptions);
codeMirror.on("change", onInput);
codeMirror.on("blur", onBlur);
}
let textarea: HTMLTextAreaElement;
onMount(() => {
codeMirror.focus();
codeMirror.setCursor(codeMirror.lineCount(), 0);
const codeMirrorElement = textarea.nextElementSibling!;
codeMirrorElement.classList.add("mathjax-editor");
});
</script>
<div
on:click|stopPropagation
on:focus|stopPropagation
on:focusin|stopPropagation
on:keydown|stopPropagation
on:keyup|stopPropagation
on:mousedown|preventDefault|stopPropagation
on:mouseup|stopPropagation
on:paste|stopPropagation
>
<!-- TODO no focusin for now, as EditingArea will defer to Editable/Codable -->
<textarea
bind:this={textarea}
value={initialValue}
on:input={onInput}
use:openCodemirror
/>
</div>
<style lang="scss">
div :global(.mathjax-editor) {
border-radius: 0;
border-width: 0 1px;
border-color: var(--medium-border);
height: auto;
border-radius: 0 0 5px 5px;
padding: 6px 0;
}
</style>

View file

@ -1,36 +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 ButtonGroup from "../../components/ButtonGroup.svelte";
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
import IconButton from "../../components/IconButton.svelte";
import * as tr from "../../lib/ftl";
import { inlineIcon, blockIcon } from "./icons";
export let activeImage: HTMLImageElement;
$: mathjaxElement = activeImage.parentElement!;
</script>
<ButtonGroup size={1.6} wrap={false}>
<ButtonGroupItem>
<IconButton
tooltip={tr.editingMathjaxInline()}
active={activeImage.getAttribute("block") === "true"}
on:click={() => mathjaxElement.setAttribute("block", "false")}
on:click>{@html inlineIcon}</IconButton
>
</ButtonGroupItem>
<ButtonGroupItem>
<IconButton
tooltip={tr.editingMathjaxBlock()}
active={activeImage.getAttribute("block") === "false"}
on:click={() => mathjaxElement.setAttribute("block", "true")}
on:click>{@html blockIcon}</IconButton
>
</ButtonGroupItem>
</ButtonGroup>

View file

@ -0,0 +1,54 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
import Item from "../../components/Item.svelte";
import ButtonGroup from "../../components/ButtonGroup.svelte";
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
import IconButton from "../../components/IconButton.svelte";
import * as tr from "../../lib/ftl";
import { inlineIcon, blockIcon, deleteIcon } from "./icons";
import { createEventDispatcher } from "svelte";
export let activeImage: HTMLImageElement;
export let mathjaxElement: HTMLElement;
const dispatch = createEventDispatcher();
</script>
<ButtonToolbar size={1.6} wrap={false}>
<Item>
<ButtonGroup>
<ButtonGroupItem>
<IconButton
tooltip={tr.editingMathjaxInline()}
active={activeImage.getAttribute("block") === "true"}
on:click={() => mathjaxElement.setAttribute("block", "false")}
on:click>{@html inlineIcon}</IconButton
>
</ButtonGroupItem>
<ButtonGroupItem>
<IconButton
tooltip={tr.editingMathjaxBlock()}
active={activeImage.getAttribute("block") === "false"}
on:click={() => mathjaxElement.setAttribute("block", "true")}
on:click>{@html blockIcon}</IconButton
>
</ButtonGroupItem>
</ButtonGroup>
</Item>
<Item>
<ButtonGroup>
<ButtonGroupItem>
<IconButton
tooltip={tr.actionsDelete()}
on:click={() => dispatch("delete")}>{@html deleteIcon}</IconButton
>
</ButtonGroupItem>
</ButtonGroup>
</Item>
</ButtonToolbar>

View file

@ -0,0 +1,59 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import CodeMirror from "../CodeMirror.svelte";
import type { Writable } from "svelte/store";
import { baseOptions, latex } from "../code-mirror";
import { getPlatformString } from "../../lib/shortcuts";
import { noop } from "../../lib/functional";
import * as tr from "../../lib/ftl";
export let acceptShortcut: string;
export let newlineShortcut: string;
export let code: Writable<string>;
const configuration = {
...Object.assign({}, baseOptions, {
extraKeys: {
...(baseOptions.extraKeys as CodeMirror.KeyMap),
[acceptShortcut]: noop,
[newlineShortcut]: noop,
},
}),
placeholder: tr.editingMathjaxPlaceholder({
accept: getPlatformString(acceptShortcut),
newline: getPlatformString(newlineShortcut),
}),
mode: latex,
};
</script>
<div class="mathjax-editor">
<CodeMirror
{code}
{configuration}
on:change={({ detail }) => code.set(detail)}
on:blur
autofocus
/>
</div>
<style lang="scss">
.mathjax-editor {
:global(.CodeMirror) {
max-width: 28rem;
min-width: 14rem;
margin-bottom: 0.25rem;
}
:global(.CodeMirror-placeholder) {
font-family: sans-serif;
font-size: 55%;
text-align: center;
color: var(--slightly-grey-text);
}
}
</style>

View file

@ -4,35 +4,71 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import WithDropdown from "../../components/WithDropdown.svelte";
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
import DropdownMenu from "../../components/DropdownMenu.svelte";
import Item from "../../components/Item.svelte";
import HandleSelection from "../HandleSelection.svelte";
import HandleBackground from "../HandleBackground.svelte";
import HandleControl from "../HandleControl.svelte";
import InlineBlock from "./InlineBlock.svelte";
import Editor from "./Editor.svelte";
import { onMount, tick } from "svelte";
import MathjaxMenu from "./MathjaxMenu.svelte";
import { onMount, onDestroy, tick } from "svelte";
import { writable } from "svelte/store";
import { getRichTextInput } from "../RichTextInput.svelte";
import { placeCaretAfter } from "../../domlib/place-caret";
import { noop } from "../../lib/functional";
import { on } from "../../lib/events";
const { container, api } = getRichTextInput();
const code = writable("");
let activeImage: HTMLImageElement | null = null;
let allow: () => void;
let mathjaxElement: HTMLElement | null = null;
let allow = noop;
let unsubscribe = noop;
const caretKeyword = "caretAfter";
function showHandle(image: HTMLImageElement): void {
allow = api.preventResubscription();
activeImage = image;
image.setAttribute(caretKeyword, "true");
mathjaxElement = activeImage.closest("anki-mathjax")!;
code.set(mathjaxElement.dataset.mathjax ?? "");
unsubscribe = code.subscribe((value: string) => {
mathjaxElement!.dataset.mathjax = value;
});
}
async function maybeShowHandle(event: Event): Promise<void> {
await resetHandle();
const target = event.target;
async function clearImage(): Promise<void> {
if (activeImage && mathjaxElement) {
unsubscribe();
activeImage = null;
mathjaxElement = null;
}
await tick();
container.focus();
}
function placeCaret(image: HTMLImageElement): void {
placeCaretAfter(image);
image.removeAttribute(caretKeyword);
}
async function resetHandle(deletes: boolean = false): Promise<void> {
await clearImage();
const image = container.querySelector(`[${caretKeyword}]`);
if (image) {
placeCaret(image as HTMLImageElement);
if (deletes) {
image.remove();
}
}
allow();
}
async function maybeShowHandle({ target }: Event): Promise<void> {
await resetHandle();
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
showHandle(target);
}
@ -45,71 +81,45 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
showHandle(detail);
}
async function resetHandle(): Promise<void> {
if (activeImage) {
allow();
activeImage = null;
await tick();
}
}
onMount(() => {
container.addEventListener("click", maybeShowHandle);
container.addEventListener("focusmathjax" as any, showAutofocusHandle);
container.addEventListener("key", resetHandle);
container.addEventListener("paste", resetHandle);
const removeClick = on(container, "click", maybeShowHandle);
const removeFocus = on(container, "focusmathjax" as any, showAutofocusHandle);
return () => {
container.removeEventListener("click", maybeShowHandle);
container.removeEventListener("focusmathjax" as any, showAutofocusHandle);
container.removeEventListener("key", resetHandle);
container.removeEventListener("paste", resetHandle);
removeClick();
removeFocus();
};
});
let updateSelection: () => Promise<void>;
let errorMessage: string;
let dropdownApi: any;
async function onImageResize(): Promise<void> {
if (activeImage) {
errorMessage = activeImage.title;
await updateSelection();
dropdownApi.update();
}
errorMessage = activeImage!.title;
await updateSelection();
dropdownApi.update();
}
const resizeObserver = new ResizeObserver(onImageResize);
let clearResize = noop;
function handleImageResizing(activeImage: HTMLImageElement | null) {
if (activeImage) {
activeImage.addEventListener("resize", onImageResize);
const lastImage = activeImage;
clearResize = () => lastImage.removeEventListener("resize", onImageResize);
resizeObserver.observe(container);
clearResize = on(activeImage, "resize", onImageResize);
} else {
resizeObserver.unobserve(container);
clearResize();
}
}
$: handleImageResizing(activeImage);
const resizeObserver = new ResizeObserver(async () => {
if (activeImage) {
await updateSelection();
dropdownApi.update();
}
onDestroy(() => {
resizeObserver.disconnect();
clearResize();
});
resizeObserver.observe(container);
let updateSelection: () => Promise<void>;
let errorMessage: string;
function getComponent(image: HTMLImageElement): HTMLElement {
return image.closest("anki-mathjax")! as HTMLElement;
}
function onEditorUpdate(event: CustomEvent): void {
/* this updates the image in Mathjax.svelte */
getComponent(activeImage!).dataset.mathjax = event.detail.mathjax;
}
</script>
<WithDropdown
@ -119,37 +129,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
distance={4}
let:createDropdown
>
{#if activeImage}
<HandleSelection
image={activeImage}
{#if activeImage && mathjaxElement}
<MathjaxMenu
{activeImage}
{mathjaxElement}
{container}
{errorMessage}
{code}
bind:updateSelection
on:mount={(event) => (dropdownApi = createDropdown(event.detail.selection))}
>
<HandleBackground tooltip={errorMessage} />
<HandleControl offsetX={1} offsetY={1} />
</HandleSelection>
<DropdownMenu>
<Editor
initialValue={getComponent(activeImage).dataset.mathjax ?? ""}
on:update={onEditorUpdate}
on:codemirrorblur={resetHandle}
/>
<div class="margin-x">
<ButtonToolbar>
<Item>
<InlineBlock {activeImage} on:click={updateSelection} />
</Item>
</ButtonToolbar>
</div>
</DropdownMenu>
on:reset={() => resetHandle(false)}
on:delete={() => resetHandle(true)}
/>
{/if}
</WithDropdown>
<style lang="scss">
.margin-x {
margin: 0 0.125rem;
}
</style>

View file

@ -0,0 +1,69 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Shortcut from "../../components/Shortcut.svelte";
import DropdownMenu from "../../components/DropdownMenu.svelte";
import HandleSelection from "../HandleSelection.svelte";
import HandleBackground from "../HandleBackground.svelte";
import HandleControl from "../HandleControl.svelte";
import MathjaxEditor from "./MathjaxEditor.svelte";
import MathjaxButtons from "./MathjaxButtons.svelte";
import type { Writable } from "svelte/store";
import { createEventDispatcher } from "svelte";
export let activeImage: HTMLImageElement;
export let mathjaxElement: HTMLElement;
export let container: HTMLElement;
export let errorMessage: string;
export let code: Writable<string>;
const acceptShortcut = "Enter";
const newlineShortcut = "Shift+Enter";
function appendNewline(): void {
code.update((value) => `${value}\n`);
}
export let updateSelection: () => Promise<void>;
let dropdownApi: any;
export async function update() {
await updateSelection?.();
dropdownApi.update();
}
const dispatch = createEventDispatcher();
</script>
<div class="mathjax-menu">
<HandleSelection image={activeImage} {container} bind:updateSelection on:mount>
<HandleBackground tooltip={errorMessage} />
<HandleControl offsetX={1} offsetY={1} />
</HandleSelection>
<DropdownMenu>
<MathjaxEditor
{acceptShortcut}
{newlineShortcut}
{code}
on:blur={() => dispatch("reset")}
/>
<Shortcut keyCombination={acceptShortcut} on:action={() => dispatch("reset")} />
<Shortcut keyCombination={newlineShortcut} on:action={appendNewline} />
<MathjaxButtons
{activeImage}
{mathjaxElement}
on:delete={() => dispatch("delete")}
/>
</DropdownMenu>
</div>
<style lang="scss">
.mathjax-menu :global(.dropdown-menu) {
border-color: var(--border);
}
</style>

View file

@ -3,3 +3,4 @@
export { default as inlineIcon } from "@mdi/svg/svg/format-wrap-square.svg";
export { default as blockIcon } from "@mdi/svg/svg/format-wrap-top-bottom.svg";
export { default as deleteIcon } from "@mdi/svg/svg/delete.svg";

View file

@ -31,3 +31,7 @@ export function on<T extends EventTarget, K extends keyof EventTargetToMap<T>>(
return () =>
target.removeEventListener(eventType, handler as EventListener, options);
}
export function preventDefault(event: Event): void {
event.preventDefault();
}

17
ts/lib/uuid.ts Normal file
View file

@ -0,0 +1,17 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/**
* TODO replace with crypto.randomUUID
*/
export function randomUUID(): string {
const value = `${1e7}-${1e3}-${4e3}-${8e3}-${1e11}`;
return value.replace(/[018]/g, (character: string): string =>
(
Number(character) ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (Number(character) / 4)))
).toString(16),
);
}