anki/ts/editable/ContentEditable.svelte
Henrik Giesel 64d46ca638
Reverse-engineer surrounding with execCommand (#1377)
* Add utility functions for saving and restoring the caret location

Implement surroundNoSplitting

Clarify surroundNoSplitting comments

Start implementing surroundSplitting and triggerIfSimpleInput

Fix after rebase

Implement findBefore / findAfter in lib/surround

* to merge adjacent nodes into the surrounding nodes

Use new prettier settings with lib/{location,surround}

Fix imports that I missed to rename

Add some tests for find-adjacent

Split find-within from find-adjacent

Normalize nodes after insertion in surroundNoSplitting

Do not deep clone surroundNode

-> no intention of supporting deep nodes, as normalization would be impossible

Add some tests concerning nested surrounding nested nodes

Select surroundedRange after surrounding

Fix ascendWhileSingleInline

A flawed first surround/trigger implementation

Move trigger out of lib/surround

Implement Input Manager as a way to handle bold on empty selection

Switch bold button away from execCommand

Pass in Matcher instead of selector to find-adjacent and surroundNoSplitting

* Also adds a failing test for no-splitting

Refactor find-adjacent

* add failing test when findBefore's nodes have different amounts of
  child nodes

Change type signature of find-adjacent methods to more single-concern

Add test for surrounding where adjacent block becomes three Text elements

Make nodes found within surrounded range extend the ranges endOffset

Add base parameter to surroundNoSplitting to stop ascending beyond container

Stop surrounding from bubbling beyond base in merge-match

Make all tests pass

Add some failing tests to point to future development

Add empty elements as constant

Implement a broken version of unsurround

Even split text if it creates zero-length texts

-> they are still valid, despite what Chromium says

Rename {start,end} to {start,end}Container

Add more unit tests with surround after a nested element

Set endOffset after split-off possibly zero length text nodes

Deal with empty elements when surrounding

Only include split off end text if zero length

Use range anchors instead off calcluating surroundedRange from offsets

* this approach allows for removal of base elements when unsurrounding

Comment out test which fail because of jsdom bugs

We'll be able to enable them again after Jest 28

Make the first unsurround tests pass

Add new failing test for unsurround text within tag

Fix unsurround

Test is deactivated until Jest 28

Rewrite input-manager and trigger callback after insertion

Avoid creating zero length text nodes by using insertBefore when appropriate

Implement matches vs keepMatches

Make shadow root and editable element available on component tree

Make WithState work with asynchronous updater functions

Add new Bold/Italic/UnderlineButton using our logic

Add failing test for unsurrounding

* Move surround/ to domlib

* Add jest dependency

* Make find-within return a sum type array rather than two arrays

* Use FoundMatch sum-type for find-above (and find-within)

* Fix issue where elements could be cleared twice

* if they are IN the range.endContainer

* Pass remaining test

* Add another failing test

* Fix empty text nodes being considered for surrounding

* Satisfy svelte check

* Make on more type correct

* Satisfy remaining tests

* Add missing copyright header
2021-11-18 19:18:39 +10:00

91 lines
2.5 KiB
Svelte

<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import type { Writable } from "svelte/store";
import { updateAllState } from "../components/WithState.svelte";
import { saveSelection, restoreSelection } from "../domlib/location";
import { on } from "../lib/events";
import { registerShortcut } from "../lib/shortcuts";
export let nodes: Writable<DocumentFragment>;
export let resolve: (editable: HTMLElement) => void;
export let mirror: (
editable: HTMLElement,
params: { store: Writable<DocumentFragment> },
) => void;
export let inputManager: (editable: HTMLElement) => void;
/* must execute before DOMMirror */
function saveLocation(editable: HTMLElement) {
let removeOnFocus: () => void;
let removeOnPointerdown: () => void;
const removeOnBlur = on(editable, "blur", () => {
const location = saveSelection(editable);
removeOnFocus = on(
editable,
"focus",
() => {
if (location) {
restoreSelection(editable, location);
}
},
{ once: true },
);
removeOnPointerdown = on(editable, "pointerdown", () => removeOnFocus?.(), {
once: true,
});
});
return {
destroy() {
removeOnBlur();
removeOnFocus?.();
removeOnPointerdown?.();
},
};
}
let editable: HTMLElement;
$: if (editable) {
registerShortcut((event) => event.preventDefault(), "Control+B", editable);
registerShortcut((event) => event.preventDefault(), "Control+U", editable);
registerShortcut((event) => event.preventDefault(), "Control+I", editable);
registerShortcut((event) => event.preventDefault(), "Control+R", editable);
}
</script>
<anki-editable
contenteditable="true"
bind:this={editable}
use:resolve
use:saveLocation
use:mirror={{ store: nodes }}
use:inputManager
on:focus
on:blur
on:click={updateAllState}
on:keyup={updateAllState}
/>
<style lang="scss">
anki-editable {
display: block;
overflow-wrap: break-word;
overflow: auto;
padding: 6px;
&:focus {
outline: none;
}
}
/* editable-base.scss contains styling targeting user HTML */
</style>