mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
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:
parent
3db3ae28af
commit
2778b9220c
22 changed files with 447 additions and 326 deletions
|
@ -1,2 +1,6 @@
|
|||
licenses.json
|
||||
vendor
|
||||
node_modules
|
||||
bazel-*
|
||||
ftl/usage
|
||||
.mypy_cache
|
|
@ -2,5 +2,5 @@
|
|||
"trailingComma": "all",
|
||||
"printWidth": 88,
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"semi": true
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
14
ts/domlib/place-caret.ts
Normal 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);
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
54
ts/editor/mathjax-overlay/MathjaxButtons.svelte
Normal file
54
ts/editor/mathjax-overlay/MathjaxButtons.svelte
Normal 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>
|
59
ts/editor/mathjax-overlay/MathjaxEditor.svelte
Normal file
59
ts/editor/mathjax-overlay/MathjaxEditor.svelte
Normal 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>
|
|
@ -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>
|
||||
|
|
69
ts/editor/mathjax-overlay/MathjaxMenu.svelte
Normal file
69
ts/editor/mathjax-overlay/MathjaxMenu.svelte
Normal 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>
|
|
@ -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";
|
||||
|
|
|
@ -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
17
ts/lib/uuid.ts
Normal 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),
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue