mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12: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
|
licenses.json
|
||||||
vendor
|
vendor
|
||||||
|
node_modules
|
||||||
|
bazel-*
|
||||||
|
ftl/usage
|
||||||
|
.mypy_cache
|
|
@ -2,5 +2,5 @@
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"printWidth": 88,
|
"printWidth": 88,
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"semi": true,
|
"semi": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ editing-latex-math-env = LaTeX math env.
|
||||||
editing-mathjax-block = MathJax block
|
editing-mathjax-block = MathJax block
|
||||||
editing-mathjax-chemistry = MathJax chemistry
|
editing-mathjax-chemistry = MathJax chemistry
|
||||||
editing-mathjax-inline = MathJax inline
|
editing-mathjax-inline = MathJax inline
|
||||||
|
editing-mathjax-placeholder = Press { $accept } to accept, { $newline } for new line.
|
||||||
editing-media = Media
|
editing-media = Media
|
||||||
editing-ordered-list = Ordered list
|
editing-ordered-list = Ordered list
|
||||||
editing-outdent = Decrease indent
|
editing-outdent = Decrease indent
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.card {
|
.card {
|
||||||
font-family: arial;
|
font-family: arial;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,7 @@
|
||||||
@use "sass:list";
|
@use "sass:list";
|
||||||
@use "sass:map";
|
@use "sass:map";
|
||||||
|
|
||||||
$bps: (
|
$bps: ("xs", "sm", "md", "lg", "xl", "xxl");
|
||||||
"xs",
|
|
||||||
"sm",
|
|
||||||
"md",
|
|
||||||
"lg",
|
|
||||||
"xl",
|
|
||||||
"xxl",
|
|
||||||
);
|
|
||||||
|
|
||||||
$breakpoints: (
|
$breakpoints: (
|
||||||
list.nth($bps, 2): 576px,
|
list.nth($bps, 2): 576px,
|
||||||
|
@ -28,10 +21,10 @@ $breakpoints: (
|
||||||
} @else {
|
} @else {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
@mixin with-breakpoints($prefix, $dict) {
|
@mixin with-breakpoints($prefix, $dict) {
|
||||||
@each $property, $values in $dict {
|
@each $property, $values in $dict {
|
||||||
@each $bp, $value in $values {
|
@each $bp, $value in $values {
|
||||||
@if map.get($breakpoints, $bp) {
|
@if map.get($breakpoints, $bp) {
|
||||||
@media (min-width: map.get($breakpoints, $bp)) {
|
@media (min-width: map.get($breakpoints, $bp)) {
|
||||||
|
@ -46,7 +39,7 @@ $breakpoints: (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
@function breakpoints-upto($upto) {
|
@function breakpoints-upto($upto) {
|
||||||
$result: ();
|
$result: ();
|
||||||
|
@ -66,14 +59,14 @@ $breakpoints: (
|
||||||
$result: ();
|
$result: ();
|
||||||
|
|
||||||
@each $bp in breakpoints-upto($upto) {
|
@each $bp in breakpoints-upto($upto) {
|
||||||
$result: list.append($result, ".#{$prefix}-#{$bp}", $separator: comma)
|
$result: list.append($result, ".#{$prefix}-#{$bp}", $separator: comma);
|
||||||
}
|
}
|
||||||
|
|
||||||
@return $result;
|
@return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin with-breakpoints-upto($prefix, $dict) {
|
@mixin with-breakpoints-upto($prefix, $dict) {
|
||||||
@each $property, $values in $dict {
|
@each $property, $values in $dict {
|
||||||
@each $bp, $value in $values {
|
@each $bp, $value in $values {
|
||||||
$selector: breakpoint-selector-upto($prefix, $bp);
|
$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 type { Writable } from "svelte/store";
|
||||||
import { updateAllState } from "../components/WithState.svelte";
|
import { updateAllState } from "../components/WithState.svelte";
|
||||||
import { saveSelection, restoreSelection } from "../domlib/location";
|
import { saveSelection, restoreSelection } from "../domlib/location";
|
||||||
import { on } from "../lib/events";
|
import { on, preventDefault } from "../lib/events";
|
||||||
import { registerShortcut } from "../lib/shortcuts";
|
import { registerShortcut } from "../lib/shortcuts";
|
||||||
|
|
||||||
export let nodes: Writable<DocumentFragment>;
|
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;
|
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 */
|
/* must execute before DOMMirror */
|
||||||
function saveLocation(editable: HTMLElement) {
|
function saveLocation(editable: HTMLElement) {
|
||||||
let removeOnFocus: () => void;
|
const removeOnBlur = on(editable, "blur", onBlur);
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -54,10 +56,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let editable: HTMLElement;
|
let editable: HTMLElement;
|
||||||
|
|
||||||
$: if (editable) {
|
$: if (editable) {
|
||||||
registerShortcut((event) => event.preventDefault(), "Control+B", editable);
|
for (const keyCombination of [
|
||||||
registerShortcut((event) => event.preventDefault(), "Control+U", editable);
|
"Control+B",
|
||||||
registerShortcut((event) => event.preventDefault(), "Control+I", editable);
|
"Control+U",
|
||||||
registerShortcut((event) => event.preventDefault(), "Control+R", editable);
|
"Control+I",
|
||||||
|
"Control+R",
|
||||||
|
]) {
|
||||||
|
registerShortcut(preventDefault, keyCombination, editable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,67 +2,85 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { onMount, onDestroy, getContext, tick } from "svelte";
|
import type { Writable } from "svelte/store";
|
||||||
import { nightModeKey } from "../components/context-keys";
|
|
||||||
import { convertMathjax } from "./mathjax";
|
|
||||||
|
|
||||||
export let mathjax: string;
|
const imageToHeightMap = new Map<string, Writable<number>>();
|
||||||
export let block: boolean;
|
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||||
export let autofocus = false;
|
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 */
|
setTimeout(() => entry.target.dispatchEvent(new Event("resize")));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
</script>
|
||||||
onDestroy(() => {
|
|
||||||
observer.unobserve(image);
|
<script lang="ts">
|
||||||
observer.disconnect();
|
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>
|
</script>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
bind:this={image}
|
|
||||||
src="data:image/svg+xml,{encoded}"
|
src="data:image/svg+xml,{encoded}"
|
||||||
class:block
|
class:block
|
||||||
class:empty
|
class:empty
|
||||||
style="--vertical-center: {-imageHeight / 2 + fontSize / 4}px;"
|
style="--vertical-center: {verticalCenter}px;"
|
||||||
alt="Mathjax"
|
alt="Mathjax"
|
||||||
{title}
|
{title}
|
||||||
data-anki="mathjax"
|
data-anki="mathjax"
|
||||||
|
data-uuid={uuid}
|
||||||
on:dragstart|preventDefault
|
on:dragstart|preventDefault
|
||||||
|
use:maybeAutofocus
|
||||||
|
use:observe
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -76,7 +94,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
margin: auto;
|
margin: 1rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import "mathjax/es5/tex-svg-full";
|
||||||
|
|
||||||
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
|
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
|
||||||
import { nodeIsElement } from "../lib/dom";
|
import { nodeIsElement } from "../lib/dom";
|
||||||
|
import { noop } from "../lib/functional";
|
||||||
|
import { placeCaretAfter } from "../domlib/place-caret";
|
||||||
import { nightModeKey } from "../components/context-keys";
|
import { nightModeKey } from "../components/context-keys";
|
||||||
|
|
||||||
import Mathjax_svelte from "./Mathjax.svelte";
|
import Mathjax_svelte from "./Mathjax.svelte";
|
||||||
|
@ -35,16 +37,6 @@ function moveNodeOutOfElement(
|
||||||
return referenceNode;
|
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 {
|
function moveNodesInsertedOutside(element: Element, allowedChild: Node): () => void {
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
if (element.childNodes.length === 1) {
|
if (element.childNodes.length === 1) {
|
||||||
|
@ -123,9 +115,7 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||||
}
|
}
|
||||||
|
|
||||||
block = false;
|
block = false;
|
||||||
disconnect: () => void = () => {
|
disconnect = noop;
|
||||||
/* noop */
|
|
||||||
};
|
|
||||||
component?: Mathjax_svelte;
|
component?: Mathjax_svelte;
|
||||||
|
|
||||||
static get observedAttributes(): string[] {
|
static get observedAttributes(): string[] {
|
||||||
|
|
|
@ -11,13 +11,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import storeSubscribe from "../sveltelib/store-subscribe";
|
import storeSubscribe from "../sveltelib/store-subscribe";
|
||||||
import { directionKey } from "../lib/context-keys";
|
import { directionKey } from "../lib/context-keys";
|
||||||
|
|
||||||
export let configuration: CodeMirror.EditorConfiguration;
|
export let configuration: CodeMirror.EditorConfiguration;
|
||||||
export let code: Writable<string>;
|
export let code: Writable<string>;
|
||||||
|
export let autofocus = false;
|
||||||
|
|
||||||
const direction = getContext<Writable<"ltr" | "rtl">>(directionKey);
|
const direction = getContext<Writable<"ltr" | "rtl">>(directionKey);
|
||||||
const defaultConfiguration = {
|
const defaultConfiguration = {
|
||||||
|
@ -52,13 +53,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
});
|
});
|
||||||
codeMirror.on("focus", () => {
|
codeMirror.on("focus", () => {
|
||||||
if (ranges) {
|
if (ranges) {
|
||||||
codeMirror.setSelections(ranges);
|
try {
|
||||||
|
codeMirror.setSelections(ranges);
|
||||||
|
} catch {
|
||||||
|
ranges = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
|
dispatch("focus");
|
||||||
});
|
});
|
||||||
codeMirror.on("blur", () => {
|
codeMirror.on("blur", () => {
|
||||||
ranges = codeMirror.listSelections();
|
ranges = codeMirror.listSelections();
|
||||||
subscribe();
|
subscribe();
|
||||||
|
dispatch("blur");
|
||||||
});
|
});
|
||||||
|
|
||||||
subscribe();
|
subscribe();
|
||||||
|
@ -70,6 +77,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
editor: { get: () => codeMirror },
|
editor: { get: () => codeMirror },
|
||||||
},
|
},
|
||||||
) as CodeMirrorAPI;
|
) as CodeMirrorAPI;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (autofocus) {
|
||||||
|
codeMirror.focus();
|
||||||
|
codeMirror.setCursor(codeMirror.lineCount(), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="code-mirror">
|
<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">
|
<style lang="scss">
|
||||||
.code-mirror :global(.CodeMirror) {
|
.code-mirror :global(.CodeMirror) {
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,7 +16,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CodeMirror from "./CodeMirror.svelte";
|
import CodeMirror from "./CodeMirror.svelte";
|
||||||
import type { CodeMirrorAPI } from "./CodeMirror.svelte";
|
import type { CodeMirrorAPI } from "./CodeMirror.svelte";
|
||||||
|
|
||||||
import { tick, onMount } from "svelte";
|
import { tick, onMount } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { getDecoratedElements } from "./DecoratedElements.svelte";
|
import { getDecoratedElements } from "./DecoratedElements.svelte";
|
||||||
|
@ -143,7 +142,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class:hidden on:focusin on:focusout>
|
<div class="plain-text-input" class:hidden on:focusin on:focusout>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
{configuration}
|
{configuration}
|
||||||
{code}
|
{code}
|
||||||
|
@ -153,6 +152,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.plain-text-input :global(.CodeMirror) {
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
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 { promiseWithResolver } from "../lib/promise";
|
||||||
import { bridgeCommand } from "../lib/bridgecommand";
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
import { wrapInternal } from "../lib/wrap";
|
import { wrapInternal } from "../lib/wrap";
|
||||||
|
import { on } from "../lib/events";
|
||||||
import { nodeStore } from "../sveltelib/node-store";
|
import { nodeStore } from "../sveltelib/node-store";
|
||||||
import type { DecoratedElement } from "../editable/decorated";
|
import type { DecoratedElement } from "../editable/decorated";
|
||||||
import { nightModeKey } from "../components/context-keys";
|
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");
|
bridgeCommand("cutOrCopy");
|
||||||
}
|
}
|
||||||
|
|
||||||
richTextInput.addEventListener("paste", onPaste);
|
const removePaste = on(richTextInput, "paste", onPaste);
|
||||||
richTextInput.addEventListener("copy", onCutOrCopy);
|
const removeCopy = on(richTextInput, "copy", onCutOrCopy);
|
||||||
richTextInput.addEventListener("cut", onCutOrCopy);
|
const removeCut = on(richTextInput, "cut", onCutOrCopy);
|
||||||
richTextResolve(richTextInput);
|
richTextResolve(richTextInput);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
richTextInput.removeEventListener("paste", onPaste);
|
removePaste();
|
||||||
richTextInput.removeEventListener("copy", onCutOrCopy);
|
removeCopy();
|
||||||
richTextInput.removeEventListener("cut", onCutOrCopy);
|
removeCut();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,9 @@ import "codemirror/mode/stex/stex";
|
||||||
import "codemirror/addon/fold/foldcode";
|
import "codemirror/addon/fold/foldcode";
|
||||||
import "codemirror/addon/fold/foldgutter";
|
import "codemirror/addon/fold/foldgutter";
|
||||||
import "codemirror/addon/fold/xml-fold";
|
import "codemirror/addon/fold/xml-fold";
|
||||||
import "codemirror/addon/edit/matchtags.js";
|
import "codemirror/addon/edit/matchtags";
|
||||||
import "codemirror/addon/edit/closetag.js";
|
import "codemirror/addon/edit/closetag";
|
||||||
|
import "codemirror/addon/display/placeholder";
|
||||||
|
|
||||||
export { CodeMirror };
|
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">
|
<script lang="ts">
|
||||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
import MathjaxMenu from "./MathjaxMenu.svelte";
|
||||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
import { onMount, onDestroy, tick } from "svelte";
|
||||||
import Item from "../../components/Item.svelte";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
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 { getRichTextInput } from "../RichTextInput.svelte";
|
import { getRichTextInput } from "../RichTextInput.svelte";
|
||||||
|
import { placeCaretAfter } from "../../domlib/place-caret";
|
||||||
import { noop } from "../../lib/functional";
|
import { noop } from "../../lib/functional";
|
||||||
|
import { on } from "../../lib/events";
|
||||||
|
|
||||||
const { container, api } = getRichTextInput();
|
const { container, api } = getRichTextInput();
|
||||||
|
|
||||||
|
const code = writable("");
|
||||||
|
|
||||||
let activeImage: HTMLImageElement | null = null;
|
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 {
|
function showHandle(image: HTMLImageElement): void {
|
||||||
allow = api.preventResubscription();
|
allow = api.preventResubscription();
|
||||||
|
|
||||||
activeImage = image;
|
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> {
|
async function clearImage(): Promise<void> {
|
||||||
await resetHandle();
|
if (activeImage && mathjaxElement) {
|
||||||
const target = event.target;
|
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") {
|
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
||||||
showHandle(target);
|
showHandle(target);
|
||||||
}
|
}
|
||||||
|
@ -45,71 +81,45 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
showHandle(detail);
|
showHandle(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetHandle(): Promise<void> {
|
|
||||||
if (activeImage) {
|
|
||||||
allow();
|
|
||||||
activeImage = null;
|
|
||||||
await tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
container.addEventListener("click", maybeShowHandle);
|
const removeClick = on(container, "click", maybeShowHandle);
|
||||||
container.addEventListener("focusmathjax" as any, showAutofocusHandle);
|
const removeFocus = on(container, "focusmathjax" as any, showAutofocusHandle);
|
||||||
container.addEventListener("key", resetHandle);
|
|
||||||
container.addEventListener("paste", resetHandle);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener("click", maybeShowHandle);
|
removeClick();
|
||||||
container.removeEventListener("focusmathjax" as any, showAutofocusHandle);
|
removeFocus();
|
||||||
container.removeEventListener("key", resetHandle);
|
|
||||||
container.removeEventListener("paste", resetHandle);
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let updateSelection: () => Promise<void>;
|
||||||
|
let errorMessage: string;
|
||||||
let dropdownApi: any;
|
let dropdownApi: any;
|
||||||
|
|
||||||
async function onImageResize(): Promise<void> {
|
async function onImageResize(): Promise<void> {
|
||||||
if (activeImage) {
|
errorMessage = activeImage!.title;
|
||||||
errorMessage = activeImage.title;
|
await updateSelection();
|
||||||
await updateSelection();
|
dropdownApi.update();
|
||||||
dropdownApi.update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(onImageResize);
|
||||||
|
|
||||||
let clearResize = noop;
|
let clearResize = noop;
|
||||||
function handleImageResizing(activeImage: HTMLImageElement | null) {
|
function handleImageResizing(activeImage: HTMLImageElement | null) {
|
||||||
if (activeImage) {
|
if (activeImage) {
|
||||||
activeImage.addEventListener("resize", onImageResize);
|
resizeObserver.observe(container);
|
||||||
|
clearResize = on(activeImage, "resize", onImageResize);
|
||||||
const lastImage = activeImage;
|
|
||||||
clearResize = () => lastImage.removeEventListener("resize", onImageResize);
|
|
||||||
} else {
|
} else {
|
||||||
|
resizeObserver.unobserve(container);
|
||||||
clearResize();
|
clearResize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: handleImageResizing(activeImage);
|
$: handleImageResizing(activeImage);
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(async () => {
|
onDestroy(() => {
|
||||||
if (activeImage) {
|
resizeObserver.disconnect();
|
||||||
await updateSelection();
|
clearResize();
|
||||||
dropdownApi.update();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<WithDropdown
|
<WithDropdown
|
||||||
|
@ -119,37 +129,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
distance={4}
|
distance={4}
|
||||||
let:createDropdown
|
let:createDropdown
|
||||||
>
|
>
|
||||||
{#if activeImage}
|
{#if activeImage && mathjaxElement}
|
||||||
<HandleSelection
|
<MathjaxMenu
|
||||||
image={activeImage}
|
{activeImage}
|
||||||
|
{mathjaxElement}
|
||||||
{container}
|
{container}
|
||||||
|
{errorMessage}
|
||||||
|
{code}
|
||||||
bind:updateSelection
|
bind:updateSelection
|
||||||
on:mount={(event) => (dropdownApi = createDropdown(event.detail.selection))}
|
on:mount={(event) => (dropdownApi = createDropdown(event.detail.selection))}
|
||||||
>
|
on:reset={() => resetHandle(false)}
|
||||||
<HandleBackground tooltip={errorMessage} />
|
on:delete={() => resetHandle(true)}
|
||||||
|
/>
|
||||||
<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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</WithDropdown>
|
</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 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 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 () =>
|
return () =>
|
||||||
target.removeEventListener(eventType, handler as EventListener, options);
|
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