mirror of
https://github.com/ankitects/anki.git
synced 2025-09-20 23:12:21 -04:00
Formalize "Decorated components API"
This commit is contained in:
parent
9fb0ce973b
commit
6a1fae53df
4 changed files with 107 additions and 53 deletions
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();
|
|
@ -1,6 +1,8 @@
|
||||||
// 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 { DecoratedElement } from "./decorated";
|
||||||
|
import { decoratedComponents } from "./decorated";
|
||||||
import { bridgeCommand } from "lib/bridgecommand";
|
import { bridgeCommand } from "lib/bridgecommand";
|
||||||
import { elementIsBlock, getBlockElement } from "lib/dom";
|
import { elementIsBlock, getBlockElement } from "lib/dom";
|
||||||
// import { inCodable } from "./toolbar";
|
// import { inCodable } from "./toolbar";
|
||||||
|
@ -25,13 +27,6 @@ function containsInlineContent(element: Element): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DecoratedElement extends HTMLElement {
|
|
||||||
decorate(): void;
|
|
||||||
undecorate(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoratedTags = ["anki-mathjax"];
|
|
||||||
|
|
||||||
export class Editable extends HTMLElement {
|
export class Editable extends HTMLElement {
|
||||||
set fieldHTML(content: string) {
|
set fieldHTML(content: string) {
|
||||||
this.innerHTML = content;
|
this.innerHTML = content;
|
||||||
|
@ -44,8 +39,8 @@ export class Editable extends HTMLElement {
|
||||||
get fieldHTML(): string {
|
get fieldHTML(): string {
|
||||||
const clone = this.cloneNode(true) as Element;
|
const clone = this.cloneNode(true) as Element;
|
||||||
|
|
||||||
for (const component of decoratedTags) {
|
for (const component of decoratedComponents) {
|
||||||
for (const element of clone.getElementsByTagName(component)) {
|
for (const element of clone.getElementsByTagName(component.tagName)) {
|
||||||
(element as DecoratedElement).undecorate();
|
(element as DecoratedElement).undecorate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import "mathjax/es5/tex-svg-full";
|
import "mathjax/es5/tex-svg-full";
|
||||||
|
|
||||||
|
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
|
||||||
|
import { decoratedComponents } from "./decorated";
|
||||||
import { nodeIsElement } from "lib/dom";
|
import { nodeIsElement } from "lib/dom";
|
||||||
|
|
||||||
import Mathjax_svelte from "./Mathjax.svelte";
|
import Mathjax_svelte from "./Mathjax.svelte";
|
||||||
|
@ -37,9 +40,44 @@ function moveNodesInsertedBeforeEndToAfterEnd(element: Element): () => void {
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
class Mathjax extends HTMLElement {
|
const mathjaxTagPattern =
|
||||||
block: boolean = false;
|
/<anki-mathjax(?:[^>]*?block="(.*?)")?[^>]*?>(.*?)<\/anki-mathjax>/gsu;
|
||||||
disconnect: () => void = () => {};
|
|
||||||
|
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 */};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -82,40 +120,6 @@ class Mathjax extends HTMLElement {
|
||||||
this.removeAttribute("block");
|
this.removeAttribute("block");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
customElements.define("anki-mathjax", Mathjax);
|
decoratedComponents.push(Mathjax);
|
||||||
|
|
||||||
const mathjaxTagPattern =
|
|
||||||
/<anki-mathjax(?:[^>]*?block="(.*?)")?[^>]*?>(.*?)<\/anki-mathjax>/gsu;
|
|
||||||
|
|
||||||
export function toMathjaxDelimiters(html: string): string {
|
|
||||||
return html.replace(
|
|
||||||
mathjaxTagPattern,
|
|
||||||
(_match: string, block: string | undefined, text: string) => {
|
|
||||||
console.log("delim", _match, block, "text", text);
|
|
||||||
return typeof block === "string" && block !== "false"
|
|
||||||
? `\\[${text}\\]`
|
|
||||||
: `\\(${text}\\)`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mathjaxBlockDelimiterPattern = /\\\[(.*?)\\\]/gsu;
|
|
||||||
const mathjaxInlineDelimiterPattern = /\\\((.*?)\\\)/gsu;
|
|
||||||
|
|
||||||
export function toMathjaxTags(html: string): string {
|
|
||||||
return html
|
|
||||||
.replace(
|
|
||||||
mathjaxBlockDelimiterPattern,
|
|
||||||
(_match: string, text: string) => (
|
|
||||||
console.log(text), `<anki-mathjax block="true">${text}</anki-mathjax>`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
mathjaxInlineDelimiterPattern,
|
|
||||||
(_match: string, text: string) => (
|
|
||||||
console.log(text), `<anki-mathjax>${text}</anki-mathjax>`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ 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 { onFocus, onBlur } from "./focus-handlers";
|
||||||
import { nightModeKey } from "components/context-keys";
|
import { nightModeKey } from "components/context-keys";
|
||||||
import { toMathjaxTags, toMathjaxDelimiters } from "editable/mathjax-component";
|
import { decoratedComponents } from "editable/decorated";
|
||||||
|
|
||||||
function onCutOrCopy(): void {
|
function onCutOrCopy(): void {
|
||||||
bridgeCommand("cutOrCopy");
|
bridgeCommand("cutOrCopy");
|
||||||
|
@ -93,13 +93,23 @@ export class EditingArea extends HTMLDivElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
set fieldHTML(content: string) {
|
set fieldHTML(content: string) {
|
||||||
this.imageHandle.then(
|
this.imageHandle.then(() => {
|
||||||
() => (this.activeInput.fieldHTML = toMathjaxTags(content))
|
let result = content;
|
||||||
);
|
for (const component of decoratedComponents) {
|
||||||
|
result = component.toUndecorated(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeInput.fieldHTML = result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get fieldHTML(): string {
|
get fieldHTML(): string {
|
||||||
return toMathjaxDelimiters(this.activeInput.fieldHTML);
|
let result = this.activeInput.fieldHTML;
|
||||||
|
for (const component of decoratedComponents) {
|
||||||
|
result = component.toStored(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
|
|
Loading…
Reference in a new issue