@webwriter/quiz 1.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +8 -0
- package/LICENSE +6 -6
- package/README.md +778 -0
- package/assets/icons/grip-horizontal.svg +8 -0
- package/assets/icons/grip-vertical.svg +8 -0
- package/assets/icons/truefalse.svg +6 -0
- package/custom-elements.json +2779 -0
- package/custom.d.ts +180 -176
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.js +40 -0
- package/dist/migrate.js +1434 -0
- package/dist/widgets/webwriter-choice-item.js +4036 -3895
- package/dist/widgets/webwriter-choice.js +2269 -5821
- package/dist/widgets/webwriter-gap-item.js +3987 -0
- package/dist/widgets/webwriter-gap.js +2908 -0
- package/dist/widgets/webwriter-mark.js +4335 -3489
- package/dist/widgets/webwriter-order-item.js +4014 -3306
- package/dist/widgets/webwriter-order.js +2944 -4386
- package/dist/widgets/webwriter-pairing-item.js +1058 -2421
- package/dist/widgets/webwriter-pairing.js +2993 -4383
- package/dist/widgets/webwriter-quiz-hint.js +2469 -0
- package/dist/widgets/webwriter-quiz.js +4933 -6099
- package/dist/widgets/webwriter-speech.js +6077 -5230
- package/dist/widgets/webwriter-task-prompt.js +4775 -2165
- package/dist/widgets/webwriter-task.js +2364 -8382
- package/dist/widgets/webwriter-text.js +4169 -4337
- package/dist/widgets/webwriter-true-false.js +4674 -0
- package/editing-config.json +106 -0
- package/lit-localize.json +21 -15
- package/localization/de.xlf +407 -74
- package/localization/es.xlf +407 -69
- package/localization/fr.xlf +407 -69
- package/localization/generated/de.ts +112 -29
- package/localization/generated/es.ts +112 -29
- package/localization/generated/fr.ts +112 -29
- package/localization/generated/index.d.ts +5 -0
- package/localization/generated/index.js +2 -2
- package/localization/generated/it.ts +112 -29
- package/localization/generated/locale-codes.js +31 -79
- package/localization/generated/nl.ts +112 -29
- package/localization/it.xlf +407 -69
- package/localization/nl.xlf +407 -69
- package/package.json +119 -252
- package/scripts/apply-translations.mjs +97 -0
- package/src/api/index.ts +72 -0
- package/src/lib/artboard.ts +686 -0
- package/src/lib/draggable-grid.ts +432 -0
- package/src/lib/encrypted-property.ts +202 -0
- package/src/lib/hint-popup.ts +152 -0
- package/src/lib/utils.ts +97 -0
- package/src/lib/webwriter-quirks.ts +121 -0
- package/src/migrate/index.ts +622 -0
- package/src/migrate/legacy-solution.ts +362 -0
- package/src/snippets/order.html +9 -8
- package/src/widgets/choice/webwriter-choice-item.ts +316 -0
- package/src/widgets/choice/webwriter-choice.ts +328 -0
- package/src/widgets/choice/webwriter-true-false.ts +113 -0
- package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
- package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
- package/src/widgets/dnd/webwriter-dnd.ts +36 -0
- package/src/widgets/gap/webwriter-gap-item.ts +250 -0
- package/src/widgets/gap/webwriter-gap.ts +447 -0
- package/src/widgets/order/webwriter-order-item.ts +243 -0
- package/src/widgets/order/webwriter-order.ts +282 -0
- package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
- package/src/widgets/pairing/webwriter-pairing.ts +934 -0
- package/src/widgets/speech/audio-player.ts +325 -0
- package/src/widgets/speech/audio-recorder.ts +285 -0
- package/src/widgets/speech/waveform.ts +113 -0
- package/src/widgets/speech/webwriter-speech.ts +87 -0
- package/src/widgets/webwriter-mark.ts +374 -432
- package/src/widgets/webwriter-quiz-hint.ts +36 -0
- package/src/widgets/webwriter-quiz.ts +320 -282
- package/src/widgets/webwriter-task-prompt.ts +49 -24
- package/src/widgets/webwriter-task.ts +216 -547
- package/src/widgets/webwriter-text.ts +291 -156
- package/tsconfig.api.json +12 -0
- package/tsconfig.json +8 -7
- package/dist/widgets/webwriter-choice.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.js +0 -5866
- package/dist/widgets/webwriter-cloze.css +0 -444
- package/dist/widgets/webwriter-cloze.js +0 -3786
- package/dist/widgets/webwriter-mark.css +0 -444
- package/dist/widgets/webwriter-order.css +0 -444
- package/dist/widgets/webwriter-pairing-item.css +0 -444
- package/dist/widgets/webwriter-pairing.css +0 -444
- package/dist/widgets/webwriter-quiz.css +0 -444
- package/dist/widgets/webwriter-task-explainer.css +0 -444
- package/dist/widgets/webwriter-task-explainer.js +0 -2872
- package/dist/widgets/webwriter-task-hint.css +0 -444
- package/dist/widgets/webwriter-task-hint.js +0 -2811
- package/dist/widgets/webwriter-task.css +0 -444
- package/dist/widgets/webwriter-text.css +0 -444
- package/localization/bg.xlf +0 -69
- package/localization/cs.xlf +0 -69
- package/localization/da.xlf +0 -69
- package/localization/el.xlf +0 -69
- package/localization/et.xlf +0 -69
- package/localization/fi.xlf +0 -69
- package/localization/generated/bg.ts +0 -30
- package/localization/generated/cs.ts +0 -30
- package/localization/generated/da.ts +0 -30
- package/localization/generated/el.ts +0 -30
- package/localization/generated/et.ts +0 -30
- package/localization/generated/fi.ts +0 -30
- package/localization/generated/hu.ts +0 -30
- package/localization/generated/id.ts +0 -30
- package/localization/generated/ja.ts +0 -30
- package/localization/generated/ko.ts +0 -30
- package/localization/generated/lt.ts +0 -30
- package/localization/generated/lv.ts +0 -30
- package/localization/generated/nb.ts +0 -30
- package/localization/generated/pl.ts +0 -30
- package/localization/generated/pt-BR.ts +0 -30
- package/localization/generated/pt-PT.ts +0 -30
- package/localization/generated/ro.ts +0 -30
- package/localization/generated/ru.ts +0 -30
- package/localization/generated/sk.ts +0 -30
- package/localization/generated/sl.ts +0 -30
- package/localization/generated/sv.ts +0 -30
- package/localization/generated/tr.ts +0 -30
- package/localization/generated/uk.ts +0 -30
- package/localization/generated/zh-hans.ts +0 -30
- package/localization/hu.xlf +0 -69
- package/localization/id.xlf +0 -69
- package/localization/ja.xlf +0 -69
- package/localization/ko.xlf +0 -69
- package/localization/lt.xlf +0 -69
- package/localization/lv.xlf +0 -69
- package/localization/nb.xlf +0 -69
- package/localization/pl.xlf +0 -69
- package/localization/pt-BR.xlf +0 -69
- package/localization/pt-PT.xlf +0 -69
- package/localization/ro.xlf +0 -69
- package/localization/ru.xlf +0 -69
- package/localization/sk.xlf +0 -69
- package/localization/sl.xlf +0 -69
- package/localization/sv.xlf +0 -69
- package/localization/tr.xlf +0 -69
- package/localization/uk.xlf +0 -69
- package/localization/zh-hans.xlf +0 -69
- package/src/lib/combobox.ts +0 -456
- package/src/lib/highlighter-fill.svg +0 -6
- package/src/snippets/Beispiel-Choice.html +0 -57
- package/src/snippets/Beispiel-Mark.html +0 -14
- package/src/snippets/Beispiel-Order.html +0 -52
- package/src/snippets/Beispiel-Text.html +0 -20
- package/src/snippets/blank.html +0 -1
- package/src/snippets/choice.html +0 -8
- package/src/snippets/cloze.html +0 -4
- package/src/snippets/mark.html +0 -4
- package/src/snippets/pairing.html +0 -10
- package/src/snippets/speech.html +0 -4
- package/src/snippets/text.html +0 -4
- package/src/snippets/wordsearch.html +0 -4
- package/src/widgets/webwriter-choice-item.ts +0 -254
- package/src/widgets/webwriter-choice.ts +0 -353
- package/src/widgets/webwriter-cloze-gap.ts +0 -216
- package/src/widgets/webwriter-cloze.ts +0 -136
- package/src/widgets/webwriter-order-item.ts +0 -441
- package/src/widgets/webwriter-order.ts +0 -327
- package/src/widgets/webwriter-pairing-item.ts +0 -161
- package/src/widgets/webwriter-pairing.ts +0 -193
- package/src/widgets/webwriter-speech.ts +0 -273
- package/src/widgets/webwriter-task-explainer.ts +0 -43
- package/src/widgets/webwriter-task-hint.ts +0 -23
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js";
|
|
2
|
+
import SlPopup from "@shoelace-style/shoelace/dist/components/popup/popup.component.js";
|
|
3
|
+
import "@shoelace-style/shoelace/dist/themes/light.css";
|
|
4
|
+
import { LitElementWw } from "@webwriter/lit";
|
|
5
|
+
import IconQuestionCircleFill from "bootstrap-icons/icons/question-circle-fill.svg";
|
|
6
|
+
import IconQuestionCircle from "bootstrap-icons/icons/question-circle.svg";
|
|
7
|
+
import { css, html, nothing } from "lit";
|
|
8
|
+
import { property, state } from "lit/decorators.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A reusable popup with a "?" trigger that manages a `<webwriter-quiz-hint>`
|
|
12
|
+
* element on its host.
|
|
13
|
+
*
|
|
14
|
+
* Use it inside your widget like this:
|
|
15
|
+
* <hint-popup>
|
|
16
|
+
* <slot name="hint"></slot>
|
|
17
|
+
* </hint-popup>
|
|
18
|
+
*
|
|
19
|
+
* Make sure to include `webwriter-quiz-hint?` in your widget's content property.
|
|
20
|
+
*/
|
|
21
|
+
export class HintPopup extends LitElementWw {
|
|
22
|
+
static readonly CONTENT_TAG = "webwriter-quiz-hint";
|
|
23
|
+
|
|
24
|
+
static scopedElements = {
|
|
25
|
+
"sl-icon-button": SlIconButton,
|
|
26
|
+
"sl-popup": SlPopup,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
static styles = css`
|
|
30
|
+
:host {
|
|
31
|
+
display: inline-block;
|
|
32
|
+
height: 1.5em;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
sl-icon-button::part(base) {
|
|
36
|
+
padding: var(--sl-spacing-2x-small);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
sl-popup {
|
|
40
|
+
--arrow-color: var(--sl-color-neutral-700);
|
|
41
|
+
|
|
42
|
+
&::part(popup) {
|
|
43
|
+
padding: var(--sl-spacing-x-small);
|
|
44
|
+
max-width: 20rem;
|
|
45
|
+
background: var(--sl-color-neutral-700);
|
|
46
|
+
border-radius: var(--sl-border-radius-medium);
|
|
47
|
+
color: var(--sl-color-neutral-0);
|
|
48
|
+
font-size: var(--sl-font-size-small);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
@property({ type: String, attribute: "slot-name", reflect: true })
|
|
54
|
+
accessor slotName = "hint";
|
|
55
|
+
|
|
56
|
+
@property({ type: String, attribute: "position", reflect: true })
|
|
57
|
+
accessor position: "start" | "end" = "end";
|
|
58
|
+
|
|
59
|
+
@state()
|
|
60
|
+
private accessor open = false;
|
|
61
|
+
|
|
62
|
+
private get host(): HTMLElement | null {
|
|
63
|
+
const root = this.getRootNode();
|
|
64
|
+
if (root instanceof ShadowRoot) {
|
|
65
|
+
return root.host as HTMLElement;
|
|
66
|
+
}
|
|
67
|
+
return this.parentElement;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private get hostContentEls(): HTMLElement[] {
|
|
71
|
+
const host = this.host;
|
|
72
|
+
if (!host || !this.slotName) return [];
|
|
73
|
+
return Array.from(host.querySelectorAll<HTMLElement>(`:scope > [slot="${this.slotName}"]`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private get hasContent(): boolean {
|
|
77
|
+
return this.hostContentEls.some(el => el.innerText.trim() !== "");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private insertHintElement(host: HTMLElement): HTMLElement {
|
|
81
|
+
const el = host.ownerDocument.createElement(HintPopup.CONTENT_TAG);
|
|
82
|
+
el.slot = this.slotName;
|
|
83
|
+
|
|
84
|
+
if (this.position === "start") {
|
|
85
|
+
host.prepend(el);
|
|
86
|
+
} else {
|
|
87
|
+
host.append(el);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return el;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private toggle() {
|
|
94
|
+
this.open = !this.open;
|
|
95
|
+
const host = this.host;
|
|
96
|
+
if (!host) return;
|
|
97
|
+
|
|
98
|
+
if (host.isContentEditable && this.open) {
|
|
99
|
+
if (this.hostContentEls.length === 0) this.insertHintElement(host);
|
|
100
|
+
|
|
101
|
+
// Move cursor to the end of the hint content when opening the popup
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
if (!(this.children[0] instanceof HTMLSlotElement)) return;
|
|
104
|
+
const assignedElements = this.children[0].assignedElements();
|
|
105
|
+
const hintContent = assignedElements[assignedElements.length - 1];
|
|
106
|
+
if (!(hintContent instanceof HTMLElement)) return;
|
|
107
|
+
|
|
108
|
+
let endContainer: Node = hintContent;
|
|
109
|
+
while (endContainer.lastChild) endContainer = endContainer.lastChild;
|
|
110
|
+
|
|
111
|
+
const range = hintContent.ownerDocument.createRange();
|
|
112
|
+
range.setStart(
|
|
113
|
+
endContainer,
|
|
114
|
+
endContainer.nodeType === Node.TEXT_NODE
|
|
115
|
+
? (endContainer.textContent ?? "").length
|
|
116
|
+
: endContainer.childNodes.length,
|
|
117
|
+
);
|
|
118
|
+
range.collapse(true);
|
|
119
|
+
|
|
120
|
+
const selection = hintContent.ownerDocument.getSelection();
|
|
121
|
+
selection?.removeAllRanges();
|
|
122
|
+
selection?.addRange(range);
|
|
123
|
+
}, 0);
|
|
124
|
+
} else if (host.isContentEditable && !this.open) {
|
|
125
|
+
if (!this.hasContent) {
|
|
126
|
+
this.hostContentEls.forEach(el => el.remove());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
render() {
|
|
132
|
+
if (this.host && !this.host.isContentEditable && !this.hasContent) return nothing;
|
|
133
|
+
|
|
134
|
+
return html`<sl-popup
|
|
135
|
+
?active=${this.open}
|
|
136
|
+
placement="left"
|
|
137
|
+
arrow
|
|
138
|
+
auto-size
|
|
139
|
+
shift
|
|
140
|
+
distance="5"
|
|
141
|
+
@selectstart=${(e: Event) => e.stopImmediatePropagation()}
|
|
142
|
+
>
|
|
143
|
+
<sl-icon-button
|
|
144
|
+
part=${this.hasContent || this.open ? "filled" : "empty"}
|
|
145
|
+
slot="anchor"
|
|
146
|
+
src=${this.hasContent || this.open ? IconQuestionCircleFill : IconQuestionCircle}
|
|
147
|
+
@click=${() => this.toggle()}
|
|
148
|
+
></sl-icon-button>
|
|
149
|
+
<slot @slotchange=${() => this.requestUpdate()}></slot>
|
|
150
|
+
</sl-popup>`;
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { css } from "lit";
|
|
2
|
+
|
|
3
|
+
export const commonStyles = css`
|
|
4
|
+
:host(:not([contenteditable="true"]):not([contenteditable=""])) .author-only {
|
|
5
|
+
display: none;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
:host(:is([contenteditable="true"], [contenteditable=""])) .user-only {
|
|
9
|
+
display: none;
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export function randomizeArray<T>(items: T[]): T[] {
|
|
14
|
+
const randomized = [...items];
|
|
15
|
+
for (let i = randomized.length - 1; i > 0; i--) {
|
|
16
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
17
|
+
[randomized[i], randomized[j]] = [randomized[j], randomized[i]];
|
|
18
|
+
}
|
|
19
|
+
return randomized;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolvedCssVariableCache = new Map<string, string>();
|
|
23
|
+
|
|
24
|
+
export function resolveCssVariable(variableName: `--${string}`, fallback: string) {
|
|
25
|
+
const cachedValue = resolvedCssVariableCache.get(variableName);
|
|
26
|
+
if (cachedValue) return cachedValue;
|
|
27
|
+
|
|
28
|
+
const value = getComputedStyle(document.body).getPropertyValue(variableName).trim() || fallback;
|
|
29
|
+
|
|
30
|
+
resolvedCssVariableCache.set(variableName, value);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class BiMap<K, V> {
|
|
35
|
+
private readonly keyToValue = new Map<K, V>();
|
|
36
|
+
private readonly valueToKey = new Map<V, K>();
|
|
37
|
+
|
|
38
|
+
set(key: K, value: V): this {
|
|
39
|
+
if (this.keyToValue.has(key)) {
|
|
40
|
+
const oldValue = this.keyToValue.get(key)!;
|
|
41
|
+
this.valueToKey.delete(oldValue);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (this.valueToKey.has(value)) {
|
|
45
|
+
const oldKey = this.valueToKey.get(value)!;
|
|
46
|
+
this.keyToValue.delete(oldKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.keyToValue.set(key, value);
|
|
50
|
+
this.valueToKey.set(value, key);
|
|
51
|
+
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getValue(key: K): V | undefined {
|
|
56
|
+
return this.keyToValue.get(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getKey(value: V): K | undefined {
|
|
60
|
+
return this.valueToKey.get(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
hasKey(key: K): boolean {
|
|
64
|
+
return this.keyToValue.has(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
hasValue(value: V): boolean {
|
|
68
|
+
return this.valueToKey.has(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
deleteKey(key: K): boolean {
|
|
72
|
+
if (!this.keyToValue.has(key)) return false;
|
|
73
|
+
|
|
74
|
+
const value = this.keyToValue.get(key)!;
|
|
75
|
+
this.keyToValue.delete(key);
|
|
76
|
+
this.valueToKey.delete(value);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
deleteValue(value: V): boolean {
|
|
81
|
+
if (!this.valueToKey.has(value)) return false;
|
|
82
|
+
|
|
83
|
+
const key = this.valueToKey.get(value)!;
|
|
84
|
+
this.valueToKey.delete(value);
|
|
85
|
+
this.keyToValue.delete(key);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
clear(): void {
|
|
90
|
+
this.keyToValue.clear();
|
|
91
|
+
this.valueToKey.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get size(): number {
|
|
95
|
+
return this.keyToValue.size;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @returns True if the user is using WebWriter, false otherwise.
|
|
3
|
+
*/
|
|
4
|
+
export const isWebWriter = () => document.documentElement.id.startsWith("ww-");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Equivalent to the check used in ProseMirror to determine if the user is on a Mac or iOS device,
|
|
8
|
+
* which is used to determined if the `Mod-` key refers to `Meta` or `Control`.
|
|
9
|
+
* @see https://code.haverbeke.berlin/prosemirror/prosemirror-keymap/src/commit/176de4152dca0e473f74504e8995d7a281392eb5/src/keymap.ts#L5
|
|
10
|
+
*
|
|
11
|
+
* @returns True if the user is on a Mac or iOS device, false otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export const isMac = () => typeof navigator != "undefined" && /Mac|iP(hone|[oa]d)/.test(navigator.platform);
|
|
14
|
+
|
|
15
|
+
let enterCreatesNewSelectors = new Set<string>();
|
|
16
|
+
let enterCreatesNewInitialized = false;
|
|
17
|
+
|
|
18
|
+
export function registerAsEnterCreatesNew(selector: string) {
|
|
19
|
+
if (!isWebWriter()) return;
|
|
20
|
+
|
|
21
|
+
enterCreatesNewSelectors.add(selector.toLowerCase());
|
|
22
|
+
if (!enterCreatesNewInitialized) {
|
|
23
|
+
document.body.addEventListener(
|
|
24
|
+
"keydown",
|
|
25
|
+
event => {
|
|
26
|
+
if (!event.isTrusted) return;
|
|
27
|
+
|
|
28
|
+
const focus = document.getSelection()?.focusNode;
|
|
29
|
+
const closestElement = focus?.parentElement?.closest(Array.from(enterCreatesNewSelectors).join(","));
|
|
30
|
+
const insideHint = !!focus?.parentElement?.closest("webwriter-quiz-hint");
|
|
31
|
+
|
|
32
|
+
if (closestElement && !insideHint && event.key === "Enter") {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
event.stopImmediatePropagation();
|
|
35
|
+
|
|
36
|
+
const modKey = !event.shiftKey;
|
|
37
|
+
const ctrlEnterEvent = new KeyboardEvent("keydown", {
|
|
38
|
+
key: "Enter",
|
|
39
|
+
metaKey: modKey && isMac(),
|
|
40
|
+
ctrlKey: modKey && !isMac(),
|
|
41
|
+
});
|
|
42
|
+
document.body.dispatchEvent(ctrlEnterEvent);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
true,
|
|
46
|
+
);
|
|
47
|
+
enterCreatesNewInitialized = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RelativeSelectionEndpoint {
|
|
52
|
+
path: number[];
|
|
53
|
+
offset: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RelativeSelection {
|
|
57
|
+
anchor: RelativeSelectionEndpoint;
|
|
58
|
+
focus: RelativeSelectionEndpoint;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serializes the current selection relative to `root`
|
|
63
|
+
* @returns the serialized selection, or `undefined` if there is no selection or if the selection is not fully contained in `root`
|
|
64
|
+
*/
|
|
65
|
+
export function serializeRelativeSelection(root: Node): RelativeSelection | undefined {
|
|
66
|
+
const selection = root.ownerDocument?.getSelection();
|
|
67
|
+
if (!selection?.anchorNode || !selection.focusNode) return;
|
|
68
|
+
|
|
69
|
+
const serialize = (node: Node, offset: number): RelativeSelectionEndpoint | undefined => {
|
|
70
|
+
const path = [];
|
|
71
|
+
while (node !== root) {
|
|
72
|
+
const parent = node.parentNode;
|
|
73
|
+
if (!parent) return;
|
|
74
|
+
path.unshift(Array.prototype.indexOf.call(parent.childNodes, node));
|
|
75
|
+
node = parent;
|
|
76
|
+
}
|
|
77
|
+
return { path, offset };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const anchor = serialize(selection.anchorNode, selection.anchorOffset);
|
|
81
|
+
const focus = serialize(selection.focusNode, selection.focusOffset);
|
|
82
|
+
if (anchor && focus) return { anchor, focus };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Restores a selection previously serialized with `serializeRelativeSelection` on the given `root` element.
|
|
87
|
+
* @returns true if the selection was successfully restored
|
|
88
|
+
*/
|
|
89
|
+
export function restoreRelativeSelection(root: Node, relative: RelativeSelection): boolean {
|
|
90
|
+
const resolve = ({ path, offset }: RelativeSelectionEndpoint) => {
|
|
91
|
+
let node = root;
|
|
92
|
+
for (const index of path) {
|
|
93
|
+
node = node.childNodes[index];
|
|
94
|
+
if (!node) return;
|
|
95
|
+
}
|
|
96
|
+
const maxOffset =
|
|
97
|
+
node.nodeType === Node.TEXT_NODE || node.nodeType === Node.COMMENT_NODE
|
|
98
|
+
? (node.textContent?.length ?? 0)
|
|
99
|
+
: node.childNodes.length;
|
|
100
|
+
return offset >= 0 && offset <= maxOffset ? { node, offset } : undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const anchor = resolve(relative.anchor);
|
|
104
|
+
const focus = resolve(relative.focus);
|
|
105
|
+
const selection = root.ownerDocument?.getSelection();
|
|
106
|
+
if (!anchor || !focus || !selection) return false;
|
|
107
|
+
|
|
108
|
+
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Runs a DOM mutation and restores the selection in the replacement element with the same ID.
|
|
114
|
+
* This overrides ProseMirror's behavior of trying to select the old "same" position from the old document
|
|
115
|
+
*/
|
|
116
|
+
export function mutateWithStableSelection(root: HTMLElement, mutator: () => void) {
|
|
117
|
+
const savedSelection = serializeRelativeSelection(root);
|
|
118
|
+
mutator();
|
|
119
|
+
const newRoot = document.getElementById(root.id);
|
|
120
|
+
if (newRoot && savedSelection) restoreRelativeSelection(newRoot, savedSelection);
|
|
121
|
+
}
|