Merge pull request #1324 from hgiesel/mathjaxboxv4
MathJax Preview + In-line editor
This commit is contained in:
commit
22e8ce3eb3
@ -21,11 +21,19 @@ copy_files_into_group(
|
|||||||
name = "editor",
|
name = "editor",
|
||||||
srcs = [
|
srcs = [
|
||||||
"editor.css",
|
"editor.css",
|
||||||
"editable.css",
|
|
||||||
],
|
],
|
||||||
package = "//ts/editor",
|
package = "//ts/editor",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
copy_files_into_group(
|
||||||
|
name = "editable",
|
||||||
|
srcs = [
|
||||||
|
"editable-build.css",
|
||||||
|
],
|
||||||
|
package = "//ts/editable",
|
||||||
|
)
|
||||||
|
|
||||||
copy_files_into_group(
|
copy_files_into_group(
|
||||||
name = "reviewer",
|
name = "reviewer",
|
||||||
srcs = [
|
srcs = [
|
||||||
@ -39,6 +47,7 @@ filegroup(
|
|||||||
srcs = [
|
srcs = [
|
||||||
"css_local",
|
"css_local",
|
||||||
"editor",
|
"editor",
|
||||||
|
"editable",
|
||||||
"reviewer",
|
"reviewer",
|
||||||
],
|
],
|
||||||
visibility = ["//qt:__subpackages__"],
|
visibility = ["//qt:__subpackages__"],
|
||||||
|
@ -80,10 +80,10 @@ audio = (
|
|||||||
|
|
||||||
_html = """
|
_html = """
|
||||||
<div id="fields"></div>
|
<div id="fields"></div>
|
||||||
<div id="dupes" class="is-inactive">
|
<div id="dupes" class="d-none">
|
||||||
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="cloze-hint"></div>
|
<div id="cloze-hint" class="d-none"></div>
|
||||||
<div id="tag-editor-anchor" class="d-none"></div>
|
<div id="tag-editor-anchor" class="d-none"></div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ChangeNotetypeState } from "./lib";
|
import type { ChangeNotetypeState } from "./lib";
|
||||||
|
|
||||||
import StickyBar from "components/StickyBar.svelte";
|
import StickyHeader from "components/StickyHeader.svelte";
|
||||||
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
||||||
import Item from "components/Item.svelte";
|
import Item from "components/Item.svelte";
|
||||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||||
@ -25,7 +25,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<StickyBar>
|
<StickyHeader>
|
||||||
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
||||||
<Item>
|
<Item>
|
||||||
<ButtonGroup class="flex-grow-1">
|
<ButtonGroup class="flex-grow-1">
|
||||||
@ -48,4 +48,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<SaveButton {state} />
|
<SaveButton {state} />
|
||||||
</Item>
|
</Item>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyBar>
|
</StickyHeader>
|
||||||
|
@ -19,14 +19,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
<ButtonToolbar
|
<ButtonToolbar
|
||||||
{id}
|
{id}
|
||||||
class={`dropdown-menu btn-dropdown-menu ${className}`}
|
class="dropdown-menu btn-dropdown-menu {className}"
|
||||||
wrap={false}
|
wrap={false}
|
||||||
{api}
|
{api}
|
||||||
>
|
>
|
||||||
<slot />
|
<div on:mousedown|preventDefault|stopPropagation>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.dropdown-menu.btn-dropdown-menu) {
|
:global(.dropdown-menu.btn-dropdown-menu) {
|
||||||
display: none;
|
display: none;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -16,7 +16,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
setContext(dropdownKey, null);
|
setContext(dropdownKey, null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {id} class="dropdown-menu" class:show aria-labelledby={labelledby}>
|
<div
|
||||||
|
{id}
|
||||||
|
class="dropdown-menu"
|
||||||
|
class:show
|
||||||
|
aria-labelledby={labelledby}
|
||||||
|
on:mousedown|preventDefault|stopPropagation
|
||||||
|
>
|
||||||
<div class="dropdown-content {className}">
|
<div class="dropdown-content {className}">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@ -24,7 +30,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
|
border-radius: 5px;
|
||||||
background-color: var(--frame-bg);
|
background-color: var(--frame-bg);
|
||||||
border-color: var(--medium-border);
|
border-color: var(--medium-border);
|
||||||
|
min-width: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -7,16 +7,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
let className: string = "";
|
let className: string = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
export let height: number;
|
export let height: number = 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer {id} bind:offsetHeight={height} class={`container-fluid pt-1 ${className}`}>
|
<footer {id} bind:offsetHeight={height} class="container-fluid pt-1 {className}">
|
||||||
<slot />
|
<slot />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
footer {
|
footer {
|
||||||
position: fixed;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
@ -8,12 +8,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav {id} class={`container-fluid py-1 ${className}`}>
|
<header {id} class="container-fluid pb-1 {className}">
|
||||||
<slot />
|
<slot />
|
||||||
</nav>
|
</header>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
nav {
|
header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
@ -46,6 +46,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
$: dropClass = `drop${drop}`;
|
$: dropClass = `drop${drop}`;
|
||||||
|
|
||||||
|
export let skidding = 0;
|
||||||
|
export let distance = 2;
|
||||||
|
|
||||||
setContext(dropdownKey, {
|
setContext(dropdownKey, {
|
||||||
dropdown: true,
|
dropdown: true,
|
||||||
"data-bs-toggle": "dropdown",
|
"data-bs-toggle": "dropdown",
|
||||||
@ -72,6 +75,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
dropdown = new Dropdown(toggle, {
|
dropdown = new Dropdown(toggle, {
|
||||||
autoClose,
|
autoClose,
|
||||||
|
offset: [skidding, distance],
|
||||||
popperConfig: { placement },
|
popperConfig: { placement },
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
@ -79,7 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
dropdown.show();
|
dropdown.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
let api = {
|
api = {
|
||||||
show: dropdown.show.bind(dropdown),
|
show: dropdown.show.bind(dropdown),
|
||||||
toggle: dropdown.toggle.bind(dropdown),
|
toggle: dropdown.toggle.bind(dropdown),
|
||||||
hide: dropdown.hide.bind(dropdown),
|
hide: dropdown.hide.bind(dropdown),
|
||||||
|
@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import type Modal from "bootstrap/js/dist/modal";
|
import type Modal from "bootstrap/js/dist/modal";
|
||||||
|
|
||||||
import TextInputModal from "./TextInputModal.svelte";
|
import TextInputModal from "./TextInputModal.svelte";
|
||||||
import StickyBar from "components/StickyBar.svelte";
|
import StickyHeader from "components/StickyHeader.svelte";
|
||||||
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
||||||
import Item from "components/Item.svelte";
|
import Item from "components/Item.svelte";
|
||||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||||
@ -87,7 +87,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
bind:modalKey
|
bind:modalKey
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StickyBar class="g-1">
|
<StickyHeader class="g-1">
|
||||||
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
||||||
<Item>
|
<Item>
|
||||||
<ButtonGroup class="flex-grow-1">
|
<ButtonGroup class="flex-grow-1">
|
||||||
@ -115,4 +115,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyBar>
|
</StickyHeader>
|
||||||
|
114
ts/editable/BUILD.bazel
Normal file
114
ts/editable/BUILD.bazel
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||||
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
|
load("//ts:esbuild.bzl", "esbuild")
|
||||||
|
load("//ts:vendor.bzl", "copy_mdi_icons")
|
||||||
|
load("//ts:compile_sass.bzl", "compile_sass")
|
||||||
|
|
||||||
|
svelte_files = glob(["*.svelte"])
|
||||||
|
|
||||||
|
svelte_names = [f.replace(".svelte", "") for f in svelte_files]
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "svelte_components",
|
||||||
|
srcs = svelte_names,
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
compile_svelte(
|
||||||
|
name = "svelte",
|
||||||
|
srcs = svelte_files,
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/components",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
compile_sass(
|
||||||
|
srcs = [
|
||||||
|
"editable-base.scss",
|
||||||
|
],
|
||||||
|
group = "editable_scss",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/sass:scrollbar_lib",
|
||||||
|
"//ts/sass/codemirror",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
copy_mdi_icons(
|
||||||
|
name = "mdi-icons",
|
||||||
|
icons = [
|
||||||
|
"math-integral-box.svg",
|
||||||
|
],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "editable",
|
||||||
|
srcs = glob(["*.ts"]),
|
||||||
|
module_name = "editable",
|
||||||
|
tsconfig = "//ts:tsconfig.json",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/lib",
|
||||||
|
"//ts/sveltelib",
|
||||||
|
"//ts/components",
|
||||||
|
"//ts:image_module_support",
|
||||||
|
"@npm//svelte",
|
||||||
|
"@npm//mathjax-full",
|
||||||
|
"@npm//mathjax",
|
||||||
|
] + svelte_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
esbuild(
|
||||||
|
name = "editable-build",
|
||||||
|
args = [
|
||||||
|
"--loader:.svg=text",
|
||||||
|
"--resolve-extensions=.mjs,.js",
|
||||||
|
"--log-level=warning",
|
||||||
|
],
|
||||||
|
entry_point = "index.ts",
|
||||||
|
output_css = "editable-build.css",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"mdi-icons",
|
||||||
|
"editable",
|
||||||
|
"editable_scss",
|
||||||
|
"svelte_components",
|
||||||
|
"//ts/components",
|
||||||
|
"//ts/components:svelte_components",
|
||||||
|
"@npm//protobufjs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
################
|
||||||
|
|
||||||
|
prettier_test(
|
||||||
|
name = "format_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
eslint_test(
|
||||||
|
name = "eslint",
|
||||||
|
srcs = glob(
|
||||||
|
[
|
||||||
|
"*.ts",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
svelte_check(
|
||||||
|
name = "svelte_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]) + [
|
||||||
|
"//ts/components",
|
||||||
|
],
|
||||||
|
)
|
64
ts/editable/Mathjax.svelte
Normal file
64
ts/editable/Mathjax.svelte
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!--
|
||||||
|
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, getContext } from "svelte";
|
||||||
|
import { nightModeKey } from "components/context-keys";
|
||||||
|
import { convertMathjax } from "./mathjax";
|
||||||
|
|
||||||
|
export let mathjax: string;
|
||||||
|
export let block: boolean;
|
||||||
|
export let autofocus = false;
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
setTimeout(() => (imageHeight = image.getBoundingClientRect().height));
|
||||||
|
}
|
||||||
|
|
||||||
|
let image: HTMLImageElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (autofocus) {
|
||||||
|
image.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img
|
||||||
|
bind:this={image}
|
||||||
|
src="data:image/svg+xml,{encoded}"
|
||||||
|
class:block
|
||||||
|
class:empty
|
||||||
|
style="--vertical-center: {-imageHeight / 2 + fontSize / 4}px;"
|
||||||
|
alt="Mathjax"
|
||||||
|
{title}
|
||||||
|
data-anki="mathjax"
|
||||||
|
on:dragstart|preventDefault
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
img {
|
||||||
|
vertical-align: var(--vertical-center);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
</style>
|
45
ts/editable/decorated.ts
Normal file
45
ts/editable/decorated.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/**
|
||||||
|
* decorated elements know three states:
|
||||||
|
* - stored, which is stored in the DB, e.g. `\(\alpha + \beta\)`
|
||||||
|
* - undecorated, which is displayed to the user in Codable, e.g. `<anki-mathjax>\alpha + \beta</anki-mathjax>`
|
||||||
|
* - decorated, which is displayed to the user in Editable, e.g. `<anki-mathjax data-mathjax="\alpha + \beta"><img src="data:..."></anki-mathjax>`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DecoratedElement extends HTMLElement {
|
||||||
|
/**
|
||||||
|
* Transforms itself from undecorated to decorated state.
|
||||||
|
* Should be called in connectedCallback.
|
||||||
|
*/
|
||||||
|
decorate(): void;
|
||||||
|
/**
|
||||||
|
* Transforms itself from decorated to undecorated state.
|
||||||
|
*/
|
||||||
|
undecorate(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecoratedElementConstructor extends CustomElementConstructor {
|
||||||
|
prototype: DecoratedElement;
|
||||||
|
tagName: string;
|
||||||
|
/**
|
||||||
|
* Transforms elements in input HTML from undecorated to stored state.
|
||||||
|
*/
|
||||||
|
toStored(undecorated: string): string;
|
||||||
|
/**
|
||||||
|
* Transforms elements in input HTML from stored to undecorated state.
|
||||||
|
*/
|
||||||
|
toUndecorated(stored: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefineArray extends Array {
|
||||||
|
push(...elements: DecoratedElementConstructor[]) {
|
||||||
|
for (const element of elements) {
|
||||||
|
customElements.define(element.tagName, element);
|
||||||
|
}
|
||||||
|
return super.push(...elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decoratedComponents: DecoratedElementConstructor[] = new DefineArray();
|
@ -15,6 +15,10 @@ anki-editable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anki-mathjax {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
96
ts/editable/editable-container.ts
Normal file
96
ts/editable/editable-container.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
|
function loadStyleLink(container: Node, href: string): Promise<void> {
|
||||||
|
const rootStyle = document.createElement("link");
|
||||||
|
rootStyle.setAttribute("rel", "stylesheet");
|
||||||
|
rootStyle.setAttribute("href", href);
|
||||||
|
|
||||||
|
let styleResolve: () => void;
|
||||||
|
const stylePromise = new Promise<void>((resolve) => (styleResolve = resolve));
|
||||||
|
|
||||||
|
rootStyle.addEventListener("load", () => styleResolve());
|
||||||
|
container.appendChild(rootStyle);
|
||||||
|
|
||||||
|
return stylePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStyleTag(container: Node): [HTMLStyleElement, Promise<void>] {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.setAttribute("rel", "stylesheet");
|
||||||
|
|
||||||
|
let styleResolve: () => void;
|
||||||
|
const stylePromise = new Promise<void>((resolve) => (styleResolve = resolve));
|
||||||
|
|
||||||
|
style.addEventListener("load", () => styleResolve());
|
||||||
|
container.appendChild(style);
|
||||||
|
|
||||||
|
return [style, stylePromise];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditableContainer extends HTMLDivElement {
|
||||||
|
baseStyle: HTMLStyleElement;
|
||||||
|
imageStyle: HTMLStyleElement;
|
||||||
|
|
||||||
|
imagePromise: Promise<void>;
|
||||||
|
stylePromise: Promise<void>;
|
||||||
|
|
||||||
|
baseRule?: CSSStyleRule;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const shadow = this.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
if (document.documentElement.classList.contains("night-mode")) {
|
||||||
|
this.classList.add("night-mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootPromise = loadStyleLink(shadow, "./_anki/css/editable-build.css");
|
||||||
|
const [baseStyle, basePromise] = loadStyleTag(shadow);
|
||||||
|
const [imageStyle, imagePromise] = loadStyleTag(shadow);
|
||||||
|
|
||||||
|
this.baseStyle = baseStyle;
|
||||||
|
this.imageStyle = imageStyle;
|
||||||
|
|
||||||
|
this.imagePromise = imagePromise;
|
||||||
|
this.stylePromise = Promise.all([
|
||||||
|
rootPromise,
|
||||||
|
basePromise,
|
||||||
|
imagePromise,
|
||||||
|
]) as unknown as Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
const sheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||||
|
const baseIndex = sheet.insertRule("anki-editable {}");
|
||||||
|
this.baseRule = sheet.cssRules[baseIndex] as CSSStyleRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(color: string): void {
|
||||||
|
this.setBaseColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaseColor(color: string): void {
|
||||||
|
if (this.baseRule) {
|
||||||
|
this.baseRule.style.color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
|
||||||
|
if (this.baseRule) {
|
||||||
|
this.baseRule.style.fontFamily = fontFamily;
|
||||||
|
this.baseRule.style.fontSize = fontSize;
|
||||||
|
this.baseRule.style.direction = direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRightToLeft(): boolean {
|
||||||
|
return this.baseRule!.style.direction === "rtl";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
|
@ -1,10 +1,24 @@
|
|||||||
// 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
|
||||||
|
|
||||||
import { bridgeCommand } from "./lib";
|
/* eslint
|
||||||
import { elementIsBlock, caretToEnd, getBlockElement } from "./helpers";
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
import { inCodable } from "./toolbar";
|
*/
|
||||||
import { wrap } from "./wrap";
|
|
||||||
|
import type { DecoratedElement } from "./decorated";
|
||||||
|
import { decoratedComponents } from "./decorated";
|
||||||
|
import { bridgeCommand } from "lib/bridgecommand";
|
||||||
|
import { elementIsBlock, getBlockElement } from "lib/dom";
|
||||||
|
import { wrapInternal } from "lib/wrap";
|
||||||
|
|
||||||
|
export function caretToEnd(node: Node): void {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
function containsInlineContent(element: Element): boolean {
|
function containsInlineContent(element: Element): boolean {
|
||||||
for (const child of element.children) {
|
for (const child of element.children) {
|
||||||
@ -26,26 +40,32 @@ export class Editable extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get fieldHTML(): string {
|
get fieldHTML(): string {
|
||||||
return containsInlineContent(this) && this.innerHTML.endsWith("<br>")
|
const clone = this.cloneNode(true) as Element;
|
||||||
? this.innerHTML.slice(0, -4) // trim trailing <br>
|
|
||||||
: this.innerHTML;
|
for (const component of decoratedComponents) {
|
||||||
|
for (const element of clone.getElementsByTagName(component.tagName)) {
|
||||||
|
(element as DecoratedElement).undecorate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
containsInlineContent(clone) && clone.innerHTML.endsWith("<br>")
|
||||||
|
? clone.innerHTML.slice(0, -4) // trim trailing <br>
|
||||||
|
: clone.innerHTML;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.setAttribute("contenteditable", "");
|
this.setAttribute("contenteditable", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
focus(): void {
|
|
||||||
super.focus();
|
|
||||||
inCodable.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
caretToEnd(): void {
|
caretToEnd(): void {
|
||||||
caretToEnd(this);
|
caretToEnd(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
surroundSelection(before: string, after: string): void {
|
surroundSelection(before: string, after: string): void {
|
||||||
wrap(before, after);
|
wrapInternal(this.getRootNode() as ShadowRoot, before, after, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter(event: KeyboardEvent): void {
|
onEnter(event: KeyboardEvent): void {
|
||||||
@ -63,3 +83,5 @@ export class Editable extends HTMLElement {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editable", Editable);
|
4
ts/editable/icons.ts
Normal file
4
ts/editable/icons.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
export { default as mathIcon } from "./math-integral-box.svg";
|
7
ts/editable/index.ts
Normal file
7
ts/editable/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import "./editable-base.css";
|
||||||
|
import "./editable-container";
|
||||||
|
import "./editable";
|
||||||
|
import "./mathjax-component";
|
196
ts/editable/mathjax-component.ts
Normal file
196
ts/editable/mathjax-component.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "mathjax/es5/tex-svg-full";
|
||||||
|
|
||||||
|
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
|
||||||
|
import { decoratedComponents } from "./decorated";
|
||||||
|
import { nodeIsElement } from "lib/dom";
|
||||||
|
import { nightModeKey } from "components/context-keys";
|
||||||
|
|
||||||
|
import Mathjax_svelte from "./Mathjax.svelte";
|
||||||
|
|
||||||
|
function moveNodeOutOfElement(
|
||||||
|
element: Element,
|
||||||
|
node: Node,
|
||||||
|
placement: "beforebegin" | "afterend"
|
||||||
|
): Node {
|
||||||
|
element.removeChild(node);
|
||||||
|
|
||||||
|
let referenceNode: Node;
|
||||||
|
|
||||||
|
if (nodeIsElement(node)) {
|
||||||
|
referenceNode = element.insertAdjacentElement(placement, node)!;
|
||||||
|
} else {
|
||||||
|
element.insertAdjacentText(placement, (node as Text).wholeText);
|
||||||
|
referenceNode =
|
||||||
|
placement === "beforebegin"
|
||||||
|
? element.previousSibling!
|
||||||
|
: element.nextSibling!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return referenceNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeCaretAfter(node: Node): void {
|
||||||
|
const range = document.createRange();
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNodes = [...element.childNodes];
|
||||||
|
const allowedIndex = childNodes.findIndex((child) => child === allowedChild);
|
||||||
|
|
||||||
|
const beforeChildren = childNodes.slice(0, allowedIndex);
|
||||||
|
const afterChildren = childNodes.slice(allowedIndex + 1);
|
||||||
|
|
||||||
|
// Special treatment for pressing return after mathjax block
|
||||||
|
if (
|
||||||
|
afterChildren.length === 2 &&
|
||||||
|
afterChildren.every((child) => (child as Element).tagName === "BR")
|
||||||
|
) {
|
||||||
|
const first = afterChildren.pop();
|
||||||
|
element.removeChild(first!);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastNode: Node | null = null;
|
||||||
|
|
||||||
|
for (const node of beforeChildren) {
|
||||||
|
lastNode = moveNodeOutOfElement(element, node, "beforebegin");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of afterChildren) {
|
||||||
|
lastNode = moveNodeOutOfElement(element, node, "afterend");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastNode) {
|
||||||
|
placeCaretAfter(lastNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(element, { childList: true, characterData: true });
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const mathjaxTagPattern =
|
||||||
|
/<anki-mathjax(?:[^>]*?block="(.*?)")?[^>]*?>(.*?)<\/anki-mathjax>/gsu;
|
||||||
|
|
||||||
|
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
|
||||||
|
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
|
||||||
|
|
||||||
|
export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||||
|
extends HTMLElement
|
||||||
|
implements DecoratedElement
|
||||||
|
{
|
||||||
|
static tagName = "anki-mathjax";
|
||||||
|
|
||||||
|
static toStored(undecorated: string): string {
|
||||||
|
return undecorated.replace(
|
||||||
|
mathjaxTagPattern,
|
||||||
|
(_match: string, block: string | undefined, text: string) => {
|
||||||
|
return typeof block === "string" && block !== "false"
|
||||||
|
? `\\[${text}\\]`
|
||||||
|
: `\\(${text}\\)`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static toUndecorated(stored: string): string {
|
||||||
|
return stored
|
||||||
|
.replace(
|
||||||
|
mathjaxBlockDelimiterPattern,
|
||||||
|
(_match: string, text: string) =>
|
||||||
|
`<anki-mathjax block="true">${text}</anki-mathjax>`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
mathjaxInlineDelimiterPattern,
|
||||||
|
(_match: string, text: string) => `<anki-mathjax>${text}</anki-mathjax>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
block = false;
|
||||||
|
disconnect: () => void = () => {
|
||||||
|
/* noop */
|
||||||
|
};
|
||||||
|
component?: Mathjax_svelte;
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return ["block", "data-mathjax"];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.decorate();
|
||||||
|
this.disconnect = moveNodesInsertedOutside(this, this.children[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, _old: string, newValue: string): void {
|
||||||
|
switch (name) {
|
||||||
|
case "block":
|
||||||
|
this.block = newValue !== "false";
|
||||||
|
this.component?.$set({ block: this.block });
|
||||||
|
break;
|
||||||
|
case "data-mathjax":
|
||||||
|
this.component?.$set({ mathjax: newValue });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(): void {
|
||||||
|
const mathjax = (this.dataset.mathjax = this.innerText);
|
||||||
|
this.innerHTML = "";
|
||||||
|
this.style.whiteSpace = "normal";
|
||||||
|
|
||||||
|
const context = new Map();
|
||||||
|
context.set(
|
||||||
|
nightModeKey,
|
||||||
|
document.documentElement.classList.contains("night-mode")
|
||||||
|
);
|
||||||
|
|
||||||
|
this.component = new Mathjax_svelte({
|
||||||
|
target: this,
|
||||||
|
props: {
|
||||||
|
mathjax,
|
||||||
|
block: this.block,
|
||||||
|
autofocus: this.hasAttribute("focusonmount"),
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
undecorate(): void {
|
||||||
|
this.innerHTML = this.dataset.mathjax ?? "";
|
||||||
|
delete this.dataset.mathjax;
|
||||||
|
this.removeAttribute("style");
|
||||||
|
this.removeAttribute("focusonmount");
|
||||||
|
|
||||||
|
this.component?.$destroy();
|
||||||
|
this.component = undefined;
|
||||||
|
|
||||||
|
if (this.block) {
|
||||||
|
this.setAttribute("block", "true");
|
||||||
|
} else {
|
||||||
|
this.removeAttribute("block");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
decoratedComponents.push(Mathjax);
|
57
ts/editable/mathjax.ts
Normal file
57
ts/editable/mathjax.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import { mathIcon } from "./icons";
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
function getCSS(nightMode: boolean, fontSize: number): string {
|
||||||
|
const color = nightMode ? "white" : "black";
|
||||||
|
/* color is set for Maths, fill for the empty icon */
|
||||||
|
return `svg { color: ${color}; fill: ${color}; font-size: ${fontSize}px; };`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyle(css: string): HTMLStyleElement {
|
||||||
|
const style = document.createElement("style") as HTMLStyleElement;
|
||||||
|
style.appendChild(document.createTextNode(css));
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyIcon(style: HTMLStyleElement): [string, string] {
|
||||||
|
const icon = parser.parseFromString(mathIcon, "image/svg+xml");
|
||||||
|
const svg = icon.children[0];
|
||||||
|
svg.insertBefore(style, svg.children[0]);
|
||||||
|
|
||||||
|
return [svg.outerHTML, "MathJax"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertMathjax(
|
||||||
|
input: string,
|
||||||
|
nightMode: boolean,
|
||||||
|
fontSize: number
|
||||||
|
): [string, string] {
|
||||||
|
const style = getStyle(getCSS(nightMode, fontSize));
|
||||||
|
|
||||||
|
if (input.trim().length === 0) {
|
||||||
|
return getEmptyIcon(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = globalThis.MathJax.tex2svg(input);
|
||||||
|
const svg = output.children[0];
|
||||||
|
|
||||||
|
if (svg.viewBox.baseVal.height === 16) {
|
||||||
|
return getEmptyIcon(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = "";
|
||||||
|
|
||||||
|
if (svg.innerHTML.includes("data-mjx-error")) {
|
||||||
|
svg.querySelector("rect").setAttribute("fill", "yellow");
|
||||||
|
svg.querySelector("text").setAttribute("color", "red");
|
||||||
|
title = svg.querySelector("title").innerHTML;
|
||||||
|
} else {
|
||||||
|
svg.insertBefore(style, svg.children[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [svg.outerHTML, title];
|
||||||
|
}
|
@ -22,18 +22,8 @@ compile_svelte(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
],
|
"@npm//@types/codemirror",
|
||||||
)
|
"@npm//codemirror",
|
||||||
|
|
||||||
compile_sass(
|
|
||||||
srcs = [
|
|
||||||
"editable.scss",
|
|
||||||
],
|
|
||||||
group = "editable_scss",
|
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
deps = [
|
|
||||||
"//ts/sass:scrollbar_lib",
|
|
||||||
"//ts/sass/codemirror",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,8 +35,9 @@ compile_sass(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//ts/sass:base_lib",
|
"//ts/sass:base_lib",
|
||||||
"//ts/sass:buttons_lib",
|
|
||||||
"//ts/sass:scrollbar_lib",
|
"//ts/sass:scrollbar_lib",
|
||||||
|
"//ts/sass:buttons_lib",
|
||||||
|
"//ts/sass:button_mixins_lib",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,6 +62,7 @@ ts_library(
|
|||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
"//ts/sveltelib",
|
"//ts/sveltelib",
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
|
"//ts/editable",
|
||||||
"//ts/html-filter",
|
"//ts/html-filter",
|
||||||
"//ts:image_module_support",
|
"//ts:image_module_support",
|
||||||
"@npm//svelte",
|
"@npm//svelte",
|
||||||
@ -130,6 +122,10 @@ copy_mdi_icons(
|
|||||||
"image-size-select-large.svg",
|
"image-size-select-large.svg",
|
||||||
"image-size-select-actual.svg",
|
"image-size-select-actual.svg",
|
||||||
|
|
||||||
|
# mathjax handle
|
||||||
|
"format-wrap-square.svg",
|
||||||
|
"format-wrap-top-bottom.svg",
|
||||||
|
|
||||||
# tag editor
|
# tag editor
|
||||||
"tag-outline.svg",
|
"tag-outline.svg",
|
||||||
"tag.svg",
|
"tag.svg",
|
||||||
@ -156,8 +152,11 @@ esbuild(
|
|||||||
"bootstrap-icons",
|
"bootstrap-icons",
|
||||||
"mdi-icons",
|
"mdi-icons",
|
||||||
"svelte_components",
|
"svelte_components",
|
||||||
|
"//ts/editable",
|
||||||
|
"//ts/editable:mdi-icons",
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
"//ts/components:svelte_components",
|
"//ts/components:svelte_components",
|
||||||
|
"//ts/editable:svelte_components",
|
||||||
"@npm//protobufjs",
|
"@npm//protobufjs",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -192,5 +191,7 @@ svelte_check(
|
|||||||
"//ts/sass/bootstrap",
|
"//ts/sass/bootstrap",
|
||||||
"@npm//@types/bootstrap",
|
"@npm//@types/bootstrap",
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
|
"@npm//@types/codemirror",
|
||||||
|
"@npm//codemirror",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { isApplePlatform } from "lib/platform";
|
import { isApplePlatform } from "lib/platform";
|
||||||
import StickyBar from "components/StickyBar.svelte";
|
import StickyHeader from "components/StickyHeader.svelte";
|
||||||
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
||||||
import Item from "components/Item.svelte";
|
import Item from "components/Item.svelte";
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
export const templateButtons = {};
|
export const templateButtons = {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<StickyBar>
|
<StickyHeader>
|
||||||
<ButtonToolbar {size} {wrap} api={toolbar}>
|
<ButtonToolbar {size} {wrap} api={toolbar}>
|
||||||
<Item id="notetype">
|
<Item id="notetype">
|
||||||
<NoteTypeButtons api={notetypeButtons} />
|
<NoteTypeButtons api={notetypeButtons} />
|
||||||
@ -73,4 +73,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<TemplateButtons api={templateButtons} />
|
<TemplateButtons api={templateButtons} />
|
||||||
</Item>
|
</Item>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyBar>
|
</StickyHeader>
|
||||||
|
@ -14,7 +14,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import OnlyEditable from "./OnlyEditable.svelte";
|
import OnlyEditable from "./OnlyEditable.svelte";
|
||||||
import CommandIconButton from "./CommandIconButton.svelte";
|
import CommandIconButton from "./CommandIconButton.svelte";
|
||||||
|
|
||||||
import { getCurrentField, getListItem } from "./helpers";
|
import { getListItem } from "lib/dom";
|
||||||
|
import { getCurrentField } from "./helpers";
|
||||||
import {
|
import {
|
||||||
ulIcon,
|
ulIcon,
|
||||||
olIcon,
|
olIcon,
|
||||||
@ -31,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
function outdentListItem() {
|
function outdentListItem() {
|
||||||
const currentField = getCurrentField();
|
const currentField = getCurrentField();
|
||||||
if (getListItem(currentField.editableContainer.shadowRoot!)) {
|
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
|
||||||
document.execCommand("outdent");
|
document.execCommand("outdent");
|
||||||
} else {
|
} else {
|
||||||
alert("Indent/unindent currently only works with lists.");
|
alert("Indent/unindent currently only works with lists.");
|
||||||
@ -40,7 +41,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
function indentListItem() {
|
function indentListItem() {
|
||||||
const currentField = getCurrentField();
|
const currentField = getCurrentField();
|
||||||
if (getListItem(currentField.editableContainer.shadowRoot!)) {
|
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
|
||||||
document.execCommand("indent");
|
document.execCommand("indent");
|
||||||
} else {
|
} else {
|
||||||
alert("Indent/unindent currently only works with lists.");
|
alert("Indent/unindent currently only works with lists.");
|
||||||
|
@ -5,13 +5,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, createEventDispatcher } from "svelte";
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
let background: HTMLDivElement;
|
let background: HTMLDivElement;
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
onMount(() => dispatch("mount", { background }));
|
onMount(() => dispatch("mount", { background }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={background} on:mousedown|preventDefault on:dblclick />
|
<div
|
||||||
|
bind:this={background}
|
||||||
|
title={tooltip}
|
||||||
|
on:mousedown|preventDefault
|
||||||
|
on:click|stopPropagation
|
||||||
|
on:dblclick
|
||||||
|
/>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
div {
|
div {
|
||||||
|
@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
let dimensions: HTMLDivElement;
|
let dimensions: HTMLDivElement;
|
||||||
let overflowFix = 0;
|
let overflowFix = 0;
|
||||||
|
|
||||||
function updateOverflow(dimensions: HTMLDivElement) {
|
function updateOverflow(dimensions: HTMLDivElement): void {
|
||||||
const boundingClientRect = dimensions.getBoundingClientRect();
|
const boundingClientRect = dimensions.getBoundingClientRect();
|
||||||
const overflow = isRtl
|
const overflow = isRtl
|
||||||
? window.innerWidth - boundingClientRect.x - boundingClientRect.width
|
? window.innerWidth - boundingClientRect.x - boundingClientRect.width
|
||||||
@ -19,12 +19,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
overflowFix = Math.min(0, overflowFix + overflow, overflow);
|
overflowFix = Math.min(0, overflowFix + overflow, overflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOverflowAsync(dimensions: HTMLDivElement) {
|
afterUpdate(() => updateOverflow(dimensions));
|
||||||
|
|
||||||
|
function updateOverflowAsync(dimensions: HTMLDivElement): void {
|
||||||
setTimeout(() => updateOverflow(dimensions));
|
setTimeout(() => updateOverflow(dimensions));
|
||||||
}
|
}
|
||||||
|
|
||||||
afterUpdate(() => updateOverflow(dimensions));
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
onMount(() => dispatch("mount"));
|
onMount(() => dispatch("mount"));
|
||||||
|
@ -15,7 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
let width: number;
|
let width: number;
|
||||||
let height: number;
|
let height: number;
|
||||||
|
|
||||||
export function updateSelection(_div: HTMLDivElement): void {
|
function setSelection(_selection?: HTMLDivElement): void {
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const imageRect = image!.getBoundingClientRect();
|
const imageRect = image!.getBoundingClientRect();
|
||||||
|
|
||||||
@ -28,6 +28,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
height = image!.clientHeight;
|
height = image!.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateSelection(): Promise<void> {
|
||||||
|
let updateResolve: () => void;
|
||||||
|
const afterUpdate: Promise<void> = new Promise((resolve) => {
|
||||||
|
updateResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelection();
|
||||||
|
setTimeout(() => updateResolve());
|
||||||
|
|
||||||
|
return afterUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let selection: HTMLDivElement;
|
let selection: HTMLDivElement;
|
||||||
|
|
||||||
@ -36,7 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={selection}
|
bind:this={selection}
|
||||||
use:updateSelection
|
use:setSelection
|
||||||
on:click={(event) =>
|
on:click={(event) =>
|
||||||
/* prevent triggering Bootstrap dropdown */ event.stopImmediatePropagation()}
|
/* prevent triggering Bootstrap dropdown */ event.stopImmediatePropagation()}
|
||||||
style="--left: {left}px; --top: {top}px; --width: {width}px; --height: {height}px; --offsetX: {offsetX}px; --offsetY: {offsetY}px;"
|
style="--left: {left}px; --top: {top}px; --width: {width}px; --height: {height}px; --offsetX: {offsetX}px; --offsetY: {offsetY}px;"
|
||||||
|
@ -17,9 +17,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
|
|
||||||
|
export let activeImage: HTMLImageElement | null = null;
|
||||||
export let container: HTMLElement;
|
export let container: HTMLElement;
|
||||||
export let sheet: CSSStyleSheet;
|
export let sheet: CSSStyleSheet;
|
||||||
export let activeImage: HTMLImageElement | null = null;
|
|
||||||
export let isRtl: boolean = false;
|
export let isRtl: boolean = false;
|
||||||
|
|
||||||
$: naturalWidth = activeImage?.naturalWidth;
|
$: naturalWidth = activeImage?.naturalWidth;
|
||||||
@ -53,10 +53,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updateSelection: () => void;
|
let updateSelection: () => Promise<void>;
|
||||||
|
|
||||||
async function updateSizesWithDimensions() {
|
async function updateSizesWithDimensions() {
|
||||||
updateSelection();
|
await updateSelection();
|
||||||
updateDimensions();
|
updateDimensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,9 +135,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
{#if sheet}
|
{#if sheet}
|
||||||
<WithDropdown
|
<WithDropdown
|
||||||
placement="bottom"
|
drop="down"
|
||||||
autoOpen={true}
|
autoOpen={true}
|
||||||
autoClose={false}
|
autoClose={false}
|
||||||
|
distance={3}
|
||||||
let:createDropdown
|
let:createDropdown
|
||||||
let:dropdownObject
|
let:dropdownObject
|
||||||
>
|
>
|
||||||
|
@ -41,7 +41,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
active={image.style.float === "" || image.style.float === "none"}
|
active={image.style.float === "" || image.style.float === "none"}
|
||||||
flipX={isRtl}
|
flipX={isRtl}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
image.style.float = "";
|
image.style.removeProperty("float");
|
||||||
|
|
||||||
|
if (image.getAttribute("style")?.length === 0) {
|
||||||
|
image.removeAttribute("style");
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => dispatch("update"));
|
setTimeout(() => dispatch("update"));
|
||||||
}}>{@html floatNoneIcon}</IconButton
|
}}>{@html floatNoneIcon}</IconButton
|
||||||
>
|
>
|
||||||
|
104
ts/editor/MathjaxHandle.svelte
Normal file
104
ts/editor/MathjaxHandle.svelte
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
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 MathjaxHandleInlineBlock from "./MathjaxHandleInlineBlock.svelte";
|
||||||
|
import MathjaxHandleEditor from "./MathjaxHandleEditor.svelte";
|
||||||
|
|
||||||
|
export let activeImage: HTMLImageElement | null = null;
|
||||||
|
export let container: HTMLElement;
|
||||||
|
export let isRtl: boolean;
|
||||||
|
|
||||||
|
let dropdownApi: any;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(async () => {
|
||||||
|
if (activeImage) {
|
||||||
|
await updateSelection();
|
||||||
|
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) {
|
||||||
|
getComponent(activeImage!).dataset.mathjax = event.detail.mathjax;
|
||||||
|
|
||||||
|
let selectionResolve: (value: void) => void;
|
||||||
|
const afterSelectionUpdate = new Promise((resolve) => {
|
||||||
|
selectionResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
errorMessage = activeImage!.title;
|
||||||
|
await updateSelection();
|
||||||
|
selectionResolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
return afterSelectionUpdate;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<WithDropdown
|
||||||
|
drop="down"
|
||||||
|
autoOpen={true}
|
||||||
|
autoClose={false}
|
||||||
|
distance={4}
|
||||||
|
let:createDropdown
|
||||||
|
let:dropdownObject
|
||||||
|
>
|
||||||
|
{#if activeImage}
|
||||||
|
<HandleSelection
|
||||||
|
image={activeImage}
|
||||||
|
{container}
|
||||||
|
bind:updateSelection
|
||||||
|
on:mount={(event) => (dropdownApi = createDropdown(event.detail.selection))}
|
||||||
|
>
|
||||||
|
<HandleBackground tooltip={errorMessage} />
|
||||||
|
|
||||||
|
<HandleControl offsetX={1} offsetY={1} />
|
||||||
|
</HandleSelection>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<MathjaxHandleEditor
|
||||||
|
initialValue={getComponent(activeImage).dataset.mathjax ?? ""}
|
||||||
|
on:update={async (event) => {
|
||||||
|
await onEditorUpdate(event);
|
||||||
|
dropdownObject.update();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="margin-x">
|
||||||
|
<ButtonToolbar>
|
||||||
|
<Item>
|
||||||
|
<MathjaxHandleInlineBlock
|
||||||
|
{activeImage}
|
||||||
|
{isRtl}
|
||||||
|
on:click={updateSelection}
|
||||||
|
/>
|
||||||
|
</Item>
|
||||||
|
</ButtonToolbar>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu>
|
||||||
|
{/if}
|
||||||
|
</WithDropdown>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.margin-x {
|
||||||
|
margin: 0 0.125rem;
|
||||||
|
}
|
||||||
|
</style>
|
70
ts/editor/MathjaxHandleEditor.svelte
Normal file
70
ts/editor/MathjaxHandleEditor.svelte
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
import { ChangeTimer } from "./change-timer";
|
||||||
|
import { CodeMirror, latex, baseOptions } from "./codeMirror";
|
||||||
|
|
||||||
|
export let initialValue: string;
|
||||||
|
|
||||||
|
const codeMirrorOptions = {
|
||||||
|
mode: latex,
|
||||||
|
...baseOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
let codeMirror: CodeMirror.EditorFromTextArea;
|
||||||
|
const changeTimer = new ChangeTimer();
|
||||||
|
|
||||||
|
function onInput() {
|
||||||
|
changeTimer.schedule(
|
||||||
|
() => dispatch("update", { mathjax: codeMirror.getValue() }),
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCodemirror(textarea: HTMLTextAreaElement): void {
|
||||||
|
codeMirror = CodeMirror.fromTextArea(textarea, codeMirrorOptions);
|
||||||
|
codeMirror.on("change", onInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
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">
|
||||||
|
/* TODO there is global CSS in fields.scss */
|
||||||
|
div :global(.mathjax-editor) {
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: 0 1px;
|
||||||
|
border-color: var(--medium-border);
|
||||||
|
}
|
||||||
|
</style>
|
38
ts/editor/MathjaxHandleInlineBlock.svelte
Normal file
38
ts/editor/MathjaxHandleInlineBlock.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import * as tr from "lib/i18n";
|
||||||
|
|
||||||
|
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||||
|
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||||
|
import IconButton from "components/IconButton.svelte";
|
||||||
|
|
||||||
|
import { inlineIcon, blockIcon } from "./icons";
|
||||||
|
|
||||||
|
export let activeImage: HTMLImageElement;
|
||||||
|
export let isRtl: boolean;
|
||||||
|
|
||||||
|
$: mathjaxElement = activeImage.parentElement!;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonGroup size={1.6} wrap={false} reverse={isRtl}>
|
||||||
|
<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>
|
@ -6,8 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import { isApplePlatform } from "lib/platform";
|
import { isApplePlatform } from "lib/platform";
|
||||||
import { bridgeCommand } from "lib/bridgecommand";
|
import { bridgeCommand } from "lib/bridgecommand";
|
||||||
import Spacer from "components/Spacer.svelte";
|
import StickyFooter from "components/StickyFooter.svelte";
|
||||||
import StickyBottom from "components/StickyBottom.svelte";
|
|
||||||
import TagOptionsBadge from "./TagOptionsBadge.svelte";
|
import TagOptionsBadge from "./TagOptionsBadge.svelte";
|
||||||
import TagEditMode from "./TagEditMode.svelte";
|
import TagEditMode from "./TagEditMode.svelte";
|
||||||
import TagInput from "./TagInput.svelte";
|
import TagInput from "./TagInput.svelte";
|
||||||
@ -65,7 +64,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
return response.tags;
|
return response.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colonAtStartOrEnd = /^:?|:?$/g;
|
const withoutSingleColonAtStartOrEnd = /^:?([^:].*?[^:]):?$/;
|
||||||
|
|
||||||
function updateSuggestions(): void {
|
function updateSuggestions(): void {
|
||||||
const activeTag = tags[active!];
|
const activeTag = tags[active!];
|
||||||
@ -76,11 +75,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
if (autocompleteDisabled) {
|
if (autocompleteDisabled) {
|
||||||
suggestionsPromise = noSuggestions;
|
suggestionsPromise = noSuggestions;
|
||||||
} else {
|
} else {
|
||||||
const cleanedName = replaceWithColons(activeName).replace(
|
const withColons = replaceWithColons(activeName);
|
||||||
colonAtStartOrEnd,
|
const withoutSingleColons = withoutSingleColonAtStartOrEnd.test(withColons)
|
||||||
""
|
? withColons.replace(withoutSingleColonAtStartOrEnd, "$1")
|
||||||
);
|
: withColons;
|
||||||
suggestionsPromise = fetchSuggestions(cleanedName).then(
|
|
||||||
|
suggestionsPromise = fetchSuggestions(withoutSingleColons).then(
|
||||||
(names: string[]): string[] => {
|
(names: string[]): string[] => {
|
||||||
autocompleteDisabled = names.length === 0;
|
autocompleteDisabled = names.length === 0;
|
||||||
return names.map(replaceWithUnicodeSeparator);
|
return names.map(replaceWithUnicodeSeparator);
|
||||||
@ -390,9 +390,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
$: shortenTags = shortenTags || assumedRows > 2;
|
$: shortenTags = shortenTags || assumedRows > 2;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Spacer --height="{height}px" />
|
<StickyFooter bind:height class="d-flex">
|
||||||
|
|
||||||
<StickyBottom class="d-flex" bind:height>
|
|
||||||
{#if !wrap}
|
{#if !wrap}
|
||||||
<TagOptionsBadge
|
<TagOptionsBadge
|
||||||
--buttons-size="{size}rem"
|
--buttons-size="{size}rem"
|
||||||
@ -505,7 +503,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<Tag>SPACER</Tag>
|
<Tag>SPACER</Tag>
|
||||||
</div>
|
</div>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyBottom>
|
</StickyFooter>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tag-spacer {
|
.tag-spacer {
|
||||||
|
@ -132,6 +132,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
setPosition(positionStart);
|
setPosition(positionStart);
|
||||||
|
dispatch("taginput");
|
||||||
return;
|
return;
|
||||||
} else if (after.startsWith(":")) {
|
} else if (after.startsWith(":")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -101,7 +101,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
let:shortcutLabel
|
let:shortcutLabel
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => wrapCurrent("\\(", "\\)")}
|
on:click={() =>
|
||||||
|
wrapCurrent(
|
||||||
|
"<anki-mathjax focusonmount>",
|
||||||
|
"</anki-mathjax>"
|
||||||
|
)}
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingMathjaxInline()}
|
{tr.editingMathjaxInline()}
|
||||||
@ -115,7 +119,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
let:shortcutLabel
|
let:shortcutLabel
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => wrapCurrent("\\[", "\\]")}
|
on:click={() =>
|
||||||
|
wrapCurrent(
|
||||||
|
'<anki-mathjax block="true" focusonmount>',
|
||||||
|
"</anki-matjax>"
|
||||||
|
)}
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingMathjaxBlock()}
|
{tr.editingMathjaxBlock()}
|
||||||
@ -129,7 +137,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
let:shortcutLabel
|
let:shortcutLabel
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => wrapCurrent("\\(\\ce{", "}\\)")}
|
on:click={() =>
|
||||||
|
wrapCurrent(
|
||||||
|
"<anki-mathjax focusonmount>\\ce{",
|
||||||
|
"}</anki-mathjax>"
|
||||||
|
)}
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingMathjaxChemistry()}
|
{tr.editingMathjaxChemistry()}
|
||||||
|
@ -4,12 +4,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
import { createEventDispatcher, onDestroy } from "svelte";
|
||||||
import { nodeIsElement } from "./helpers";
|
import { nodeIsElement } from "lib/dom";
|
||||||
|
|
||||||
|
export let activeImage: HTMLImageElement | null;
|
||||||
export let container: HTMLElement;
|
export let container: HTMLElement;
|
||||||
export let sheet: CSSStyleSheet;
|
export let sheet: CSSStyleSheet;
|
||||||
|
|
||||||
export let activeImage: HTMLImageElement | null;
|
|
||||||
let active: boolean = false;
|
let active: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
@ -70,7 +70,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.tagName === "IMG") {
|
if (node.tagName === "IMG" && !(node as HTMLElement).dataset.anki) {
|
||||||
result.push(node as HTMLImageElement);
|
result.push(node as HTMLImageElement);
|
||||||
} else {
|
} else {
|
||||||
result.push(...filterImages(node.children));
|
result.push(...filterImages(node.children));
|
||||||
|
@ -1,47 +1,18 @@
|
|||||||
// 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
|
||||||
|
|
||||||
import type { EditingArea } from "./editing-area";
|
export class ChangeTimer {
|
||||||
|
private value: number | null = null;
|
||||||
|
|
||||||
import { getCurrentField } from "./helpers";
|
schedule(action: () => void, delay: number): void {
|
||||||
import { bridgeCommand } from "./lib";
|
this.clear();
|
||||||
import { getNoteId } from "./note-id";
|
this.value = setTimeout(action, delay);
|
||||||
|
|
||||||
let changeTimer: number | null = null;
|
|
||||||
|
|
||||||
export function triggerChangeTimer(currentField: EditingArea): void {
|
|
||||||
clearChangeTimer();
|
|
||||||
changeTimer = setTimeout(() => saveField(currentField, "key"), 600);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearChangeTimer(): void {
|
|
||||||
if (changeTimer) {
|
|
||||||
clearTimeout(changeTimer);
|
|
||||||
changeTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
|
|
||||||
clearChangeTimer();
|
|
||||||
const command = `${type}:${currentField.ord}:${getNoteId()}:${
|
|
||||||
currentField.fieldHTML
|
|
||||||
}`;
|
|
||||||
bridgeCommand(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveNow(keepFocus: boolean): void {
|
|
||||||
const currentField = getCurrentField();
|
|
||||||
|
|
||||||
if (!currentField) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChangeTimer();
|
clear(): void {
|
||||||
|
if (this.value) {
|
||||||
if (keepFocus) {
|
clearTimeout(this.value);
|
||||||
saveField(currentField, "key");
|
this.value = null;
|
||||||
} else {
|
}
|
||||||
// triggers onBlur, which saves
|
|
||||||
currentField.blur();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,29 @@
|
|||||||
// 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
|
||||||
|
|
||||||
import * as CodeMirror from "codemirror/lib/codemirror";
|
/* eslint
|
||||||
import "codemirror/mode/htmlmixed/htmlmixed";
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
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, htmlanki, baseOptions, gutterOptions } from "./codeMirror";
|
||||||
import { inCodable } from "./toolbar";
|
import { inCodable } from "./toolbar";
|
||||||
|
|
||||||
const codeMirrorOptions = {
|
const codeMirrorOptions = {
|
||||||
mode: "htmlmixed",
|
mode: htmlanki,
|
||||||
theme: "monokai",
|
...baseOptions,
|
||||||
lineNumbers: true,
|
...gutterOptions,
|
||||||
lineWrapping: true,
|
|
||||||
foldGutter: true,
|
|
||||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
|
||||||
matchTags: { bothTags: true },
|
|
||||||
autoCloseTags: true,
|
|
||||||
extraKeys: { Tab: false, "Shift-Tab": false },
|
|
||||||
viewportMargin: Infinity,
|
|
||||||
lineWiseCopyCut: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
|
const parseStyle = "<style>anki-mathjax { white-space: pre; }</style>";
|
||||||
|
|
||||||
function parseHTML(html: string): string {
|
function parseHTML(html: string): string {
|
||||||
const doc = parser.parseFromString(html, "text/html");
|
const doc = parser.parseFromString(`${parseStyle}${html}`, "text/html");
|
||||||
return doc.body.innerHTML;
|
return doc.body.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Codable extends HTMLTextAreaElement {
|
export class Codable extends HTMLTextAreaElement {
|
||||||
codeMirror: CodeMirror | undefined;
|
codeMirror: CodeMirror.EditorFromTextArea | undefined;
|
||||||
|
|
||||||
get active(): boolean {
|
get active(): boolean {
|
||||||
return Boolean(this.codeMirror);
|
return Boolean(this.codeMirror);
|
||||||
@ -41,14 +31,14 @@ export class Codable extends HTMLTextAreaElement {
|
|||||||
|
|
||||||
set fieldHTML(content: string) {
|
set fieldHTML(content: string) {
|
||||||
if (this.active) {
|
if (this.active) {
|
||||||
this.codeMirror.setValue(content);
|
this.codeMirror?.setValue(content);
|
||||||
} else {
|
} else {
|
||||||
this.value = content;
|
this.value = content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get fieldHTML(): string {
|
get fieldHTML(): string {
|
||||||
return parseHTML(this.active ? this.codeMirror.getValue() : this.value);
|
return parseHTML(this.active ? this.codeMirror!.getValue() : this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
@ -58,26 +48,27 @@ export class Codable extends HTMLTextAreaElement {
|
|||||||
setup(html: string): void {
|
setup(html: string): void {
|
||||||
this.fieldHTML = html;
|
this.fieldHTML = html;
|
||||||
this.codeMirror = CodeMirror.fromTextArea(this, codeMirrorOptions);
|
this.codeMirror = CodeMirror.fromTextArea(this, codeMirrorOptions);
|
||||||
|
this.codeMirror.on("blur", () => inCodable.set(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown(): string {
|
teardown(): string {
|
||||||
this.codeMirror.toTextArea();
|
this.codeMirror!.toTextArea();
|
||||||
this.codeMirror = undefined;
|
this.codeMirror = undefined;
|
||||||
return this.fieldHTML;
|
return this.fieldHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
focus(): void {
|
focus(): void {
|
||||||
this.codeMirror.focus();
|
this.codeMirror!.focus();
|
||||||
inCodable.set(true);
|
inCodable.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
caretToEnd(): void {
|
caretToEnd(): void {
|
||||||
this.codeMirror.setCursor(this.codeMirror.lineCount(), 0);
|
this.codeMirror!.setCursor(this.codeMirror!.lineCount(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
surroundSelection(before: string, after: string): void {
|
surroundSelection(before: string, after: string): void {
|
||||||
const selection = this.codeMirror.getSelection();
|
const selection = this.codeMirror!.getSelection();
|
||||||
this.codeMirror.replaceSelection(before + selection + after);
|
this.codeMirror!.replaceSelection(before + selection + after);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter(): void {
|
onEnter(): void {
|
||||||
@ -88,3 +79,5 @@ export class Codable extends HTMLTextAreaElement {
|
|||||||
/* default */
|
/* default */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
||||||
|
45
ts/editor/codeMirror.ts
Normal file
45
ts/editor/codeMirror.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import CodeMirror from "codemirror";
|
||||||
|
import "codemirror/mode/htmlmixed/htmlmixed";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export { CodeMirror };
|
||||||
|
|
||||||
|
export const latex = {
|
||||||
|
name: "stex",
|
||||||
|
inMathMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const htmlanki = {
|
||||||
|
name: "htmlmixed",
|
||||||
|
tags: {
|
||||||
|
"anki-mathjax": [[null, null, latex]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const noop = (): void => {
|
||||||
|
/* noop */
|
||||||
|
};
|
||||||
|
|
||||||
|
export const baseOptions = {
|
||||||
|
theme: "monokai",
|
||||||
|
lineWrapping: true,
|
||||||
|
matchTags: { bothTags: true },
|
||||||
|
autoCloseTags: true,
|
||||||
|
extraKeys: { Tab: noop, "Shift-Tab": noop },
|
||||||
|
viewportMargin: Infinity,
|
||||||
|
lineWiseCopyCut: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gutterOptions = {
|
||||||
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||||
|
lineNumbers: true,
|
||||||
|
foldGutter: true,
|
||||||
|
};
|
@ -1,59 +0,0 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
/* eslint
|
|
||||||
@typescript-eslint/no-non-null-assertion: "off",
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class EditableContainer extends HTMLDivElement {
|
|
||||||
baseStyle: HTMLStyleElement;
|
|
||||||
baseRule?: CSSStyleRule;
|
|
||||||
imageStyle?: HTMLStyleElement;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const shadow = this.attachShadow({ mode: "open" });
|
|
||||||
|
|
||||||
if (document.documentElement.classList.contains("night-mode")) {
|
|
||||||
this.classList.add("night-mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootStyle = document.createElement("link");
|
|
||||||
rootStyle.setAttribute("rel", "stylesheet");
|
|
||||||
rootStyle.setAttribute("href", "./_anki/css/editable.css");
|
|
||||||
shadow.appendChild(rootStyle);
|
|
||||||
|
|
||||||
this.baseStyle = document.createElement("style");
|
|
||||||
this.baseStyle.setAttribute("rel", "stylesheet");
|
|
||||||
this.baseStyle.id = "baseStyle";
|
|
||||||
shadow.appendChild(this.baseStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback(): void {
|
|
||||||
const sheet = this.baseStyle.sheet as CSSStyleSheet;
|
|
||||||
const baseIndex = sheet.insertRule("anki-editable {}");
|
|
||||||
this.baseRule = sheet.cssRules[baseIndex] as CSSStyleRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(color: string): void {
|
|
||||||
this.setBaseColor(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBaseColor(color: string): void {
|
|
||||||
if (this.baseRule) {
|
|
||||||
this.baseRule.style.color = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
|
|
||||||
if (this.baseRule) {
|
|
||||||
this.baseRule.style.fontFamily = fontFamily;
|
|
||||||
this.baseRule.style.fontSize = fontSize;
|
|
||||||
this.baseRule.style.direction = direction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isRightToLeft(): boolean {
|
|
||||||
return this.baseRule!.style.direction === "rtl";
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,16 +7,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ImageHandle from "./ImageHandle.svelte";
|
import ImageHandle from "./ImageHandle.svelte";
|
||||||
|
import MathjaxHandle from "./MathjaxHandle.svelte";
|
||||||
|
|
||||||
import type { EditableContainer } from "./editable-container";
|
import type { EditableContainer } from "editable/editable-container";
|
||||||
import type { Editable } from "./editable";
|
import type { Editable } from "editable/editable";
|
||||||
import type { Codable } from "./codable";
|
import type { Codable } from "./codable";
|
||||||
|
|
||||||
import { updateActiveButtons } from "./toolbar";
|
import { updateActiveButtons } from "./toolbar";
|
||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "./lib";
|
||||||
import { onInput, onKey, onKeyUp } from "./input-handlers";
|
import { onInput, onKey, onKeyUp } from "./input-handlers";
|
||||||
import { onFocus, onBlur } from "./focus-handlers";
|
import { deferFocusDown, saveFieldIfFieldChanged } from "./focus-handlers";
|
||||||
import { nightModeKey } from "components/context-keys";
|
import { nightModeKey } from "components/context-keys";
|
||||||
|
import { decoratedComponents } from "editable/decorated";
|
||||||
|
|
||||||
function onCutOrCopy(): void {
|
function onCutOrCopy(): void {
|
||||||
bridgeCommand("cutOrCopy");
|
bridgeCommand("cutOrCopy");
|
||||||
@ -24,6 +26,7 @@ function onCutOrCopy(): void {
|
|||||||
|
|
||||||
export class EditingArea extends HTMLDivElement {
|
export class EditingArea extends HTMLDivElement {
|
||||||
imageHandle: Promise<ImageHandle>;
|
imageHandle: Promise<ImageHandle>;
|
||||||
|
mathjaxHandle: MathjaxHandle;
|
||||||
editableContainer: EditableContainer;
|
editableContainer: EditableContainer;
|
||||||
editable: Editable;
|
editable: Editable;
|
||||||
codable: Codable;
|
codable: Codable;
|
||||||
@ -36,11 +39,9 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
is: "anki-editable-container",
|
is: "anki-editable-container",
|
||||||
}) as EditableContainer;
|
}) as EditableContainer;
|
||||||
|
|
||||||
const imageStyle = document.createElement("style");
|
|
||||||
imageStyle.setAttribute("rel", "stylesheet");
|
|
||||||
imageStyle.id = "imageHandleStyle";
|
|
||||||
|
|
||||||
this.editable = document.createElement("anki-editable") as Editable;
|
this.editable = document.createElement("anki-editable") as Editable;
|
||||||
|
this.editableContainer.shadowRoot!.appendChild(this.editable);
|
||||||
|
this.appendChild(this.editableContainer);
|
||||||
|
|
||||||
const context = new Map();
|
const context = new Map();
|
||||||
context.set(
|
context.set(
|
||||||
@ -49,27 +50,32 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let imageHandleResolve: (value: ImageHandle) => void;
|
let imageHandleResolve: (value: ImageHandle) => void;
|
||||||
this.imageHandle = new Promise<ImageHandle>((resolve) => {
|
this.imageHandle = new Promise<ImageHandle>(
|
||||||
imageHandleResolve = resolve;
|
(resolve) => (imageHandleResolve = resolve)
|
||||||
});
|
);
|
||||||
|
|
||||||
imageStyle.addEventListener("load", () =>
|
this.editableContainer.imagePromise.then(() =>
|
||||||
imageHandleResolve(
|
imageHandleResolve(
|
||||||
new ImageHandle({
|
new ImageHandle({
|
||||||
target: this,
|
target: this,
|
||||||
anchor: this.editableContainer,
|
anchor: this.editableContainer,
|
||||||
props: {
|
props: {
|
||||||
container: this.editable,
|
container: this.editable,
|
||||||
sheet: imageStyle.sheet,
|
sheet: this.editableContainer.imageStyle.sheet,
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
} as any)
|
} as any)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.editableContainer.shadowRoot!.appendChild(imageStyle);
|
this.mathjaxHandle = new MathjaxHandle({
|
||||||
this.editableContainer.shadowRoot!.appendChild(this.editable);
|
target: this,
|
||||||
this.appendChild(this.editableContainer);
|
anchor: this.editableContainer,
|
||||||
|
props: {
|
||||||
|
container: this.editable,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
} as any);
|
||||||
|
|
||||||
this.codable = document.createElement("textarea", {
|
this.codable = document.createElement("textarea", {
|
||||||
is: "anki-codable",
|
is: "anki-codable",
|
||||||
@ -80,7 +86,7 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.onBlur = this.onBlur.bind(this);
|
this.onBlur = this.onBlur.bind(this);
|
||||||
this.onKey = this.onKey.bind(this);
|
this.onKey = this.onKey.bind(this);
|
||||||
this.onPaste = this.onPaste.bind(this);
|
this.onPaste = this.onPaste.bind(this);
|
||||||
this.showImageHandle = this.showImageHandle.bind(this);
|
this.showHandles = this.showHandles.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeInput(): Editable | Codable {
|
get activeInput(): Editable | Codable {
|
||||||
@ -92,11 +98,24 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set fieldHTML(content: string) {
|
set fieldHTML(content: string) {
|
||||||
this.imageHandle.then(() => (this.activeInput.fieldHTML = content));
|
this.imageHandle.then(() => {
|
||||||
|
let result = content;
|
||||||
|
|
||||||
|
for (const component of decoratedComponents) {
|
||||||
|
result = component.toUndecorated(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeInput.fieldHTML = result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get fieldHTML(): string {
|
get fieldHTML(): string {
|
||||||
return this.activeInput.fieldHTML;
|
let result = this.activeInput.fieldHTML;
|
||||||
|
for (const component of decoratedComponents) {
|
||||||
|
result = component.toStored(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
@ -109,7 +128,7 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.addEventListener("copy", onCutOrCopy);
|
this.addEventListener("copy", onCutOrCopy);
|
||||||
this.addEventListener("oncut", onCutOrCopy);
|
this.addEventListener("oncut", onCutOrCopy);
|
||||||
this.addEventListener("mouseup", updateActiveButtons);
|
this.addEventListener("mouseup", updateActiveButtons);
|
||||||
this.editable.addEventListener("click", this.showImageHandle);
|
this.editable.addEventListener("click", this.showHandles);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
@ -122,12 +141,14 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.removeEventListener("copy", onCutOrCopy);
|
this.removeEventListener("copy", onCutOrCopy);
|
||||||
this.removeEventListener("oncut", onCutOrCopy);
|
this.removeEventListener("oncut", onCutOrCopy);
|
||||||
this.removeEventListener("mouseup", updateActiveButtons);
|
this.removeEventListener("mouseup", updateActiveButtons);
|
||||||
this.editable.removeEventListener("click", this.showImageHandle);
|
this.editable.removeEventListener("click", this.showHandles);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(color: string, content: string): void {
|
initialize(color: string, content: string): void {
|
||||||
this.setBaseColor(color);
|
this.editableContainer.stylePromise.then(() => {
|
||||||
this.fieldHTML = content;
|
this.setBaseColor(color);
|
||||||
|
this.fieldHTML = content;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setBaseColor(color: string): void {
|
setBaseColor(color: string): void {
|
||||||
@ -179,13 +200,12 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.activeInput.surroundSelection(before, after);
|
this.activeInput.surroundSelection(before, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus(event: FocusEvent): void {
|
onFocus(): void {
|
||||||
onFocus(event);
|
deferFocusDown(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlur(event: FocusEvent): void {
|
onBlur(event: FocusEvent): void {
|
||||||
this.resetImageHandle();
|
saveFieldIfFieldChanged(this, event.relatedTarget as Element);
|
||||||
onBlur(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter(event: KeyboardEvent): void {
|
onEnter(event: KeyboardEvent): void {
|
||||||
@ -193,33 +213,49 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onKey(event: KeyboardEvent): void {
|
onKey(event: KeyboardEvent): void {
|
||||||
this.resetImageHandle();
|
this.resetHandles();
|
||||||
onKey(event);
|
onKey(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPaste(event: ClipboardEvent): void {
|
onPaste(event: ClipboardEvent): void {
|
||||||
this.resetImageHandle();
|
this.resetHandles();
|
||||||
this.activeInput.onPaste(event);
|
this.activeInput.onPaste(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetImageHandle(): void {
|
resetHandles(): Promise<void> {
|
||||||
this.imageHandle.then((imageHandle) =>
|
const promise = this.imageHandle.then((imageHandle) =>
|
||||||
(imageHandle as any).$set({
|
(imageHandle as any).$set({
|
||||||
activeImage: null,
|
activeImage: null,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
(this.mathjaxHandle as any).$set({
|
||||||
|
activeImage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
showImageHandle(event: MouseEvent): void {
|
async showHandles(event: MouseEvent): Promise<void> {
|
||||||
if (event.target instanceof HTMLImageElement) {
|
if (event.target instanceof HTMLImageElement) {
|
||||||
this.imageHandle.then((imageHandle) =>
|
const image = event.target as HTMLImageElement;
|
||||||
(imageHandle as any).$set({
|
await this.resetHandles();
|
||||||
activeImage: event.target,
|
|
||||||
|
if (!image.dataset.anki) {
|
||||||
|
await this.imageHandle.then((imageHandle) =>
|
||||||
|
(imageHandle as any).$set({
|
||||||
|
activeImage: image,
|
||||||
|
isRtl: this.isRightToLeft(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (image.dataset.anki === "mathjax") {
|
||||||
|
(this.mathjaxHandle as any).$set({
|
||||||
|
activeImage: image,
|
||||||
isRtl: this.isRightToLeft(),
|
isRtl: this.isRightToLeft(),
|
||||||
})
|
});
|
||||||
);
|
}
|
||||||
} else {
|
} else {
|
||||||
this.resetImageHandle();
|
await this.resetHandles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +266,7 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.fieldHTML = this.codable.teardown();
|
this.fieldHTML = this.codable.teardown();
|
||||||
this.editable.hidden = false;
|
this.editable.hidden = false;
|
||||||
} else {
|
} else {
|
||||||
this.resetImageHandle();
|
this.resetHandles();
|
||||||
this.editable.hidden = true;
|
this.editable.hidden = true;
|
||||||
this.codable.setup(this.editable.fieldHTML);
|
this.codable.setup(this.editable.fieldHTML);
|
||||||
}
|
}
|
||||||
@ -254,3 +290,5 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
blur();
|
blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
||||||
|
@ -10,7 +10,7 @@ export class EditorField extends HTMLDivElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.classList.add("editor-field");
|
this.className = "editorfield";
|
||||||
|
|
||||||
this.labelContainer = document.createElement("div", {
|
this.labelContainer = document.createElement("div", {
|
||||||
is: "anki-label-container",
|
is: "anki-label-container",
|
||||||
@ -65,3 +65,5 @@ export class EditorField extends HTMLDivElement {
|
|||||||
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
|
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
||||||
|
@ -3,6 +3,17 @@
|
|||||||
|
|
||||||
@use 'base';
|
@use 'base';
|
||||||
@use 'scrollbar';
|
@use 'scrollbar';
|
||||||
|
@use 'button-mixins';
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.nightMode {
|
.nightMode {
|
||||||
@include scrollbar.night-mode;
|
@include scrollbar.night-mode;
|
||||||
@ -10,13 +21,15 @@
|
|||||||
|
|
||||||
#fields {
|
#fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: hidden;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-x: hidden;
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-field {
|
.editorfield {
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|
||||||
@ -29,6 +42,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* editing-area */
|
||||||
.field {
|
.field {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@ -63,10 +77,6 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: var(--window-bg);
|
background-color: var(--window-bg);
|
||||||
|
|
||||||
&.is-inactive {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
}
|
}
|
||||||
|
@ -8,23 +8,34 @@
|
|||||||
import { fieldFocused } from "./toolbar";
|
import { fieldFocused } from "./toolbar";
|
||||||
import type { EditingArea } from "./editing-area";
|
import type { EditingArea } from "./editing-area";
|
||||||
|
|
||||||
import { saveField } from "./change-timer";
|
import { saveField } from "./saving";
|
||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "./lib";
|
||||||
import { getCurrentField } from "./helpers";
|
import { getCurrentField } from "./helpers";
|
||||||
|
|
||||||
export function onFocus(evt: FocusEvent): void {
|
export function deferFocusDown(editingArea: EditingArea): void {
|
||||||
const currentField = evt.currentTarget as EditingArea;
|
editingArea.focus();
|
||||||
currentField.focus();
|
editingArea.caretToEnd();
|
||||||
currentField.caretToEnd();
|
|
||||||
|
|
||||||
bridgeCommand(`focus:${currentField.ord}`);
|
if (editingArea.getSelection().anchorNode === null) {
|
||||||
|
// selection is not inside editable after focusing
|
||||||
|
editingArea.caretToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
bridgeCommand(`focus:${editingArea.ord}`);
|
||||||
fieldFocused.set(true);
|
fieldFocused.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onBlur(evt: FocusEvent): void {
|
export function saveFieldIfFieldChanged(
|
||||||
const previousFocus = evt.currentTarget as EditingArea;
|
editingArea: EditingArea,
|
||||||
const currentFieldUnchanged = previousFocus === getCurrentField();
|
focusTo: Element | null
|
||||||
|
): void {
|
||||||
|
const fieldChanged =
|
||||||
|
editingArea !== getCurrentField() && !editingArea.contains(focusTo);
|
||||||
|
|
||||||
saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
|
saveField(editingArea, fieldChanged ? "blur" : "key");
|
||||||
fieldFocused.set(false);
|
fieldFocused.set(false);
|
||||||
|
|
||||||
|
if (fieldChanged) {
|
||||||
|
editingArea.resetHandles();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,103 +1,12 @@
|
|||||||
// 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
|
||||||
|
|
||||||
/* eslint
|
|
||||||
@typescript-eslint/no-non-null-assertion: "off",
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { EditingArea } from "./editing-area";
|
import type { EditingArea } from "./editing-area";
|
||||||
|
|
||||||
export function getCurrentField(): EditingArea | null {
|
export function getCurrentField(): EditingArea | null {
|
||||||
return document.activeElement?.closest(".field") ?? null;
|
return document.activeElement?.closest(".field") ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nodeIsElement(node: Node): node is Element {
|
|
||||||
return node.nodeType === Node.ELEMENT_NODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
|
||||||
const BLOCK_TAGS = [
|
|
||||||
"ADDRESS",
|
|
||||||
"ARTICLE",
|
|
||||||
"ASIDE",
|
|
||||||
"BLOCKQUOTE",
|
|
||||||
"DETAILS",
|
|
||||||
"DIALOG",
|
|
||||||
"DD",
|
|
||||||
"DIV",
|
|
||||||
"DL",
|
|
||||||
"DT",
|
|
||||||
"FIELDSET",
|
|
||||||
"FIGCAPTION",
|
|
||||||
"FIGURE",
|
|
||||||
"FOOTER",
|
|
||||||
"FORM",
|
|
||||||
"H1",
|
|
||||||
"H2",
|
|
||||||
"H3",
|
|
||||||
"H4",
|
|
||||||
"H5",
|
|
||||||
"H6",
|
|
||||||
"HEADER",
|
|
||||||
"HGROUP",
|
|
||||||
"HR",
|
|
||||||
"LI",
|
|
||||||
"MAIN",
|
|
||||||
"NAV",
|
|
||||||
"OL",
|
|
||||||
"P",
|
|
||||||
"PRE",
|
|
||||||
"SECTION",
|
|
||||||
"TABLE",
|
|
||||||
"UL",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function elementIsBlock(element: Element): boolean {
|
|
||||||
return BLOCK_TAGS.includes(element.tagName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function caretToEnd(node: Node): void {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(node);
|
|
||||||
range.collapse(false);
|
|
||||||
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAnchorParent =
|
|
||||||
<T extends Element>(predicate: (element: Element) => element is T) =>
|
|
||||||
(currentField: DocumentOrShadowRoot): T | null => {
|
|
||||||
const anchor = currentField.getSelection()?.anchorNode;
|
|
||||||
|
|
||||||
if (!anchor) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let anchorParent: T | null = null;
|
|
||||||
let element = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
|
||||||
|
|
||||||
while (element) {
|
|
||||||
anchorParent = anchorParent || (predicate(element) ? element : null);
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return anchorParent;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isListItem = (element: Element): element is HTMLLIElement =>
|
|
||||||
window.getComputedStyle(element).display === "list-item";
|
|
||||||
const isParagraph = (element: Element): element is HTMLParamElement =>
|
|
||||||
element.tagName === "P";
|
|
||||||
const isBlockElement = (
|
|
||||||
element: Element
|
|
||||||
): element is HTMLLIElement & HTMLParamElement =>
|
|
||||||
isListItem(element) || isParagraph(element);
|
|
||||||
|
|
||||||
export const getListItem = getAnchorParent(isListItem);
|
|
||||||
export const getParagraph = getAnchorParent(isParagraph);
|
|
||||||
export const getBlockElement = getAnchorParent(isBlockElement);
|
|
||||||
|
|
||||||
export function appendInParentheses(text: string, appendix: string): string {
|
export function appendInParentheses(text: string, appendix: string): string {
|
||||||
return `${text} (${appendix})`;
|
return `${text} (${appendix})`;
|
||||||
}
|
}
|
||||||
|
@ -46,3 +46,6 @@ export { default as floatRightIcon } from "./format-float-right.svg";
|
|||||||
|
|
||||||
export { default as sizeActual } from "./image-size-select-actual.svg";
|
export { default as sizeActual } from "./image-size-select-actual.svg";
|
||||||
export { default as sizeMinimized } from "./image-size-select-large.svg";
|
export { default as sizeMinimized } from "./image-size-select-large.svg";
|
||||||
|
|
||||||
|
export { default as inlineIcon } from "./format-wrap-square.svg";
|
||||||
|
export { default as blockIcon } from "./format-wrap-top-bottom.svg";
|
||||||
|
@ -13,28 +13,30 @@ import type EditorToolbar from "./EditorToolbar.svelte";
|
|||||||
import type TagEditor from "./TagEditor.svelte";
|
import type TagEditor from "./TagEditor.svelte";
|
||||||
|
|
||||||
import { filterHTML } from "html-filter";
|
import { filterHTML } from "html-filter";
|
||||||
import { updateActiveButtons } from "./toolbar";
|
|
||||||
import { setupI18n, ModuleName } from "lib/i18n";
|
import { setupI18n, ModuleName } from "lib/i18n";
|
||||||
import { isApplePlatform } from "lib/platform";
|
import { isApplePlatform } from "lib/platform";
|
||||||
import { registerShortcut } from "lib/shortcuts";
|
import { registerShortcut } from "lib/shortcuts";
|
||||||
import { bridgeCommand } from "lib/bridgecommand";
|
import { bridgeCommand } from "lib/bridgecommand";
|
||||||
|
import { updateActiveButtons } from "./toolbar";
|
||||||
|
import { saveField } from "./saving";
|
||||||
|
|
||||||
import "./fields.css";
|
import "./fields.css";
|
||||||
|
|
||||||
import { saveField } from "./change-timer";
|
import "./label-container";
|
||||||
|
import "./codable";
|
||||||
import { EditorField } from "./editor-field";
|
import "./editor-field";
|
||||||
import { LabelContainer } from "./label-container";
|
import type { EditorField } from "./editor-field";
|
||||||
import { EditingArea } from "./editing-area";
|
import { EditingArea } from "./editing-area";
|
||||||
import { EditableContainer } from "./editable-container";
|
import "editable/editable-container";
|
||||||
import { Editable } from "./editable";
|
import "editable/editable";
|
||||||
import { Codable } from "./codable";
|
import "editable/mathjax-component";
|
||||||
|
|
||||||
import { initToolbar, fieldFocused } from "./toolbar";
|
import { initToolbar, fieldFocused } from "./toolbar";
|
||||||
import { initTagEditor } from "./tag-editor";
|
import { initTagEditor } from "./tag-editor";
|
||||||
import { getCurrentField } from "./helpers";
|
import { getCurrentField } from "./helpers";
|
||||||
|
|
||||||
export { setNoteId, getNoteId } from "./note-id";
|
export { setNoteId, getNoteId } from "./note-id";
|
||||||
export { saveNow } from "./change-timer";
|
export { saveNow } from "./saving";
|
||||||
export { wrap, wrapIntoText } from "./wrap";
|
export { wrap, wrapIntoText } from "./wrap";
|
||||||
export { editorToolbar } from "./toolbar";
|
export { editorToolbar } from "./toolbar";
|
||||||
export { activateStickyShortcuts } from "./label-container";
|
export { activateStickyShortcuts } from "./label-container";
|
||||||
@ -50,13 +52,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("anki-editable", Editable);
|
|
||||||
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
|
|
||||||
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
|
||||||
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
|
||||||
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
|
||||||
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
|
||||||
|
|
||||||
if (isApplePlatform()) {
|
if (isApplePlatform()) {
|
||||||
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
|
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
|
||||||
}
|
}
|
||||||
@ -157,11 +152,14 @@ export function setBackgrounds(cols: ("dupe" | "")[]): void {
|
|||||||
);
|
);
|
||||||
document
|
document
|
||||||
.getElementById("dupes")!
|
.getElementById("dupes")!
|
||||||
.classList.toggle("is-inactive", !cols.includes("dupe"));
|
.classList.toggle("d-none", !cols.includes("dupe"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setClozeHint(cloze_hint: string): void {
|
export function setClozeHint(hint: string): void {
|
||||||
document.getElementById("cloze-hint")!.innerHTML = cloze_hint;
|
const clozeHint = document.getElementById("cloze-hint")!;
|
||||||
|
|
||||||
|
clozeHint.innerHTML = hint;
|
||||||
|
clozeHint.classList.toggle("d-none", hint.length === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setFonts(fonts: [string, number, boolean][]): void {
|
export function setFonts(fonts: [string, number, boolean][]): void {
|
||||||
|
@ -5,10 +5,10 @@
|
|||||||
@typescript-eslint/no-non-null-assertion: "off",
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { nodeIsElement } from "lib/dom";
|
||||||
import { updateActiveButtons } from "./toolbar";
|
import { updateActiveButtons } from "./toolbar";
|
||||||
import { EditingArea } from "./editing-area";
|
import { EditingArea } from "./editing-area";
|
||||||
import { nodeIsElement } from "./helpers";
|
import { triggerChangeTimer } from "./saving";
|
||||||
import { triggerChangeTimer } from "./change-timer";
|
|
||||||
import { registerShortcut } from "lib/shortcuts";
|
import { registerShortcut } from "lib/shortcuts";
|
||||||
|
|
||||||
export function onInput(event: Event): void {
|
export function onInput(event: Event): void {
|
||||||
|
@ -7,7 +7,7 @@ import * as tr from "lib/i18n";
|
|||||||
import { registerShortcut } from "lib/shortcuts";
|
import { registerShortcut } from "lib/shortcuts";
|
||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "./lib";
|
||||||
import { appendInParentheses } from "./helpers";
|
import { appendInParentheses } from "./helpers";
|
||||||
import { saveField } from "./change-timer";
|
import { saveField } from "./saving";
|
||||||
import { getCurrentField, forEditorField, i18n } from ".";
|
import { getCurrentField, forEditorField, i18n } from ".";
|
||||||
import pinIcon from "./pin-angle.svg";
|
import pinIcon from "./pin-angle.svg";
|
||||||
|
|
||||||
@ -127,3 +127,5 @@ export class LabelContainer extends HTMLDivElement {
|
|||||||
this.toggleSticky();
|
this.toggleSticky();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
||||||
|
40
ts/editor/saving.ts
Normal file
40
ts/editor/saving.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import type { EditingArea } from "./editing-area";
|
||||||
|
|
||||||
|
import { ChangeTimer } from "./change-timer";
|
||||||
|
import { getCurrentField } from "./helpers";
|
||||||
|
import { bridgeCommand } from "./lib";
|
||||||
|
import { getNoteId } from "./note-id";
|
||||||
|
|
||||||
|
const saveFieldTimer = new ChangeTimer();
|
||||||
|
|
||||||
|
export function triggerChangeTimer(currentField: EditingArea): void {
|
||||||
|
saveFieldTimer.schedule(() => saveField(currentField, "key"), 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
|
||||||
|
saveFieldTimer.clear();
|
||||||
|
const command = `${type}:${currentField.ord}:${getNoteId()}:${
|
||||||
|
currentField.fieldHTML
|
||||||
|
}`;
|
||||||
|
bridgeCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveNow(keepFocus: boolean): void {
|
||||||
|
const currentField = getCurrentField();
|
||||||
|
|
||||||
|
if (!currentField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFieldTimer.clear();
|
||||||
|
|
||||||
|
if (keepFocus) {
|
||||||
|
saveField(currentField, "key");
|
||||||
|
} else {
|
||||||
|
// triggers onBlur, which saves
|
||||||
|
currentField.blur();
|
||||||
|
}
|
||||||
|
}
|
@ -5,45 +5,15 @@
|
|||||||
@typescript-eslint/no-non-null-assertion: "off",
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentField } from "./helpers";
|
import { wrapInternal } from "lib/wrap";
|
||||||
import { setFormat } from ".";
|
import { getCurrentField } from ".";
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
|
||||||
const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
|
|
||||||
return match[1] + front + match[2] + back + match[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveCursorPastPostfix(selection: Selection, postfix: string): void {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
range.setStart(range.startContainer, range.startOffset - postfix.length);
|
|
||||||
range.collapse(true);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapInternal(front: string, back: string, plainText: boolean): void {
|
|
||||||
const currentField = getCurrentField()!;
|
|
||||||
const selection = currentField.getSelection();
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const content = range.cloneContents();
|
|
||||||
const span = document.createElement("span");
|
|
||||||
span.appendChild(content);
|
|
||||||
|
|
||||||
if (plainText) {
|
|
||||||
const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
|
|
||||||
setFormat("inserttext", new_);
|
|
||||||
} else {
|
|
||||||
const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
|
|
||||||
setFormat("inserthtml", new_);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!span.innerHTML) {
|
|
||||||
moveCursorPastPostfix(selection, back);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrap(front: string, back: string): void {
|
export function wrap(front: string, back: string): void {
|
||||||
wrapInternal(front, back, false);
|
const editingArea = getCurrentField();
|
||||||
|
|
||||||
|
if (editingArea) {
|
||||||
|
wrapInternal(editingArea.editableContainer.shadowRoot!, front, back, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapCurrent(front: string, back: string): void {
|
export function wrapCurrent(front: string, back: string): void {
|
||||||
@ -53,5 +23,9 @@ export function wrapCurrent(front: string, back: string): void {
|
|||||||
|
|
||||||
/* currently unused */
|
/* currently unused */
|
||||||
export function wrapIntoText(front: string, back: string): void {
|
export function wrapIntoText(front: string, back: string): void {
|
||||||
wrapInternal(front, back, true);
|
const editingArea = getCurrentField();
|
||||||
|
|
||||||
|
if (editingArea) {
|
||||||
|
wrapInternal(editingArea.editableContainer.shadowRoot!, front, back, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
93
ts/lib/dom.ts
Normal file
93
ts/lib/dom.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function nodeIsElement(node: Node): node is Element {
|
||||||
|
return node.nodeType === Node.ELEMENT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
||||||
|
const BLOCK_TAGS = [
|
||||||
|
"ADDRESS",
|
||||||
|
"ARTICLE",
|
||||||
|
"ASIDE",
|
||||||
|
"BLOCKQUOTE",
|
||||||
|
"DETAILS",
|
||||||
|
"DIALOG",
|
||||||
|
"DD",
|
||||||
|
"DIV",
|
||||||
|
"DL",
|
||||||
|
"DT",
|
||||||
|
"FIELDSET",
|
||||||
|
"FIGCAPTION",
|
||||||
|
"FIGURE",
|
||||||
|
"FOOTER",
|
||||||
|
"FORM",
|
||||||
|
"H1",
|
||||||
|
"H2",
|
||||||
|
"H3",
|
||||||
|
"H4",
|
||||||
|
"H5",
|
||||||
|
"H6",
|
||||||
|
"HEADER",
|
||||||
|
"HGROUP",
|
||||||
|
"HR",
|
||||||
|
"LI",
|
||||||
|
"MAIN",
|
||||||
|
"NAV",
|
||||||
|
"OL",
|
||||||
|
"P",
|
||||||
|
"PRE",
|
||||||
|
"SECTION",
|
||||||
|
"TABLE",
|
||||||
|
"UL",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function elementIsBlock(element: Element): boolean {
|
||||||
|
return BLOCK_TAGS.includes(element.tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function caretToEnd(node: Node): void {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAnchorParent =
|
||||||
|
<T extends Element>(predicate: (element: Element) => element is T) =>
|
||||||
|
(currentField: DocumentOrShadowRoot): T | null => {
|
||||||
|
const anchor = currentField.getSelection()?.anchorNode;
|
||||||
|
|
||||||
|
if (!anchor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let anchorParent: T | null = null;
|
||||||
|
let element = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
||||||
|
|
||||||
|
while (element) {
|
||||||
|
anchorParent = anchorParent || (predicate(element) ? element : null);
|
||||||
|
element = element.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return anchorParent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isListItem = (element: Element): element is HTMLLIElement =>
|
||||||
|
window.getComputedStyle(element).display === "list-item";
|
||||||
|
const isParagraph = (element: Element): element is HTMLParamElement =>
|
||||||
|
element.tagName === "P";
|
||||||
|
const isBlockElement = (
|
||||||
|
element: Element
|
||||||
|
): element is HTMLLIElement & HTMLParamElement =>
|
||||||
|
isListItem(element) || isParagraph(element);
|
||||||
|
|
||||||
|
export const getListItem = getAnchorParent(isListItem);
|
||||||
|
export const getParagraph = getAnchorParent(isParagraph);
|
||||||
|
export const getBlockElement = getAnchorParent(isBlockElement);
|
44
ts/lib/wrap.ts
Normal file
44
ts/lib/wrap.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
|
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
||||||
|
const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
|
||||||
|
return match[1] + front + match[2] + back + match[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCursorPastPostfix(selection: Selection, postfix: string): void {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
range.setStart(range.startContainer, range.startOffset - postfix.length);
|
||||||
|
range.collapse(true);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapInternal(
|
||||||
|
root: Document | ShadowRoot,
|
||||||
|
front: string,
|
||||||
|
back: string,
|
||||||
|
plainText: boolean
|
||||||
|
): void {
|
||||||
|
const selection = root.getSelection()!;
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const content = range.cloneContents();
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.appendChild(content);
|
||||||
|
|
||||||
|
if (plainText) {
|
||||||
|
const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
|
||||||
|
document.execCommand("inserttext", false, new_);
|
||||||
|
} else {
|
||||||
|
const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
|
||||||
|
document.execCommand("inserthtml", false, new_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!span.innerHTML) {
|
||||||
|
moveCursorPastPostfix(selection, back);
|
||||||
|
}
|
||||||
|
}
|
@ -164,6 +164,14 @@
|
|||||||
"path": "node_modules/commander",
|
"path": "node_modules/commander",
|
||||||
"licenseFile": "node_modules/commander/LICENSE"
|
"licenseFile": "node_modules/commander/LICENSE"
|
||||||
},
|
},
|
||||||
|
"commander@8.1.0": {
|
||||||
|
"licenses": "MIT",
|
||||||
|
"repository": "https://github.com/tj/commander.js",
|
||||||
|
"publisher": "TJ Holowaychuk",
|
||||||
|
"email": "tj@vision-media.ca",
|
||||||
|
"path": "node_modules/speech-rule-engine/node_modules/commander",
|
||||||
|
"licenseFile": "node_modules/speech-rule-engine/node_modules/commander/LICENSE"
|
||||||
|
},
|
||||||
"css-browser-selector@0.6.5": {
|
"css-browser-selector@0.6.5": {
|
||||||
"licenses": "CC-BY-SA-2.5",
|
"licenses": "CC-BY-SA-2.5",
|
||||||
"repository": "https://github.com/verbatim/css_browser_selector",
|
"repository": "https://github.com/verbatim/css_browser_selector",
|
||||||
@ -426,6 +434,14 @@
|
|||||||
"path": "node_modules/delaunator",
|
"path": "node_modules/delaunator",
|
||||||
"licenseFile": "node_modules/delaunator/LICENSE"
|
"licenseFile": "node_modules/delaunator/LICENSE"
|
||||||
},
|
},
|
||||||
|
"esm@3.2.25": {
|
||||||
|
"licenses": "MIT",
|
||||||
|
"repository": "https://github.com/standard-things/esm",
|
||||||
|
"publisher": "John-David Dalton",
|
||||||
|
"email": "john.david.dalton@gmail.com",
|
||||||
|
"path": "node_modules/esm",
|
||||||
|
"licenseFile": "node_modules/esm/LICENSE"
|
||||||
|
},
|
||||||
"iconv-lite@0.6.3": {
|
"iconv-lite@0.6.3": {
|
||||||
"licenses": "MIT",
|
"licenses": "MIT",
|
||||||
"repository": "https://github.com/ashtuchkin/iconv-lite",
|
"repository": "https://github.com/ashtuchkin/iconv-lite",
|
||||||
@ -489,12 +505,31 @@
|
|||||||
"path": "node_modules/marked",
|
"path": "node_modules/marked",
|
||||||
"licenseFile": "node_modules/marked/LICENSE.md"
|
"licenseFile": "node_modules/marked/LICENSE.md"
|
||||||
},
|
},
|
||||||
|
"mathjax-full@3.2.0": {
|
||||||
|
"licenses": "Apache-2.0",
|
||||||
|
"repository": "https://github.com/mathjax/Mathjax-src",
|
||||||
|
"path": "node_modules/mathjax-full",
|
||||||
|
"licenseFile": "node_modules/mathjax-full/LICENSE"
|
||||||
|
},
|
||||||
"mathjax@3.1.4": {
|
"mathjax@3.1.4": {
|
||||||
"licenses": "Apache-2.0",
|
"licenses": "Apache-2.0",
|
||||||
"repository": "https://github.com/mathjax/MathJax",
|
"repository": "https://github.com/mathjax/MathJax",
|
||||||
"path": "node_modules/mathjax",
|
"path": "node_modules/mathjax",
|
||||||
"licenseFile": "node_modules/mathjax/LICENSE"
|
"licenseFile": "node_modules/mathjax/LICENSE"
|
||||||
},
|
},
|
||||||
|
"mhchemparser@4.1.1": {
|
||||||
|
"licenses": "Apache-2.0",
|
||||||
|
"repository": "https://github.com/mhchem/mhchemParser",
|
||||||
|
"publisher": "Martin Hensel",
|
||||||
|
"path": "node_modules/mhchemparser",
|
||||||
|
"licenseFile": "node_modules/mhchemparser/LICENSE.txt"
|
||||||
|
},
|
||||||
|
"mj-context-menu@0.6.1": {
|
||||||
|
"licenses": "Apache-2.0",
|
||||||
|
"repository": "https://github.com/zorkow/context-menu",
|
||||||
|
"path": "node_modules/mj-context-menu",
|
||||||
|
"licenseFile": "node_modules/mj-context-menu/README.md"
|
||||||
|
},
|
||||||
"protobufjs@6.11.2": {
|
"protobufjs@6.11.2": {
|
||||||
"licenses": "BSD-3-Clause",
|
"licenses": "BSD-3-Clause",
|
||||||
"repository": "https://github.com/protobufjs/protobuf.js",
|
"repository": "https://github.com/protobufjs/protobuf.js",
|
||||||
@ -526,6 +561,28 @@
|
|||||||
"url": "https://github.com/ChALkeR",
|
"url": "https://github.com/ChALkeR",
|
||||||
"path": "node_modules/safer-buffer",
|
"path": "node_modules/safer-buffer",
|
||||||
"licenseFile": "node_modules/safer-buffer/LICENSE"
|
"licenseFile": "node_modules/safer-buffer/LICENSE"
|
||||||
|
},
|
||||||
|
"speech-rule-engine@3.3.3": {
|
||||||
|
"licenses": "Apache-2.0",
|
||||||
|
"repository": "https://github.com/zorkow/speech-rule-engine",
|
||||||
|
"path": "node_modules/speech-rule-engine",
|
||||||
|
"licenseFile": "node_modules/speech-rule-engine/LICENSE"
|
||||||
|
},
|
||||||
|
"wicked-good-xpath@1.3.0": {
|
||||||
|
"licenses": "MIT",
|
||||||
|
"repository": "https://github.com/google/wicked-good-xpath",
|
||||||
|
"publisher": "Google Inc.",
|
||||||
|
"path": "node_modules/wicked-good-xpath",
|
||||||
|
"licenseFile": "node_modules/wicked-good-xpath/LICENSE"
|
||||||
|
},
|
||||||
|
"xmldom-sre@0.1.31": {
|
||||||
|
"licenses": "MIT*",
|
||||||
|
"repository": "https://github.com/zorkow/xmldom",
|
||||||
|
"publisher": "jindw",
|
||||||
|
"email": "jindw@xidea.org",
|
||||||
|
"url": "http://www.xidea.org",
|
||||||
|
"path": "node_modules/xmldom-sre",
|
||||||
|
"licenseFile": "node_modules/xmldom-sre/LICENSE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "=2.0.5",
|
"marked": "=2.0.5",
|
||||||
"mathjax": "^3.1.2",
|
"mathjax": "^3.1.2",
|
||||||
|
"mathjax-full": "^3.2.0",
|
||||||
"protobufjs": "^6.10.2"
|
"protobufjs": "^6.10.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -112,7 +112,7 @@ $btn-base-color-night: #666;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be similar to -webkit-focus-ring-color
|
// should be similar to -webkit-focus-ring-color
|
||||||
$focus-color: $blue;
|
$focus-color: rgba(21 97 174);
|
||||||
|
|
||||||
@mixin impressed-shadow($intensity) {
|
@mixin impressed-shadow($intensity) {
|
||||||
box-shadow: inset 0 calc(var(--buttons-size) / 15) calc(var(--buttons-size) / 5)
|
box-shadow: inset 0 calc(var(--buttons-size) / 15) calc(var(--buttons-size) / 5)
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
],
|
],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"editable/*": ["../bazel-bin/ts/editable/*"],
|
||||||
"lib/*": ["../bazel-bin/ts/lib/*"],
|
"lib/*": ["../bazel-bin/ts/lib/*"],
|
||||||
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
|
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
|
||||||
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
|
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
|
||||||
|
49
ts/yarn.lock
49
ts/yarn.lock
@ -1596,6 +1596,11 @@ commander@7:
|
|||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||||
|
|
||||||
|
commander@>=7.0.0:
|
||||||
|
version "8.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-8.1.0.tgz#db36e3e66edf24ff591d639862c6ab2c52664362"
|
||||||
|
integrity sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
@ -2154,6 +2159,11 @@ eslint@^7.24.0:
|
|||||||
text-table "^0.2.0"
|
text-table "^0.2.0"
|
||||||
v8-compile-cache "^2.0.3"
|
v8-compile-cache "^2.0.3"
|
||||||
|
|
||||||
|
esm@^3.2.25:
|
||||||
|
version "3.2.25"
|
||||||
|
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
|
||||||
|
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
|
||||||
|
|
||||||
espree@^7.3.0, espree@^7.3.1:
|
espree@^7.3.0, espree@^7.3.1:
|
||||||
version "7.3.1"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
|
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
|
||||||
@ -3356,6 +3366,16 @@ marked@=2.0.5, marked@^2.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.5.tgz#2d15c759b9497b0e7b5b57f4c2edabe1002ef9e7"
|
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.5.tgz#2d15c759b9497b0e7b5b57f4c2edabe1002ef9e7"
|
||||||
integrity sha512-yfCEUXmKhBPLOzEC7c+tc4XZdIeTdGoRCZakFMkCxodr7wDXqoapIME4wjcpBPJLNyUnKJ3e8rb8wlAgnLnaDw==
|
integrity sha512-yfCEUXmKhBPLOzEC7c+tc4XZdIeTdGoRCZakFMkCxodr7wDXqoapIME4wjcpBPJLNyUnKJ3e8rb8wlAgnLnaDw==
|
||||||
|
|
||||||
|
mathjax-full@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mathjax-full/-/mathjax-full-3.2.0.tgz#e53269842a943d4df10502937518991268996c5c"
|
||||||
|
integrity sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==
|
||||||
|
dependencies:
|
||||||
|
esm "^3.2.25"
|
||||||
|
mhchemparser "^4.1.0"
|
||||||
|
mj-context-menu "^0.6.1"
|
||||||
|
speech-rule-engine "^3.3.3"
|
||||||
|
|
||||||
mathjax@^3.1.2:
|
mathjax@^3.1.2:
|
||||||
version "3.1.4"
|
version "3.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.1.4.tgz#4e8932d12845c0abae8b7f1976ea98cb505e8420"
|
resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.1.4.tgz#4e8932d12845c0abae8b7f1976ea98cb505e8420"
|
||||||
@ -3376,6 +3396,11 @@ merge2@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||||
|
|
||||||
|
mhchemparser@^4.1.0:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.1.1.tgz#a2142fdab37a02ec8d1b48a445059287790becd5"
|
||||||
|
integrity sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA==
|
||||||
|
|
||||||
micromatch@^4.0.2, micromatch@^4.0.4:
|
micromatch@^4.0.2, micromatch@^4.0.4:
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
|
||||||
@ -3418,6 +3443,11 @@ minimist@^1.2.0, minimist@^1.2.5:
|
|||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
|
mj-context-menu@^0.6.1:
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz#a043c5282bf7e1cf3821de07b13525ca6f85aa69"
|
||||||
|
integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==
|
||||||
|
|
||||||
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
@ -4094,6 +4124,15 @@ spdx-satisfies@^5.0.0:
|
|||||||
spdx-expression-parse "^3.0.0"
|
spdx-expression-parse "^3.0.0"
|
||||||
spdx-ranges "^2.0.0"
|
spdx-ranges "^2.0.0"
|
||||||
|
|
||||||
|
speech-rule-engine@^3.3.3:
|
||||||
|
version "3.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz#781ed03cbcf3279f94d1d80241025ea954c6d571"
|
||||||
|
integrity sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==
|
||||||
|
dependencies:
|
||||||
|
commander ">=7.0.0"
|
||||||
|
wicked-good-xpath "^1.3.0"
|
||||||
|
xmldom-sre "^0.1.31"
|
||||||
|
|
||||||
sprintf-js@~1.0.2:
|
sprintf-js@~1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
@ -4505,6 +4544,11 @@ which@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
|
wicked-good-xpath@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c"
|
||||||
|
integrity sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w=
|
||||||
|
|
||||||
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||||
@ -4554,6 +4598,11 @@ xmlcreate@^2.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.3.tgz#df9ecd518fd3890ab3548e1b811d040614993497"
|
resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.3.tgz#df9ecd518fd3890ab3548e1b811d040614993497"
|
||||||
integrity sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==
|
integrity sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==
|
||||||
|
|
||||||
|
xmldom-sre@^0.1.31:
|
||||||
|
version "0.1.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
|
||||||
|
integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
|
Loading…
Reference in New Issue
Block a user