anki/ts/editable/frame-handle.ts

309 lines
8.9 KiB
TypeScript
Raw Normal View History

Fix some Mathjax issues (#1547) * Move move-nodes logic into domlib Refactor input-manager Refactor out FocusTrap from EditingArea Remove unnecessary selecting of node from surround Add onInput interface to input-manager Create MathjaxElement.svelte - This should contain all the setup necessary for displaying <anki-mathjax> elements in the rich text input - Does not contain setup necessary for Mathjax Overlay Deal with backwards deletion, when caret inside anki-mathjax Set mathjax elements contenteditable=false Do not undecorate mathjaxx element on disconnect - Fixes issues, where Mathjax might undecorate when it is moved into a different div Add framed element custom element Introduce iterateActions to allow global hooks for RichTextInput Remove some old code Deal with deletion of frame handles Make Anki frame and frame handles restore each other Make FrameElement restore its structure upon modification Frame and strip off framing from MathjaxElement automatically Move FrameHandle to separate FrameStart/FrameEnd Refactor FrameHandle Set data-frames on FrameElement Fix logic error connected to FrameElement Communicate frameHandle move{in,out} to anki-mathjax Clear selection when blurring ContentEditable Make sure frame is destroyed when undecorating Mathjax Use Hairline space instead of zeroWidth - it has better behavior in the contenteditable when placing the caret via clicking Allow detection of block elements with `block` attribute - This way, anki-mathjax block="true" will make field a field be recognized to have block content Make block frame element operater without handles - Clicking on the left/right side of a block mathjax will insert a br element to that side When deleting, remove mathajax-element not just image Update MathjaxButtons to correctly show block state SelectAll when moving into inline anki mathjax Remove CodeMirror autofocus functionality Move it to Mathjaxeditor directly Fix getRangeAt throwing error Update older code to use cross-browser Fix issue with FrameHandles not being properyly removed Satisfy formatting Use === instead of node.isSameNode() Fix issue of focusTrap not initialized * Fix after rebasing * Fix focus not being moved to first field * Add documentation for input-manager and iterate-actions * Export logic of ContentEditable to content-editable * Fix issue with inserting newline right next to inline Mathjax * Fix reframing issue of Mathjax Svelte component * Allow for deletion of Inline Mathjax again * Rename iterate-actions to action-list * Add copyright header * Split off frame-handle from frame-element * Add some comments for framing process * Add mising return types
2022-01-08 02:46:01 +01:00
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { moveChildOutOfElement } from "../domlib/move-nodes";
import { placeCaretAfter } from "../domlib/place-caret";
import { getSelection, isSelectionCollapsed } from "../lib/cross-browser";
import { elementIsEmpty, nodeIsElement, nodeIsText } from "../lib/dom";
import { on } from "../lib/events";
Fix some Mathjax issues (#1547) * Move move-nodes logic into domlib Refactor input-manager Refactor out FocusTrap from EditingArea Remove unnecessary selecting of node from surround Add onInput interface to input-manager Create MathjaxElement.svelte - This should contain all the setup necessary for displaying <anki-mathjax> elements in the rich text input - Does not contain setup necessary for Mathjax Overlay Deal with backwards deletion, when caret inside anki-mathjax Set mathjax elements contenteditable=false Do not undecorate mathjaxx element on disconnect - Fixes issues, where Mathjax might undecorate when it is moved into a different div Add framed element custom element Introduce iterateActions to allow global hooks for RichTextInput Remove some old code Deal with deletion of frame handles Make Anki frame and frame handles restore each other Make FrameElement restore its structure upon modification Frame and strip off framing from MathjaxElement automatically Move FrameHandle to separate FrameStart/FrameEnd Refactor FrameHandle Set data-frames on FrameElement Fix logic error connected to FrameElement Communicate frameHandle move{in,out} to anki-mathjax Clear selection when blurring ContentEditable Make sure frame is destroyed when undecorating Mathjax Use Hairline space instead of zeroWidth - it has better behavior in the contenteditable when placing the caret via clicking Allow detection of block elements with `block` attribute - This way, anki-mathjax block="true" will make field a field be recognized to have block content Make block frame element operater without handles - Clicking on the left/right side of a block mathjax will insert a br element to that side When deleting, remove mathajax-element not just image Update MathjaxButtons to correctly show block state SelectAll when moving into inline anki mathjax Remove CodeMirror autofocus functionality Move it to Mathjaxeditor directly Fix getRangeAt throwing error Update older code to use cross-browser Fix issue with FrameHandles not being properyly removed Satisfy formatting Use === instead of node.isSameNode() Fix issue of focusTrap not initialized * Fix after rebasing * Fix focus not being moved to first field * Add documentation for input-manager and iterate-actions * Export logic of ContentEditable to content-editable * Fix issue with inserting newline right next to inline Mathjax * Fix reframing issue of Mathjax Svelte component * Allow for deletion of Inline Mathjax again * Rename iterate-actions to action-list * Add copyright header * Split off frame-handle from frame-element * Add some comments for framing process * Add mising return types
2022-01-08 02:46:01 +01:00
import type { FrameElement } from "./frame-element";
/**
* The frame handle also needs some awareness that it's hosted below
* the frame
*/
export const frameElementTagName = "anki-frame";
/**
* I originally used a zero width space, however, in contentEditable, if
* a line ends in a zero width space, and you click _after_ the line,
* the caret will be placed _before_ the zero width space.
* Instead I use a hairline space.
*/
const spaceCharacter = "\u200a";
const spaceRegex = /[\u200a]/g;
export function isFrameHandle(node: unknown): node is FrameHandle {
return node instanceof FrameHandle;
}
function skippableNode(handleElement: FrameHandle, node: Node): boolean {
/**
* We only want to move nodes, which are direct descendants of the FrameHandle
* MutationRecords however might include nodes which were directly removed again
*/
return (
(nodeIsText(node) &&
(node.data === spaceCharacter || node.data.length === 0)) ||
!Array.prototype.includes.call(handleElement.childNodes, node)
);
}
function restoreHandleContent(mutations: MutationRecord[]): void {
let referenceNode: Node | null = null;
for (const mutation of mutations) {
const target = mutation.target;
if (mutation.type === "childList") {
if (!isFrameHandle(target)) {
/* nested insertion */
continue;
}
const handleElement = target;
const placement =
handleElement instanceof FrameStart ? "beforebegin" : "afterend";
const frameElement = handleElement.parentElement as FrameElement;
for (const node of mutation.addedNodes) {
if (skippableNode(handleElement, node)) {
continue;
}
if (
nodeIsElement(node) &&
!elementIsEmpty(node) &&
(node.textContent === spaceCharacter ||
node.textContent?.length === 0)
) {
/**
* When we surround the spaceCharacter of the frame handle
*/
node.replaceWith(new Text(spaceCharacter));
} else {
referenceNode = moveChildOutOfElement(
frameElement,
node,
placement,
);
}
}
} else if (mutation.type === "characterData") {
if (
!nodeIsText(target) ||
!isFrameHandle(target.parentElement) ||
skippableNode(target.parentElement, target)
) {
continue;
}
const handleElement = target.parentElement;
const placement =
handleElement instanceof FrameStart ? "beforebegin" : "afterend";
const frameElement = handleElement.parentElement! as FrameElement;
const cleaned = target.data.replace(spaceRegex, "");
const text = new Text(cleaned);
if (placement === "beforebegin") {
frameElement.before(text);
} else {
frameElement.after(text);
}
handleElement.refreshSpace();
referenceNode = text;
}
}
if (referenceNode) {
placeCaretAfter(referenceNode);
}
}
const handleObserver = new MutationObserver(restoreHandleContent);
const handles: Set<FrameHandle> = new Set();
export abstract class FrameHandle extends HTMLElement {
static get observedAttributes(): string[] {
return ["data-frames"];
}
/**
* When a deletion is trigger with a FrameHandle selected, it will be treated
* differently depending on whether it is selected:
* - If partially selected, it should be restored (unless the frame element
* is also selected).
* - Otherwise, it should be deleted along with the frame element.
*/
partiallySelected = false;
Fix some Mathjax issues (#1547) * Move move-nodes logic into domlib Refactor input-manager Refactor out FocusTrap from EditingArea Remove unnecessary selecting of node from surround Add onInput interface to input-manager Create MathjaxElement.svelte - This should contain all the setup necessary for displaying <anki-mathjax> elements in the rich text input - Does not contain setup necessary for Mathjax Overlay Deal with backwards deletion, when caret inside anki-mathjax Set mathjax elements contenteditable=false Do not undecorate mathjaxx element on disconnect - Fixes issues, where Mathjax might undecorate when it is moved into a different div Add framed element custom element Introduce iterateActions to allow global hooks for RichTextInput Remove some old code Deal with deletion of frame handles Make Anki frame and frame handles restore each other Make FrameElement restore its structure upon modification Frame and strip off framing from MathjaxElement automatically Move FrameHandle to separate FrameStart/FrameEnd Refactor FrameHandle Set data-frames on FrameElement Fix logic error connected to FrameElement Communicate frameHandle move{in,out} to anki-mathjax Clear selection when blurring ContentEditable Make sure frame is destroyed when undecorating Mathjax Use Hairline space instead of zeroWidth - it has better behavior in the contenteditable when placing the caret via clicking Allow detection of block elements with `block` attribute - This way, anki-mathjax block="true" will make field a field be recognized to have block content Make block frame element operater without handles - Clicking on the left/right side of a block mathjax will insert a br element to that side When deleting, remove mathajax-element not just image Update MathjaxButtons to correctly show block state SelectAll when moving into inline anki mathjax Remove CodeMirror autofocus functionality Move it to Mathjaxeditor directly Fix getRangeAt throwing error Update older code to use cross-browser Fix issue with FrameHandles not being properyly removed Satisfy formatting Use === instead of node.isSameNode() Fix issue of focusTrap not initialized * Fix after rebasing * Fix focus not being moved to first field * Add documentation for input-manager and iterate-actions * Export logic of ContentEditable to content-editable * Fix issue with inserting newline right next to inline Mathjax * Fix reframing issue of Mathjax Svelte component * Allow for deletion of Inline Mathjax again * Rename iterate-actions to action-list * Add copyright header * Split off frame-handle from frame-element * Add some comments for framing process * Add mising return types
2022-01-08 02:46:01 +01:00
frames?: string;
constructor() {
super();
handleObserver.observe(this, {
childList: true,
subtree: true,
characterData: true,
});
}
attributeChangedCallback(name: string, old: string, newValue: string): void {
if (newValue === old) {
return;
}
switch (name) {
case "data-frames":
this.frames = newValue;
break;
}
}
abstract getFrameRange(): Range;
invalidSpace(): boolean {
return (
!this.firstChild ||
!(nodeIsText(this.firstChild) && this.firstChild.data === spaceCharacter)
);
}
refreshSpace(): void {
while (this.firstChild) {
this.removeChild(this.firstChild);
}
this.append(new Text(spaceCharacter));
}
hostedUnderFrame(): boolean {
return this.parentElement!.tagName === frameElementTagName.toUpperCase();
}
connectedCallback(): void {
if (this.invalidSpace()) {
this.refreshSpace();
}
if (!this.hostedUnderFrame()) {
const range = this.getFrameRange();
const frameElement = document.createElement(
frameElementTagName,
) as FrameElement;
frameElement.dataset.frames = this.frames;
range.surroundContents(frameElement);
}
handles.add(this);
}
removeMoveIn?: () => void;
disconnectedCallback(): void {
handles.delete(this);
this.removeMoveIn?.();
this.removeMoveIn = undefined;
}
abstract notifyMoveIn(offset: number): void;
}
export class FrameStart extends FrameHandle {
static tagName = "frame-start";
getFrameRange(): Range {
const range = new Range();
range.setStartBefore(this);
const maybeFramed = this.nextElementSibling;
if (maybeFramed?.matches(this.frames ?? ":not(*)")) {
const maybeHandleEnd = maybeFramed.nextElementSibling;
range.setEndAfter(
maybeHandleEnd?.tagName.toLowerCase() === FrameStart.tagName
? maybeHandleEnd
: maybeFramed,
);
} else {
range.setEndAfter(this);
}
return range;
}
notifyMoveIn(offset: number): void {
if (offset === 1) {
this.dispatchEvent(new Event("movein"));
}
}
connectedCallback(): void {
super.connectedCallback();
this.removeMoveIn = on(this, "movein" as keyof HTMLElementEventMap, () =>
this.parentElement?.dispatchEvent(new Event("moveinstart")),
);
}
}
export class FrameEnd extends FrameHandle {
static tagName = "frame-end";
getFrameRange(): Range {
const range = new Range();
range.setEndAfter(this);
const maybeFramed = this.previousElementSibling;
if (maybeFramed?.matches(this.frames ?? ":not(*)")) {
const maybeHandleStart = maybeFramed.previousElementSibling;
range.setEndAfter(
maybeHandleStart?.tagName.toLowerCase() === FrameEnd.tagName
? maybeHandleStart
: maybeFramed,
);
} else {
range.setStartBefore(this);
}
return range;
}
notifyMoveIn(offset: number): void {
if (offset === 0) {
this.dispatchEvent(new Event("movein"));
}
}
connectedCallback(): void {
super.connectedCallback();
this.removeMoveIn = on(this, "movein" as keyof HTMLElementEventMap, () =>
this.parentElement?.dispatchEvent(new Event("moveinend")),
);
}
}
function checkWhetherMovingIntoHandle(selection: Selection, handle: FrameHandle): void {
if (selection.anchorNode === handle.firstChild && isSelectionCollapsed(selection)) {
handle.notifyMoveIn(selection.anchorOffset);
}
}
function checkWhetherSelectingHandle(selection: Selection, handle: FrameHandle): void {
handle.partiallySelected =
handle.firstChild && !isSelectionCollapsed(selection)
? selection.containsNode(handle.firstChild)
: false;
}
export function checkHandles(): void {
Fix some Mathjax issues (#1547) * Move move-nodes logic into domlib Refactor input-manager Refactor out FocusTrap from EditingArea Remove unnecessary selecting of node from surround Add onInput interface to input-manager Create MathjaxElement.svelte - This should contain all the setup necessary for displaying <anki-mathjax> elements in the rich text input - Does not contain setup necessary for Mathjax Overlay Deal with backwards deletion, when caret inside anki-mathjax Set mathjax elements contenteditable=false Do not undecorate mathjaxx element on disconnect - Fixes issues, where Mathjax might undecorate when it is moved into a different div Add framed element custom element Introduce iterateActions to allow global hooks for RichTextInput Remove some old code Deal with deletion of frame handles Make Anki frame and frame handles restore each other Make FrameElement restore its structure upon modification Frame and strip off framing from MathjaxElement automatically Move FrameHandle to separate FrameStart/FrameEnd Refactor FrameHandle Set data-frames on FrameElement Fix logic error connected to FrameElement Communicate frameHandle move{in,out} to anki-mathjax Clear selection when blurring ContentEditable Make sure frame is destroyed when undecorating Mathjax Use Hairline space instead of zeroWidth - it has better behavior in the contenteditable when placing the caret via clicking Allow detection of block elements with `block` attribute - This way, anki-mathjax block="true" will make field a field be recognized to have block content Make block frame element operater without handles - Clicking on the left/right side of a block mathjax will insert a br element to that side When deleting, remove mathajax-element not just image Update MathjaxButtons to correctly show block state SelectAll when moving into inline anki mathjax Remove CodeMirror autofocus functionality Move it to Mathjaxeditor directly Fix getRangeAt throwing error Update older code to use cross-browser Fix issue with FrameHandles not being properyly removed Satisfy formatting Use === instead of node.isSameNode() Fix issue of focusTrap not initialized * Fix after rebasing * Fix focus not being moved to first field * Add documentation for input-manager and iterate-actions * Export logic of ContentEditable to content-editable * Fix issue with inserting newline right next to inline Mathjax * Fix reframing issue of Mathjax Svelte component * Allow for deletion of Inline Mathjax again * Rename iterate-actions to action-list * Add copyright header * Split off frame-handle from frame-element * Add some comments for framing process * Add mising return types
2022-01-08 02:46:01 +01:00
for (const handle of handles) {
const selection = getSelection(handle)!;
if (selection.rangeCount === 0) {
continue;
Fix some Mathjax issues (#1547) * Move move-nodes logic into domlib Refactor input-manager Refactor out FocusTrap from EditingArea Remove unnecessary selecting of node from surround Add onInput interface to input-manager Create MathjaxElement.svelte - This should contain all the setup necessary for displaying <anki-mathjax> elements in the rich text input - Does not contain setup necessary for Mathjax Overlay Deal with backwards deletion, when caret inside anki-mathjax Set mathjax elements contenteditable=false Do not undecorate mathjaxx element on disconnect - Fixes issues, where Mathjax might undecorate when it is moved into a different div Add framed element custom element Introduce iterateActions to allow global hooks for RichTextInput Remove some old code Deal with deletion of frame handles Make Anki frame and frame handles restore each other Make FrameElement restore its structure upon modification Frame and strip off framing from MathjaxElement automatically Move FrameHandle to separate FrameStart/FrameEnd Refactor FrameHandle Set data-frames on FrameElement Fix logic error connected to FrameElement Communicate frameHandle move{in,out} to anki-mathjax Clear selection when blurring ContentEditable Make sure frame is destroyed when undecorating Mathjax Use Hairline space instead of zeroWidth - it has better behavior in the contenteditable when placing the caret via clicking Allow detection of block elements with `block` attribute - This way, anki-mathjax block="true" will make field a field be recognized to have block content Make block frame element operater without handles - Clicking on the left/right side of a block mathjax will insert a br element to that side When deleting, remove mathajax-element not just image Update MathjaxButtons to correctly show block state SelectAll when moving into inline anki mathjax Remove CodeMirror autofocus functionality Move it to Mathjaxeditor directly Fix getRangeAt throwing error Update older code to use cross-browser Fix issue with FrameHandles not being properyly removed Satisfy formatting Use === instead of node.isSameNode() Fix issue of focusTrap not initialized * Fix after rebasing * Fix focus not being moved to first field * Add documentation for input-manager and iterate-actions * Export logic of ContentEditable to content-editable * Fix issue with inserting newline right next to inline Mathjax * Fix reframing issue of Mathjax Svelte component * Allow for deletion of Inline Mathjax again * Rename iterate-actions to action-list * Add copyright header * Split off frame-handle from frame-element * Add some comments for framing process * Add mising return types
2022-01-08 02:46:01 +01:00
}
checkWhetherMovingIntoHandle(selection, handle);
checkWhetherSelectingHandle(selection, handle);
Fix some Mathjax issues (#1547) * Move move-nodes logic into domlib Refactor input-manager Refactor out FocusTrap from EditingArea Remove unnecessary selecting of node from surround Add onInput interface to input-manager Create MathjaxElement.svelte - This should contain all the setup necessary for displaying <anki-mathjax> elements in the rich text input - Does not contain setup necessary for Mathjax Overlay Deal with backwards deletion, when caret inside anki-mathjax Set mathjax elements contenteditable=false Do not undecorate mathjaxx element on disconnect - Fixes issues, where Mathjax might undecorate when it is moved into a different div Add framed element custom element Introduce iterateActions to allow global hooks for RichTextInput Remove some old code Deal with deletion of frame handles Make Anki frame and frame handles restore each other Make FrameElement restore its structure upon modification Frame and strip off framing from MathjaxElement automatically Move FrameHandle to separate FrameStart/FrameEnd Refactor FrameHandle Set data-frames on FrameElement Fix logic error connected to FrameElement Communicate frameHandle move{in,out} to anki-mathjax Clear selection when blurring ContentEditable Make sure frame is destroyed when undecorating Mathjax Use Hairline space instead of zeroWidth - it has better behavior in the contenteditable when placing the caret via clicking Allow detection of block elements with `block` attribute - This way, anki-mathjax block="true" will make field a field be recognized to have block content Make block frame element operater without handles - Clicking on the left/right side of a block mathjax will insert a br element to that side When deleting, remove mathajax-element not just image Update MathjaxButtons to correctly show block state SelectAll when moving into inline anki mathjax Remove CodeMirror autofocus functionality Move it to Mathjaxeditor directly Fix getRangeAt throwing error Update older code to use cross-browser Fix issue with FrameHandles not being properyly removed Satisfy formatting Use === instead of node.isSameNode() Fix issue of focusTrap not initialized * Fix after rebasing * Fix focus not being moved to first field * Add documentation for input-manager and iterate-actions * Export logic of ContentEditable to content-editable * Fix issue with inserting newline right next to inline Mathjax * Fix reframing issue of Mathjax Svelte component * Allow for deletion of Inline Mathjax again * Rename iterate-actions to action-list * Add copyright header * Split off frame-handle from frame-element * Add some comments for framing process * Add mising return types
2022-01-08 02:46:01 +01:00
}
}