@barocss/math-editor 0.2.0 → 0.4.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/ADAPTERS.md +25 -14
- package/API-JAVASCRIPT.md +1 -1
- package/API-REACT.md +5 -1
- package/API-SESSION.md +168 -6
- package/API-SOLID.md +1 -1
- package/API-SVELTE.md +1 -1
- package/API-VUE.md +1 -1
- package/API-WEB-COMPONENT.md +1 -1
- package/CHANGELOG.md +51 -1
- package/EMBEDDING.md +4 -0
- package/IMPLEMENTATION.md +27 -2
- package/JSON-MODEL.md +1 -0
- package/LATEX-GUIDE.md +13 -0
- package/LATEX-SCOPE.md +2 -2
- package/LICENSE +1 -3
- package/README.md +150 -36
- package/ROADMAP.md +96 -10
- package/STYLING.md +157 -0
- package/SUPPORT.md +29 -11
- package/VALIDATION.md +117 -4
- package/dist/core.d.ts +5 -1
- package/dist/core.js +5 -1
- package/dist/document-codec.d.ts +0 -1
- package/dist/document-codec.js +0 -1
- package/dist/dom/caret-geometry.d.ts +3 -0
- package/dist/dom/caret-geometry.js +41 -0
- package/dist/dom/elements.d.ts +1 -2
- package/dist/dom/elements.js +2 -3
- package/dist/dom/latex-panel.d.ts +10 -0
- package/dist/dom/latex-panel.js +59 -0
- package/dist/dom/matrix-selection.d.ts +11 -0
- package/dist/dom/matrix-selection.js +30 -0
- package/dist/dom/menu-position.d.ts +14 -2
- package/dist/dom/menu-position.js +84 -15
- package/dist/dom/presentation-controls.d.ts +4 -0
- package/dist/dom/presentation-controls.js +56 -0
- package/dist/dom/quick-panel.d.ts +12 -0
- package/dist/dom/quick-panel.js +102 -0
- package/dist/dom/selection.d.ts +29 -0
- package/dist/dom/selection.js +53 -0
- package/dist/dom/symbol-panel.d.ts +9 -0
- package/dist/dom/symbol-panel.js +146 -0
- package/dist/dom/theme.d.ts +6 -0
- package/dist/dom/theme.js +68 -0
- package/dist/dom/toolbar-catalog.d.ts +10 -0
- package/dist/dom/toolbar-catalog.js +35 -0
- package/dist/dom/toolbar.d.ts +13 -2
- package/dist/dom/toolbar.js +295 -28
- package/dist/dom.d.ts +7 -1
- package/dist/dom.js +527 -126
- package/dist/editor-labels.d.ts +0 -1
- package/dist/editor-labels.js +0 -1
- package/dist/enter-policy.d.ts +0 -1
- package/dist/enter-policy.js +0 -1
- package/dist/fences.d.ts +0 -1
- package/dist/fences.js +0 -1
- package/dist/i18n.d.ts +0 -1
- package/dist/i18n.js +0 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/latex-insertion.d.ts +12 -0
- package/dist/latex-insertion.js +26 -0
- package/dist/latex-paste-panel.d.ts +3 -0
- package/dist/latex-paste-panel.js +18 -0
- package/dist/latex.d.ts +0 -1
- package/dist/latex.js +0 -1
- package/dist/lines.d.ts +0 -1
- package/dist/lines.js +0 -1
- package/dist/locales/en.js +28 -2
- package/dist/locales/en.json +28 -2
- package/dist/locales/ko.js +28 -2
- package/dist/locales/ko.json +28 -2
- package/dist/math-editor-toolbar.d.ts +4 -2
- package/dist/math-editor-toolbar.js +3 -4
- package/dist/math-editor.d.ts +5 -2
- package/dist/math-editor.js +300 -44
- package/dist/matrix-range.d.ts +40 -0
- package/dist/matrix-range.js +242 -0
- package/dist/matrix.d.ts +0 -1
- package/dist/matrix.js +0 -1
- package/dist/model.d.ts +0 -1
- package/dist/model.js +0 -1
- package/dist/outputs.d.ts +0 -1
- package/dist/outputs.js +0 -1
- package/dist/preferences.d.ts +24 -0
- package/dist/preferences.js +66 -0
- package/dist/presentation-controls.d.ts +10 -0
- package/dist/presentation-controls.js +10 -0
- package/dist/presentation.d.ts +13 -0
- package/dist/presentation.js +86 -0
- package/dist/quick-panel.d.ts +2 -0
- package/dist/quick-panel.js +17 -0
- package/dist/range.d.ts +4 -1
- package/dist/range.js +57 -1
- package/dist/react.d.ts +0 -1
- package/dist/react.js +0 -1
- package/dist/session.d.ts +31 -2
- package/dist/session.js +108 -7
- package/dist/solid.d.ts +0 -1
- package/dist/solid.js +0 -1
- package/dist/suggestions.d.ts +4 -1
- package/dist/suggestions.js +103 -13
- package/dist/svelte.d.ts +0 -1
- package/dist/svelte.js +0 -1
- package/dist/symbol-browser.d.ts +0 -1
- package/dist/symbol-browser.js +0 -1
- package/dist/symbols.d.ts +0 -1
- package/dist/symbols.js +0 -1
- package/dist/templates.d.ts +0 -1
- package/dist/templates.js +0 -1
- package/dist/tokens.d.ts +0 -1
- package/dist/tokens.js +0 -1
- package/dist/vertical-navigation.d.ts +15 -0
- package/dist/vertical-navigation.js +145 -0
- package/dist/vue.d.ts +0 -1
- package/dist/vue.js +0 -1
- package/dist/web-component.d.ts +0 -1
- package/dist/web-component.js +0 -1
- package/package.json +1 -1
- package/src/style.css +254 -129
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js.map +0 -1
- package/dist/document-codec.d.ts.map +0 -1
- package/dist/document-codec.js.map +0 -1
- package/dist/dom/elements.d.ts.map +0 -1
- package/dist/dom/elements.js.map +0 -1
- package/dist/dom/menu-position.d.ts.map +0 -1
- package/dist/dom/menu-position.js.map +0 -1
- package/dist/dom/toolbar.d.ts.map +0 -1
- package/dist/dom/toolbar.js.map +0 -1
- package/dist/dom.d.ts.map +0 -1
- package/dist/dom.js.map +0 -1
- package/dist/editor-labels.d.ts.map +0 -1
- package/dist/editor-labels.js.map +0 -1
- package/dist/enter-policy.d.ts.map +0 -1
- package/dist/enter-policy.js.map +0 -1
- package/dist/fences.d.ts.map +0 -1
- package/dist/fences.js.map +0 -1
- package/dist/i18n.d.ts.map +0 -1
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/latex.d.ts.map +0 -1
- package/dist/latex.js.map +0 -1
- package/dist/lines.d.ts.map +0 -1
- package/dist/lines.js.map +0 -1
- package/dist/math-editor-toolbar.d.ts.map +0 -1
- package/dist/math-editor-toolbar.js.map +0 -1
- package/dist/math-editor.d.ts.map +0 -1
- package/dist/math-editor.js.map +0 -1
- package/dist/matrix.d.ts.map +0 -1
- package/dist/matrix.js.map +0 -1
- package/dist/model.d.ts.map +0 -1
- package/dist/model.js.map +0 -1
- package/dist/outputs.d.ts.map +0 -1
- package/dist/outputs.js.map +0 -1
- package/dist/range.d.ts.map +0 -1
- package/dist/range.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/solid.d.ts.map +0 -1
- package/dist/solid.js.map +0 -1
- package/dist/suggestions.d.ts.map +0 -1
- package/dist/suggestions.js.map +0 -1
- package/dist/svelte.d.ts.map +0 -1
- package/dist/svelte.js.map +0 -1
- package/dist/symbol-browser.d.ts.map +0 -1
- package/dist/symbol-browser.js.map +0 -1
- package/dist/symbols.d.ts.map +0 -1
- package/dist/symbols.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/tokens.d.ts.map +0 -1
- package/dist/tokens.js.map +0 -1
- package/dist/vue.d.ts.map +0 -1
- package/dist/vue.js.map +0 -1
- package/dist/web-component.d.ts.map +0 -1
- package/dist/web-component.js.map +0 -1
package/dist/dom.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
|
+
import { mountLatexPanel } from './dom/latex-panel.js';
|
|
2
|
+
import { followMathTheme, syncMathTheme } from './dom/theme.js';
|
|
3
|
+
import { matrixPointerRange } from './dom/matrix-selection.js';
|
|
1
4
|
import { positionMathMenu } from './dom/menu-position.js';
|
|
2
5
|
import { fences } from './fences.js';
|
|
3
6
|
import { createEditorLabels } from './editor-labels.js';
|
|
4
7
|
import { mathEnterAction } from './enter-policy.js';
|
|
5
8
|
export { mountMathLatex, mountMathPreview, } from './outputs.js';
|
|
6
|
-
import {
|
|
9
|
+
import { elementFactory } from './dom/elements.js';
|
|
7
10
|
import { mountMathToolbar } from './dom/toolbar.js';
|
|
8
11
|
export { mountMathToolbar } from './dom/toolbar.js';
|
|
9
12
|
import { createMathSession, } from './session.js';
|
|
10
13
|
import { translate, mathLocaleDirection } from './i18n.js';
|
|
11
14
|
import { documentRows, isLiteralText, textNodes, setText, moveCaret, unwrapPrevious, gridDeletionTarget, removeGrid, isGrid, toLatex, } from './model.js';
|
|
12
|
-
import { activeGrid,
|
|
13
|
-
import {
|
|
15
|
+
import { activeGrid, activeMatrix, resizeMatrix } from './matrix.js';
|
|
16
|
+
import { MATH_MATRIX_CLIPBOARD_TYPE, parseMatrixFragment, matrixFragment, matrixFromTSV, matrixRangeBetween, resolveMatrixRange, extendMatrixRange, focusMatrixCell, } from './matrix-range.js';
|
|
17
|
+
import { joinPreviousLine } from './lines.js';
|
|
18
|
+
import { moveVertical } from './vertical-navigation.js';
|
|
19
|
+
import { renderedCaretGeometry } from './dom/caret-geometry.js';
|
|
20
|
+
import { extendsMathSelection, nativeSelectionAnchor, createSelectionMenuState, textSelectionBounds, } from './dom/selection.js';
|
|
14
21
|
import { acceptSuggestion, findSuggestions, mathStructures } from './suggestions.js';
|
|
15
|
-
import { tokenizeMathText } from './tokens.js';
|
|
16
|
-
import { fragmentLatex, parseFragment, MATH_CLIPBOARD_TYPE, resolveRange, copyRange, wrappingKinds, } from './range.js';
|
|
22
|
+
import { tokenizeMathText, tokenIndexAt } from './tokens.js';
|
|
23
|
+
import { fragmentLatex, parseFragment, MATH_CLIPBOARD_TYPE, extendKeyboardRange, collapseKeyboardRange, resolveRange, copyRange, wrappingKinds, } from './range.js';
|
|
17
24
|
/** Native DOM renderer. No React or framework runtime is imported by this module. */
|
|
18
25
|
export function mountMathEditor(host, initial = {}) {
|
|
26
|
+
const doc = host.ownerDocument;
|
|
27
|
+
const win = doc.defaultView;
|
|
28
|
+
const element = elementFactory(doc);
|
|
19
29
|
let options = { ...initial };
|
|
20
30
|
const session = initial.session ??
|
|
21
31
|
createMathSession({
|
|
22
32
|
document: initial.defaultValue,
|
|
33
|
+
preferences: initial.preferences,
|
|
23
34
|
locale: initial.locale,
|
|
24
35
|
mode: initial.mode,
|
|
25
36
|
});
|
|
@@ -34,12 +45,40 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
34
45
|
status.setAttribute('role', 'status');
|
|
35
46
|
const menu = element('div', 'me-suggestion-panel me-dom-menu');
|
|
36
47
|
menu.hidden = true;
|
|
37
|
-
(initial.menuHost ?? host.closest('dialog, [role="dialog"]') ??
|
|
48
|
+
(initial.menuHost ?? host.closest('dialog, [role="dialog"]') ?? doc.body).append(menu);
|
|
49
|
+
const stopTheme = followMathTheme(root, menu);
|
|
38
50
|
let editing = false, rendering = false, composing = false, destroyed = false, dismissed = '', operandMenuKey = '', candidateIndex = 0, selectedGrid;
|
|
39
51
|
let drag;
|
|
52
|
+
let dragMatrix;
|
|
53
|
+
const selectionMenu = createSelectionMenuState();
|
|
40
54
|
let toolbar;
|
|
55
|
+
let latexPanel;
|
|
56
|
+
const closeLatex = () => {
|
|
57
|
+
latexPanel?.destroy();
|
|
58
|
+
latexPanel = undefined;
|
|
59
|
+
editing = true;
|
|
60
|
+
render();
|
|
61
|
+
};
|
|
62
|
+
const openLatex = () => {
|
|
63
|
+
if (composing || destroyed || latexPanel)
|
|
64
|
+
return;
|
|
65
|
+
toolbar?.closePanels();
|
|
66
|
+
menu.hidden = true;
|
|
67
|
+
latexPanel = mountLatexPanel(root, {
|
|
68
|
+
locale: session.getSnapshot().locale,
|
|
69
|
+
onInsert: (source) => session.pasteLatex(source),
|
|
70
|
+
onClose: closeLatex,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
41
73
|
const t = (value) => translate(session.getSnapshot().locale, value);
|
|
42
74
|
const currentInput = () => surface.querySelector('input');
|
|
75
|
+
const cancelDrag = () => {
|
|
76
|
+
const previous = drag;
|
|
77
|
+
drag = undefined;
|
|
78
|
+
dragMatrix = undefined;
|
|
79
|
+
if (previous && surface.hasPointerCapture(previous.pointerId))
|
|
80
|
+
surface.releasePointerCapture(previous.pointerId);
|
|
81
|
+
};
|
|
43
82
|
const notify = (key) => {
|
|
44
83
|
status.textContent = t(key);
|
|
45
84
|
};
|
|
@@ -61,6 +100,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
61
100
|
if (!editing ||
|
|
62
101
|
isLiteralText(state.document, state.caret.id) ||
|
|
63
102
|
snapshot.range ||
|
|
103
|
+
snapshot.matrixRange ||
|
|
64
104
|
dismissed === `${active.id}:${active.text}:${state.caret.start}`)
|
|
65
105
|
result.candidates = [];
|
|
66
106
|
return result;
|
|
@@ -73,38 +113,94 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
73
113
|
dismissed = '';
|
|
74
114
|
candidateIndex = 0;
|
|
75
115
|
apply(acceptSuggestion(session.getSnapshot().state, result.query, item));
|
|
116
|
+
session.preferences.record(item.id);
|
|
76
117
|
}
|
|
77
118
|
};
|
|
78
|
-
const
|
|
119
|
+
const suggestionButton = (glyph, label, detail) => {
|
|
120
|
+
const button = element('button');
|
|
121
|
+
button.type = 'button';
|
|
122
|
+
button.setAttribute('role', 'option');
|
|
123
|
+
// Keep the concise option name while showing the same detail as the rich UI.
|
|
124
|
+
button.setAttribute('aria-label', `${glyph} ${label}`);
|
|
125
|
+
const copy = element('span', 'me-candidate-copy');
|
|
126
|
+
copy.append(element('strong', '', label), element('small', '', detail));
|
|
127
|
+
button.append(element('span', 'me-candidate-glyph', glyph), copy);
|
|
128
|
+
button.onmousedown = (event) => event.preventDefault();
|
|
129
|
+
button.onmousemove = (event) => {
|
|
130
|
+
// Rebuilding or scrolling the list can put a new row under a stationary
|
|
131
|
+
// pointer. Only real pointer movement may replace the keyboard choice.
|
|
132
|
+
if (composing || button.disabled || (!event.movementX && !event.movementY))
|
|
133
|
+
return;
|
|
134
|
+
const buttons = Array.from(button.parentElement.children);
|
|
135
|
+
const index = buttons.indexOf(button);
|
|
136
|
+
if (candidateIndex === index && button.getAttribute('aria-selected') === 'true')
|
|
137
|
+
return;
|
|
138
|
+
candidateIndex = index;
|
|
139
|
+
// Update the highlight in place: rebuilding would reset a wheel-scrolled list.
|
|
140
|
+
for (const option of buttons)
|
|
141
|
+
option.setAttribute('aria-selected', String(option === button));
|
|
142
|
+
// Host observers also see these attribute updates and must retain the input caret.
|
|
143
|
+
options.onRender?.();
|
|
144
|
+
};
|
|
145
|
+
return button;
|
|
146
|
+
};
|
|
147
|
+
const positionMenu = (anchor) => {
|
|
148
|
+
delete menu.dataset.compact;
|
|
149
|
+
positionMathMenu(menu, anchor, 340, options.menuAvoidElements?.());
|
|
150
|
+
// In a short dialog, leave the available space for selectable rows.
|
|
151
|
+
if (menu.getBoundingClientRect().height < 180) {
|
|
152
|
+
menu.dataset.compact = 'true';
|
|
153
|
+
positionMathMenu(menu, anchor, 340, options.menuAvoidElements?.());
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const scrollMenuSelection = () => {
|
|
157
|
+
const option = menu.querySelector('[aria-selected="true"]');
|
|
158
|
+
const list = option?.parentElement;
|
|
159
|
+
if (!option || !list)
|
|
160
|
+
return;
|
|
161
|
+
// Move only the options list, never the surrounding page or host editor.
|
|
162
|
+
const itemRect = option.getBoundingClientRect();
|
|
163
|
+
const listRect = list.getBoundingClientRect();
|
|
164
|
+
const scaleY = listRect.height / list.offsetHeight || 1;
|
|
165
|
+
if (itemRect.top < listRect.top)
|
|
166
|
+
list.scrollTop -= (listRect.top - itemRect.top) / scaleY;
|
|
167
|
+
else if (itemRect.bottom > listRect.bottom)
|
|
168
|
+
list.scrollTop += (itemRect.bottom - listRect.bottom) / scaleY;
|
|
169
|
+
};
|
|
170
|
+
const renderMenu = () => {
|
|
171
|
+
syncMathTheme(root, menu);
|
|
79
172
|
const result = candidates(), input = currentInput();
|
|
80
173
|
menu.replaceChildren();
|
|
81
174
|
const snapshot = session.getSnapshot();
|
|
82
175
|
const caret = snapshot.state.caret;
|
|
83
176
|
const selectedRange = snapshot.range ??
|
|
84
|
-
(caret.start !== caret.end
|
|
177
|
+
(input && caret.start !== caret.end
|
|
85
178
|
? {
|
|
86
179
|
anchor: { id: caret.id, offset: caret.start },
|
|
87
180
|
focus: { id: caret.id, offset: caret.end },
|
|
88
181
|
}
|
|
89
182
|
: undefined);
|
|
90
183
|
const fragment = selectedRange && copyRange(snapshot.state.document, selectedRange);
|
|
184
|
+
const showSelectionMenu = selectionMenu.sync(fragment ? selectedRange : undefined);
|
|
91
185
|
if (fragment) {
|
|
92
|
-
menu.
|
|
186
|
+
menu.dataset.kind = 'selection';
|
|
187
|
+
menu.hidden = !showSelectionMenu;
|
|
188
|
+
input?.setAttribute('aria-expanded', String(showSelectionMenu));
|
|
189
|
+
if (!showSelectionMenu)
|
|
190
|
+
return;
|
|
93
191
|
menu.lang = snapshot.locale;
|
|
94
192
|
menu.dir = mathLocaleDirection(snapshot.locale);
|
|
193
|
+
const canWrap = fragment.rows.length === 1 &&
|
|
194
|
+
!isLiteralText(snapshot.state.document, selectedRange.anchor.id);
|
|
195
|
+
menu.append(element('div', 'me-suggestion-heading', t(canWrap ? 'selection.highlighted' : 'selection.singleRowRequired')));
|
|
95
196
|
const list = element('div', 'me-suggestions');
|
|
96
197
|
list.setAttribute('role', 'listbox');
|
|
97
198
|
list.setAttribute('aria-label', t('selection.toolbar'));
|
|
98
199
|
for (const item of mathStructures.filter((item) => wrappingKinds.includes(item.kind))) {
|
|
99
|
-
const button =
|
|
100
|
-
button.type = 'button';
|
|
101
|
-
button.setAttribute('role', 'option');
|
|
200
|
+
const button = suggestionButton(item.glyph, t(item.label), t(item.detail));
|
|
102
201
|
button.setAttribute('aria-selected', String(list.children.length === candidateIndex));
|
|
103
|
-
button.disabled =
|
|
104
|
-
fragment.rows.length !== 1 ||
|
|
105
|
-
isLiteralText(snapshot.state.document, selectedRange.anchor.id);
|
|
202
|
+
button.disabled = !canWrap;
|
|
106
203
|
button.setAttribute('aria-label', translate(snapshot.locale, 'selection.wrap', { kind: t(item.label) }));
|
|
107
|
-
button.onmousedown = (event) => event.preventDefault();
|
|
108
204
|
button.onclick = () => {
|
|
109
205
|
if (!snapshot.range)
|
|
110
206
|
session.selectRange(selectedRange);
|
|
@@ -113,39 +209,65 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
113
209
|
list.append(button);
|
|
114
210
|
}
|
|
115
211
|
menu.append(list);
|
|
116
|
-
const selected = surface.querySelectorAll('.me-dom-selected');
|
|
212
|
+
const selected = surface.querySelectorAll('.me-dom-selected, .me-range-highlight');
|
|
117
213
|
const rect = (selected[selected.length - 1] ?? input ?? surface).getBoundingClientRect();
|
|
118
|
-
|
|
214
|
+
positionMenu(rect);
|
|
119
215
|
return;
|
|
120
216
|
}
|
|
217
|
+
menu.dataset.kind = 'suggestions';
|
|
121
218
|
menu.hidden = !input || !result.candidates.length;
|
|
122
219
|
input?.setAttribute('aria-expanded', String(!menu.hidden));
|
|
123
220
|
if (menu.hidden)
|
|
124
221
|
return;
|
|
125
222
|
menu.lang = session.getSnapshot().locale;
|
|
126
223
|
menu.dir = mathLocaleDirection(menu.lang);
|
|
224
|
+
const heading = element('div', 'me-suggestion-heading');
|
|
225
|
+
heading.append(element('span', 'me-suggestion-query', translate(snapshot.locale, 'suggestion.create', { query: result.query })), element('span', '', t(composing ? 'suggestion.composing' : 'suggestion.keyboardHint')));
|
|
127
226
|
const list = element('div', 'me-suggestions');
|
|
128
227
|
list.setAttribute('role', 'listbox');
|
|
129
228
|
list.setAttribute('aria-label', t('suggestion.label'));
|
|
130
229
|
candidateIndex = Math.min(candidateIndex, result.candidates.length - 1);
|
|
131
230
|
result.candidates.forEach((item, index) => {
|
|
132
|
-
const button =
|
|
133
|
-
button.
|
|
134
|
-
button.setAttribute('role', 'option');
|
|
135
|
-
button.setAttribute('aria-selected', String(index === candidateIndex));
|
|
231
|
+
const button = suggestionButton(item.glyph, item.label, item.detail);
|
|
232
|
+
button.setAttribute('aria-selected', String(!composing && index === candidateIndex));
|
|
136
233
|
button.disabled = composing;
|
|
137
|
-
button.onmousedown = (event) => event.preventDefault();
|
|
138
234
|
button.onclick = () => choose(index);
|
|
139
235
|
list.append(button);
|
|
140
236
|
});
|
|
141
|
-
menu.append(list);
|
|
237
|
+
menu.append(heading, list, element('div', 'me-suggestion-footer', t(composing ? 'suggestion.imeHint' : 'suggestion.keepSource')));
|
|
142
238
|
const rect = input.getBoundingClientRect();
|
|
143
|
-
|
|
239
|
+
positionMenu(rect);
|
|
144
240
|
input.setAttribute('aria-expanded', 'true');
|
|
145
241
|
};
|
|
242
|
+
const drawMenu = () => {
|
|
243
|
+
renderMenu();
|
|
244
|
+
// Keyup and selection events can redraw the list after keydown has scrolled it.
|
|
245
|
+
if (!menu.hidden)
|
|
246
|
+
scrollMenuSelection();
|
|
247
|
+
// Focus-only renders and native selection updates need the same notification
|
|
248
|
+
// as model edits, before a host's mutation observer restores its own selection.
|
|
249
|
+
if (!destroyed && !composing && !rendering)
|
|
250
|
+
options.onRender?.();
|
|
251
|
+
};
|
|
252
|
+
const dismissSelectionMenu = () => {
|
|
253
|
+
if (menu.hidden || menu.dataset.kind !== 'selection' || !selectionMenu.dismiss())
|
|
254
|
+
return false;
|
|
255
|
+
drawMenu();
|
|
256
|
+
return true;
|
|
257
|
+
};
|
|
258
|
+
menu.onkeydown = (event) => {
|
|
259
|
+
if (event.key !== 'Escape' || composing || event.isComposing || !dismissSelectionMenu())
|
|
260
|
+
return;
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
event.stopPropagation();
|
|
263
|
+
(currentInput() ?? surface).focus();
|
|
264
|
+
};
|
|
146
265
|
const render = () => {
|
|
147
266
|
if (destroyed || composing || rendering)
|
|
148
267
|
return;
|
|
268
|
+
// External caret/configuration changes replace the input and end its pending native drag.
|
|
269
|
+
if (drag?.input && !drag.moved)
|
|
270
|
+
cancelDrag();
|
|
149
271
|
rendering = true;
|
|
150
272
|
const snapshot = session.getSnapshot(), { state } = snapshot;
|
|
151
273
|
root.dataset.mode = snapshot.mode;
|
|
@@ -156,6 +278,14 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
156
278
|
surface.setAttribute('aria-label', t('selection.surface'));
|
|
157
279
|
surface.replaceChildren();
|
|
158
280
|
const spans = snapshot.range && resolveRange(state.document, snapshot.range);
|
|
281
|
+
const matrixSelection = snapshot.matrixRange && resolveMatrixRange(state.document, snapshot.matrixRange);
|
|
282
|
+
if (matrixSelection)
|
|
283
|
+
status.textContent = translate(snapshot.locale, 'matrix.rangeHint', {
|
|
284
|
+
rows: matrixSelection.rows,
|
|
285
|
+
columns: matrixSelection.columns,
|
|
286
|
+
});
|
|
287
|
+
else
|
|
288
|
+
status.textContent = '';
|
|
159
289
|
const selected = (r) => spans?.find((span) => span.row.id === r.id);
|
|
160
290
|
const labels = createEditorLabels(snapshot.locale);
|
|
161
291
|
const renderRow = (r, name, inheritedSelection = false) => {
|
|
@@ -164,82 +294,111 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
164
294
|
let offset = 0;
|
|
165
295
|
for (const child of r.children) {
|
|
166
296
|
const length = child.type === 'text' ? child.text.length : 1;
|
|
297
|
+
const textSelection = child.type === 'text'
|
|
298
|
+
? textSelectionBounds(length, offset, selection, inheritedSelection)
|
|
299
|
+
: undefined;
|
|
167
300
|
const highlighted = inheritedSelection ||
|
|
168
301
|
(!!selection && offset < selection.end && offset + length > selection.start);
|
|
169
302
|
offset += length;
|
|
170
303
|
if (child.type === 'text') {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
304
|
+
const parts = isLiteralText(state.document, child.id)
|
|
305
|
+
? [{ text: child.text, start: 0, end: child.text.length, kind: 'draft' }]
|
|
306
|
+
: tokenizeMathText(child.text);
|
|
307
|
+
const activeIndex = tokenIndexAt(parts, state.caret.start, state.caret.affinity);
|
|
308
|
+
for (const [index, token] of parts.entries()) {
|
|
309
|
+
const boundary = !token.text && r.children.length > 1;
|
|
310
|
+
const run = element('span', `me-run${token.text ? '' : ' me-empty'}${boundary ? ' me-empty-boundary' : ''}`);
|
|
311
|
+
run.dataset.runId = child.id;
|
|
312
|
+
run.dataset.value = token.text;
|
|
313
|
+
run.dataset.tokenKind = token.kind;
|
|
314
|
+
run.dataset.tokenStart = String(token.start);
|
|
315
|
+
run.dataset.tokenEnd = String(token.end);
|
|
316
|
+
const mirror = element('span', 'me-measure', token.text || (boundary ? '' : '□'));
|
|
317
|
+
mirror.setAttribute('aria-hidden', 'true');
|
|
318
|
+
run.append(mirror);
|
|
319
|
+
const active = editing &&
|
|
320
|
+
!snapshot.range &&
|
|
321
|
+
!snapshot.matrixRange &&
|
|
322
|
+
state.caret.id === child.id &&
|
|
323
|
+
index === activeIndex;
|
|
324
|
+
if (active) {
|
|
325
|
+
const input = element('input', 'me-input');
|
|
326
|
+
input.value = token.text;
|
|
327
|
+
input.placeholder = boundary ? '' : '□';
|
|
328
|
+
input.dataset.tokenStart = String(token.start);
|
|
329
|
+
input.dataset.tokenEnd = String(token.end);
|
|
330
|
+
input.setAttribute('aria-label', name);
|
|
331
|
+
input.setAttribute('role', 'combobox');
|
|
332
|
+
input.setAttribute('aria-expanded', 'false');
|
|
333
|
+
input.autocomplete = 'off';
|
|
334
|
+
input.spellcheck = false;
|
|
335
|
+
const syncCaret = () => {
|
|
336
|
+
if (input.isConnected && !rendering && !composing) {
|
|
337
|
+
rendering = true;
|
|
338
|
+
session.select({
|
|
339
|
+
id: child.id,
|
|
340
|
+
start: token.start + (input.selectionStart ?? 0),
|
|
341
|
+
end: token.start + (input.selectionEnd ?? 0),
|
|
342
|
+
affinity: input.selectionStart === 0 ? 'forward' : 'backward',
|
|
343
|
+
});
|
|
344
|
+
rendering = false;
|
|
345
|
+
drawMenu();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const replaceToken = () => {
|
|
349
|
+
const current = session.getSnapshot().state;
|
|
350
|
+
const text = textNodes(current.document).find((node) => node.id === child.id).text;
|
|
351
|
+
return setText(current, text.slice(0, token.start) + input.value + text.slice(token.end), token.start + (input.selectionStart ?? 0), token.start + (input.selectionEnd ?? 0));
|
|
352
|
+
};
|
|
353
|
+
input.onselect = syncCaret;
|
|
354
|
+
input.onkeyup = syncCaret;
|
|
355
|
+
input.oninput = () => {
|
|
356
|
+
if (composing) {
|
|
357
|
+
// Do not split or replace this input before the browser commits its preedit.
|
|
358
|
+
mirror.textContent = input.value || '□';
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
dismissed = '';
|
|
362
|
+
candidateIndex = 0;
|
|
363
|
+
apply(replaceToken());
|
|
364
|
+
};
|
|
365
|
+
input.addEventListener('compositionstart', () => {
|
|
366
|
+
cancelDrag();
|
|
367
|
+
composing = true;
|
|
368
|
+
toolbar?.setDisabled(true);
|
|
369
|
+
menu.hidden = true;
|
|
370
|
+
});
|
|
371
|
+
input.addEventListener('compositionend', () => {
|
|
372
|
+
composing = false;
|
|
373
|
+
toolbar?.setDisabled(false);
|
|
374
|
+
dismissed = '';
|
|
375
|
+
apply(replaceToken());
|
|
376
|
+
});
|
|
377
|
+
input.onkeydown = keydown;
|
|
378
|
+
run.append(input);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const preview = element('span', 'me-preview-token');
|
|
382
|
+
preview.setAttribute('aria-label', name);
|
|
383
|
+
preview.setAttribute('role', 'button');
|
|
384
|
+
preview.tabIndex = -1;
|
|
385
|
+
const selected = textSelectionBounds(token.text.length, token.start, textSelection);
|
|
386
|
+
if (selected) {
|
|
387
|
+
preview.append(doc.createTextNode(token.text.slice(0, selected.start)), element('mark', 'me-range-highlight', token.text.slice(selected.start, selected.end)), doc.createTextNode(token.text.slice(selected.end)));
|
|
388
|
+
run.dataset.rangeSelected = 'true';
|
|
206
389
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
apply(next);
|
|
211
|
-
};
|
|
212
|
-
input.addEventListener('compositionstart', () => {
|
|
213
|
-
composing = true;
|
|
214
|
-
menu.hidden = true;
|
|
215
|
-
});
|
|
216
|
-
input.addEventListener('compositionend', () => {
|
|
217
|
-
composing = false;
|
|
218
|
-
dismissed = '';
|
|
219
|
-
apply(setText(session.getSnapshot().state, input.value, input.selectionStart ?? 0, input.selectionEnd ?? 0));
|
|
220
|
-
});
|
|
221
|
-
input.onkeydown = keydown;
|
|
222
|
-
run.append(input);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
const preview = element('span', 'me-preview-token');
|
|
226
|
-
preview.setAttribute('aria-label', name);
|
|
227
|
-
preview.setAttribute('role', 'button');
|
|
228
|
-
preview.tabIndex = -1;
|
|
229
|
-
for (const token of tokenizeMathText(child.text)) {
|
|
230
|
-
const part = element('span', `me-dom-${token.kind}`, token.text || (boundary ? '' : '□'));
|
|
231
|
-
preview.append(part);
|
|
390
|
+
else
|
|
391
|
+
preview.textContent = token.text || (boundary ? '' : '□');
|
|
392
|
+
run.append(preview);
|
|
232
393
|
}
|
|
233
|
-
|
|
234
|
-
run.classList.add('me-dom-selected');
|
|
235
|
-
run.append(preview);
|
|
394
|
+
view.append(run);
|
|
236
395
|
}
|
|
237
|
-
view.append(run);
|
|
238
396
|
continue;
|
|
239
397
|
}
|
|
240
398
|
if (isGrid(child)) {
|
|
241
399
|
const grid = element('span', 'me-matrix');
|
|
242
400
|
grid.dataset.structure = child.type;
|
|
401
|
+
grid.dataset.matrixId = child.id;
|
|
243
402
|
grid.dataset.environment =
|
|
244
403
|
child.type === 'matrix'
|
|
245
404
|
? child.environment
|
|
@@ -252,6 +411,10 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
252
411
|
cells.style.gridTemplateColumns = `repeat(${child.columns},max-content)`;
|
|
253
412
|
child.slots.forEach((slot, i) => {
|
|
254
413
|
const cell = element('span', 'me-matrix-cell');
|
|
414
|
+
cell.dataset.cellIndex = String(i);
|
|
415
|
+
if (matrixSelection?.matrix.id === child.id && matrixSelection.indices.includes(i)) {
|
|
416
|
+
cell.dataset.cellSelected = 'true';
|
|
417
|
+
}
|
|
255
418
|
cell.append(renderRow(slot, labels.grid(name, child.type, i, child.columns), highlighted));
|
|
256
419
|
cells.append(cell);
|
|
257
420
|
});
|
|
@@ -341,15 +504,88 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
341
504
|
const input = currentInput();
|
|
342
505
|
if (input) {
|
|
343
506
|
input.focus();
|
|
344
|
-
|
|
507
|
+
const start = Number(input.dataset.tokenStart);
|
|
508
|
+
input.setSelectionRange(state.caret.start - start, state.caret.end - start);
|
|
345
509
|
}
|
|
346
510
|
rendering = false;
|
|
347
511
|
drawMenu();
|
|
348
512
|
};
|
|
513
|
+
function matrixSelectionKey(event) {
|
|
514
|
+
const snapshot = session.getSnapshot(), selection = snapshot.matrixRange;
|
|
515
|
+
if (!selection || composing || event.isComposing)
|
|
516
|
+
return false;
|
|
517
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
session.execute({ type: 'matrix-select', all: true });
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
if (event.metaKey || event.ctrlKey || event.altKey)
|
|
523
|
+
return false;
|
|
524
|
+
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
const next = extendMatrixRange(snapshot.state.document, selection, event.key);
|
|
527
|
+
if (event.shiftKey)
|
|
528
|
+
session.selectMatrixRange(next);
|
|
529
|
+
else {
|
|
530
|
+
editing = true;
|
|
531
|
+
session.select(focusMatrixCell(snapshot.state, next.matrixId, next.focus).caret);
|
|
532
|
+
}
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
if (['Escape', 'Enter', 'F2'].includes(event.key)) {
|
|
536
|
+
event.preventDefault();
|
|
537
|
+
editing = true;
|
|
538
|
+
session.selectMatrixRange();
|
|
539
|
+
render();
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (['Backspace', 'Delete'].includes(event.key) || Array.from(event.key).length === 1) {
|
|
543
|
+
event.preventDefault();
|
|
544
|
+
perform(['Backspace', 'Delete'].includes(event.key)
|
|
545
|
+
? { type: 'matrix-clear' }
|
|
546
|
+
: { type: 'text', value: event.key });
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
function keyboardSelect(event) {
|
|
552
|
+
if (composing ||
|
|
553
|
+
event.isComposing ||
|
|
554
|
+
!event.shiftKey ||
|
|
555
|
+
event.altKey ||
|
|
556
|
+
event.ctrlKey ||
|
|
557
|
+
event.metaKey ||
|
|
558
|
+
!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key))
|
|
559
|
+
return false;
|
|
560
|
+
event.preventDefault();
|
|
561
|
+
const snapshot = session.getSnapshot();
|
|
562
|
+
session.selectRange(extendKeyboardRange(snapshot.state, snapshot.range, event.key));
|
|
563
|
+
surface.focus();
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
function keyboardCollapse(event) {
|
|
567
|
+
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)
|
|
568
|
+
return false;
|
|
569
|
+
const snapshot = session.getSnapshot();
|
|
570
|
+
const point = collapseKeyboardRange(snapshot.state.document, snapshot.range, event.key);
|
|
571
|
+
if (!point)
|
|
572
|
+
return false;
|
|
573
|
+
event.preventDefault();
|
|
574
|
+
editing = true;
|
|
575
|
+
session.select({ id: point.id, start: point.offset, end: point.offset });
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
349
578
|
function keydown(event) {
|
|
350
579
|
event.stopPropagation();
|
|
351
580
|
if (event.isComposing || composing)
|
|
352
581
|
return;
|
|
582
|
+
if (event.altKey && event.shiftKey && event.code === 'KeyV') {
|
|
583
|
+
event.preventDefault();
|
|
584
|
+
openLatex();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (matrixSelectionKey(event) || keyboardSelect(event) || keyboardCollapse(event))
|
|
588
|
+
return;
|
|
353
589
|
const snapshot = session.getSnapshot(), state = snapshot.state, c = state.caret, active = textNodes(state.document).find((n) => n.id === c.id);
|
|
354
590
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
|
|
355
591
|
event.preventDefault();
|
|
@@ -375,7 +611,9 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
375
611
|
}
|
|
376
612
|
if (event.key === 'Escape') {
|
|
377
613
|
event.preventDefault();
|
|
378
|
-
if (
|
|
614
|
+
if (dismissSelectionMenu())
|
|
615
|
+
return;
|
|
616
|
+
if (!menu.hidden && result.candidates.length) {
|
|
379
617
|
dismissed = `${active.id}:${active.text}:${c.start}`;
|
|
380
618
|
drawMenu();
|
|
381
619
|
}
|
|
@@ -429,10 +667,23 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
429
667
|
if (event.key === 'Tab' ||
|
|
430
668
|
(!event.shiftKey &&
|
|
431
669
|
c.start === c.end &&
|
|
432
|
-
((event.key === 'ArrowLeft' && c.start ===
|
|
433
|
-
(event.key === 'ArrowRight' && c.end ===
|
|
670
|
+
((event.key === 'ArrowLeft' && c.start === Number(currentInput()?.dataset.tokenStart)) ||
|
|
671
|
+
(event.key === 'ArrowRight' && c.end === Number(currentInput()?.dataset.tokenEnd))))) {
|
|
434
672
|
const direction = event.key === 'Tab' ? (event.shiftKey ? -1 : 1) : event.key === 'ArrowLeft' ? -1 : 1;
|
|
435
|
-
const
|
|
673
|
+
const parts = isLiteralText(state.document, c.id) ? [] : tokenizeMathText(active.text);
|
|
674
|
+
const target = parts[tokenIndexAt(parts, c.start, c.affinity) + direction];
|
|
675
|
+
const offset = target && (direction > 0 ? target.start : target.end);
|
|
676
|
+
const next = target
|
|
677
|
+
? {
|
|
678
|
+
...state,
|
|
679
|
+
caret: {
|
|
680
|
+
id: c.id,
|
|
681
|
+
start: offset,
|
|
682
|
+
end: offset,
|
|
683
|
+
affinity: direction > 0 ? 'forward' : 'backward',
|
|
684
|
+
},
|
|
685
|
+
}
|
|
686
|
+
: moveCaret(state, direction);
|
|
436
687
|
if (next !== state) {
|
|
437
688
|
event.preventDefault();
|
|
438
689
|
session.select(next.caret);
|
|
@@ -445,9 +696,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
445
696
|
}
|
|
446
697
|
if (!event.shiftKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
|
447
698
|
const direction = event.key === 'ArrowUp' ? -1 : 1;
|
|
448
|
-
const next =
|
|
449
|
-
? moveMatrixVertical(state, direction)
|
|
450
|
-
: moveLineVertical(state, direction);
|
|
699
|
+
const next = moveVertical(state, direction, renderedCaretGeometry(surface));
|
|
451
700
|
if (next !== state) {
|
|
452
701
|
event.preventDefault();
|
|
453
702
|
session.select(next.caret);
|
|
@@ -455,6 +704,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
455
704
|
return;
|
|
456
705
|
}
|
|
457
706
|
if (c.start === c.end && ['Backspace', 'Delete'].includes(event.key)) {
|
|
707
|
+
const input = currentInput();
|
|
708
|
+
const left = event.key === 'Backspace';
|
|
709
|
+
if ((left && c.start > 0 && c.start === Number(input?.dataset.tokenStart)) ||
|
|
710
|
+
(!left && c.end < active.text.length && c.end === Number(input?.dataset.tokenEnd))) {
|
|
711
|
+
event.preventDefault();
|
|
712
|
+
const count = left
|
|
713
|
+
? Array.from(active.text.slice(0, c.start)).at(-1).length
|
|
714
|
+
: Array.from(active.text.slice(c.end))[0].length;
|
|
715
|
+
const from = c.start - (left ? count : 0);
|
|
716
|
+
apply(setText(state, active.text.slice(0, from) + active.text.slice(from + count), from));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
458
719
|
const grid = gridDeletionTarget(state, event.key === 'Backspace' ? -1 : 1);
|
|
459
720
|
if (grid) {
|
|
460
721
|
event.preventDefault();
|
|
@@ -489,9 +750,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
489
750
|
if (!best)
|
|
490
751
|
return;
|
|
491
752
|
const mirror = best.querySelector('.me-measure'), value = best.dataset.value;
|
|
753
|
+
// Empty caret slots have no text node for DOM Range to measure.
|
|
754
|
+
if (!value.length || !mirror.firstChild)
|
|
755
|
+
return {
|
|
756
|
+
id: best.dataset.runId,
|
|
757
|
+
offset: Number(best.dataset.tokenStart),
|
|
758
|
+
affinity: 'forward',
|
|
759
|
+
};
|
|
492
760
|
let offset = 0, closest = Infinity;
|
|
493
|
-
|
|
494
|
-
|
|
761
|
+
let i = 0;
|
|
762
|
+
for (const character of ['', ...value]) {
|
|
763
|
+
i += character.length;
|
|
764
|
+
const range = doc.createRange();
|
|
495
765
|
range.setStart(mirror.firstChild, i);
|
|
496
766
|
range.collapse(true);
|
|
497
767
|
const d = Math.abs(range.getBoundingClientRect().left - x);
|
|
@@ -500,55 +770,136 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
500
770
|
offset = i;
|
|
501
771
|
}
|
|
502
772
|
}
|
|
503
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
id: best.dataset.runId,
|
|
775
|
+
offset: Number(best.dataset.tokenStart) + offset,
|
|
776
|
+
affinity: offset === 0 ? 'forward' : 'backward',
|
|
777
|
+
};
|
|
504
778
|
};
|
|
505
779
|
surface.onpointerdown = (event) => {
|
|
506
|
-
if (event.button || composing || event.target.closest('.me-line-number'))
|
|
507
|
-
return;
|
|
508
|
-
if (event.target instanceof HTMLInputElement)
|
|
780
|
+
if (event.button || composing || drag || event.target.closest('.me-line-number'))
|
|
509
781
|
return;
|
|
510
782
|
const point = pointAt(event.clientX, event.clientY);
|
|
511
783
|
if (point) {
|
|
512
|
-
event.
|
|
784
|
+
event.stopPropagation();
|
|
785
|
+
selectionMenu.reset();
|
|
513
786
|
candidateIndex = 0;
|
|
514
|
-
|
|
515
|
-
|
|
787
|
+
if (event.shiftKey) {
|
|
788
|
+
const snapshot = session.getSnapshot();
|
|
789
|
+
const previous = snapshot.matrixRange;
|
|
790
|
+
const rectangle = matrixRangeBetween(snapshot.state.document, previous
|
|
791
|
+
? focusMatrixCell(snapshot.state, previous.matrixId, previous.anchor).caret.id
|
|
792
|
+
: snapshot.state.caret.id, point.id);
|
|
793
|
+
if (rectangle) {
|
|
794
|
+
event.preventDefault();
|
|
795
|
+
editing = false;
|
|
796
|
+
session.selectMatrixRange(rectangle);
|
|
797
|
+
surface.focus({ preventScroll: true });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const input = event.target === currentInput() ? currentInput() : undefined;
|
|
802
|
+
const nativeAnchor = input && event.shiftKey ? nativeSelectionAnchor(input) : null;
|
|
803
|
+
drag = {
|
|
804
|
+
point: nativeAnchor !== null
|
|
805
|
+
? {
|
|
806
|
+
id: input.closest('.me-run').dataset.runId,
|
|
807
|
+
offset: Number(input.dataset.tokenStart) + nativeAnchor,
|
|
808
|
+
}
|
|
809
|
+
: point,
|
|
810
|
+
pointerId: event.pointerId,
|
|
811
|
+
x: event.clientX,
|
|
812
|
+
y: event.clientY,
|
|
813
|
+
moved: false,
|
|
814
|
+
input,
|
|
815
|
+
};
|
|
816
|
+
if (!input) {
|
|
817
|
+
event.preventDefault();
|
|
818
|
+
surface.setPointerCapture(event.pointerId);
|
|
819
|
+
}
|
|
516
820
|
}
|
|
517
821
|
};
|
|
518
|
-
|
|
519
|
-
if (
|
|
822
|
+
const movePointer = (event) => {
|
|
823
|
+
if (composing ||
|
|
824
|
+
!drag ||
|
|
825
|
+
!extendsMathSelection(drag, event, drag.input?.getBoundingClientRect()))
|
|
520
826
|
return;
|
|
521
827
|
const point = pointAt(event.clientX, event.clientY);
|
|
522
828
|
if (point) {
|
|
829
|
+
event.preventDefault();
|
|
830
|
+
event.stopPropagation();
|
|
523
831
|
drag.moved = true;
|
|
832
|
+
// Transfer capture before rendering removes the native input that began this drag.
|
|
833
|
+
surface.setPointerCapture(event.pointerId);
|
|
524
834
|
editing = false;
|
|
525
|
-
|
|
526
|
-
|
|
835
|
+
selectedGrid = undefined;
|
|
836
|
+
const snapshot = session.getSnapshot();
|
|
837
|
+
const rectangle = dragMatrix ?? matrixRangeBetween(snapshot.state.document, drag.point.id, point.id);
|
|
838
|
+
const bounds = rectangle && resolveMatrixRange(snapshot.state.document, rectangle);
|
|
839
|
+
if (rectangle && bounds) {
|
|
840
|
+
dragMatrix = matrixPointerRange(surface, rectangle, bounds.matrix.columns, event.clientX, event.clientY);
|
|
841
|
+
session.selectMatrixRange(dragMatrix);
|
|
842
|
+
}
|
|
843
|
+
else
|
|
844
|
+
session.selectRange({ anchor: drag.point, focus: point });
|
|
845
|
+
surface.focus({ preventScroll: true });
|
|
527
846
|
}
|
|
528
847
|
};
|
|
529
|
-
|
|
848
|
+
const endPointer = (event) => {
|
|
530
849
|
const previous = drag;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (previous
|
|
850
|
+
if (!previous || previous.pointerId !== event.pointerId)
|
|
851
|
+
return;
|
|
852
|
+
cancelDrag();
|
|
853
|
+
if (previous.moved) {
|
|
854
|
+
event.preventDefault();
|
|
855
|
+
event.stopPropagation();
|
|
856
|
+
}
|
|
857
|
+
else if (!previous.input) {
|
|
535
858
|
editing = true;
|
|
536
859
|
session.select({
|
|
537
860
|
id: previous.point.id,
|
|
538
861
|
start: previous.point.offset,
|
|
539
862
|
end: previous.point.offset,
|
|
863
|
+
affinity: previous.point.affinity,
|
|
540
864
|
});
|
|
541
865
|
}
|
|
542
866
|
};
|
|
543
|
-
|
|
544
|
-
drag
|
|
867
|
+
const cancelPointer = (event) => {
|
|
868
|
+
if (drag?.pointerId === event.pointerId)
|
|
869
|
+
cancelDrag();
|
|
870
|
+
};
|
|
871
|
+
surface.onlostpointercapture = (event) => {
|
|
872
|
+
if (event.target === surface)
|
|
873
|
+
cancelPointer(event);
|
|
545
874
|
};
|
|
875
|
+
surface.ondragstart = (event) => {
|
|
876
|
+
// A formula drag selects model content; it must not become host HTML drag-and-drop.
|
|
877
|
+
event.preventDefault();
|
|
878
|
+
event.stopPropagation();
|
|
879
|
+
};
|
|
880
|
+
doc.addEventListener('pointermove', movePointer, true);
|
|
881
|
+
doc.addEventListener('pointerup', endPointer, true);
|
|
882
|
+
doc.addEventListener('pointercancel', cancelPointer, true);
|
|
883
|
+
win.addEventListener('blur', cancelDrag);
|
|
546
884
|
surface.onkeydown = (event) => {
|
|
547
|
-
if (event.target
|
|
885
|
+
if (event.target === currentInput())
|
|
548
886
|
return;
|
|
549
887
|
event.stopPropagation();
|
|
888
|
+
if (composing || event.isComposing)
|
|
889
|
+
return;
|
|
890
|
+
if (event.altKey && event.shiftKey && event.code === 'KeyV') {
|
|
891
|
+
event.preventDefault();
|
|
892
|
+
openLatex();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (matrixSelectionKey(event) || keyboardSelect(event) || keyboardCollapse(event))
|
|
896
|
+
return;
|
|
550
897
|
const snapshot = session.getSnapshot();
|
|
551
|
-
if (snapshot.range &&
|
|
898
|
+
if (snapshot.range &&
|
|
899
|
+
!event.ctrlKey &&
|
|
900
|
+
!event.metaKey &&
|
|
901
|
+
!event.shiftKey &&
|
|
902
|
+
(event.key === 'Enter' || (event.altKey && ['ArrowDown', 'ArrowUp'].includes(event.key)))) {
|
|
552
903
|
const items = [...menu.querySelectorAll('[role="option"]')];
|
|
553
904
|
if (items.length) {
|
|
554
905
|
event.preventDefault();
|
|
@@ -575,7 +926,11 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
575
926
|
perform({ type: event.shiftKey ? 'redo' : 'undo' });
|
|
576
927
|
}
|
|
577
928
|
else if (event.key === 'Escape') {
|
|
929
|
+
event.preventDefault();
|
|
930
|
+
if (dismissSelectionMenu())
|
|
931
|
+
return;
|
|
578
932
|
session.selectRange();
|
|
933
|
+
options.onCancel?.();
|
|
579
934
|
}
|
|
580
935
|
else if (snapshot.range &&
|
|
581
936
|
!event.metaKey &&
|
|
@@ -598,12 +953,17 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
598
953
|
}
|
|
599
954
|
};
|
|
600
955
|
const copy = (event, cut = false) => {
|
|
956
|
+
if (composing || !event.clipboardData)
|
|
957
|
+
return;
|
|
601
958
|
const fragment = session.copy();
|
|
602
959
|
if (!fragment)
|
|
603
960
|
return;
|
|
604
961
|
event.preventDefault();
|
|
605
962
|
event.stopPropagation();
|
|
606
|
-
|
|
963
|
+
const matrix = session.copyMatrix();
|
|
964
|
+
if (matrix)
|
|
965
|
+
event.clipboardData.setData(MATH_MATRIX_CLIPBOARD_TYPE, JSON.stringify(matrix));
|
|
966
|
+
event.clipboardData.setData(MATH_CLIPBOARD_TYPE, JSON.stringify(fragment));
|
|
607
967
|
event.clipboardData?.setData('text/plain', toLatex(fragmentLatex(fragment)));
|
|
608
968
|
if (cut)
|
|
609
969
|
perform({ type: 'text', value: '' });
|
|
@@ -614,6 +974,23 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
614
974
|
if (composing)
|
|
615
975
|
return;
|
|
616
976
|
const data = event.clipboardData, fragment = parseFragment(data?.getData(MATH_CLIPBOARD_TYPE) ?? ''), text = data?.getData('text/plain') ?? '';
|
|
977
|
+
const matrixSource = data?.getData(MATH_MATRIX_CLIPBOARD_TYPE) ?? '';
|
|
978
|
+
const matrix = matrixSource ? parseMatrixFragment(matrixSource) : matrixFromTSV(text);
|
|
979
|
+
const snapshot = session.getSnapshot();
|
|
980
|
+
const inMatrix = !!snapshot.matrixRange || !!activeMatrix(snapshot.state);
|
|
981
|
+
if (matrixSource || (inMatrix && text.includes('\t'))) {
|
|
982
|
+
event.preventDefault();
|
|
983
|
+
event.stopPropagation();
|
|
984
|
+
if (!matrix) {
|
|
985
|
+
notify('matrix.pasteRejected');
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
editing = true;
|
|
989
|
+
const ok = inMatrix ? session.pasteMatrix(matrix) : session.paste(matrixFragment(matrix));
|
|
990
|
+
if (!ok)
|
|
991
|
+
notify('matrix.pasteRejected');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
617
994
|
if (!fragment && !text)
|
|
618
995
|
return;
|
|
619
996
|
event.preventDefault();
|
|
@@ -628,6 +1005,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
628
1005
|
!composing &&
|
|
629
1006
|
!root.contains(event.relatedTarget) &&
|
|
630
1007
|
!menu.contains(event.relatedTarget)) {
|
|
1008
|
+
cancelDrag();
|
|
631
1009
|
editing = false;
|
|
632
1010
|
render();
|
|
633
1011
|
}
|
|
@@ -640,24 +1018,39 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
640
1018
|
? mountMathToolbar(tools, session, {
|
|
641
1019
|
kinds: Array.isArray(enabled) ? enabled : undefined,
|
|
642
1020
|
maxItems: options.toolbarMaxItems,
|
|
1021
|
+
onPasteLatex: openLatex,
|
|
1022
|
+
onOpenPanel: () => {
|
|
1023
|
+
latexPanel?.destroy();
|
|
1024
|
+
latexPanel = undefined;
|
|
1025
|
+
},
|
|
643
1026
|
onExecute: () => {
|
|
644
1027
|
editing = true;
|
|
645
1028
|
render();
|
|
1029
|
+
if (session.getSnapshot().matrixRange)
|
|
1030
|
+
surface.focus();
|
|
646
1031
|
},
|
|
647
1032
|
})
|
|
648
1033
|
: undefined;
|
|
1034
|
+
toolbar?.setDisabled(composing);
|
|
649
1035
|
};
|
|
650
1036
|
const unsubscribe = session.subscribe((snapshot, changed) => {
|
|
1037
|
+
if (changed) {
|
|
1038
|
+
cancelDrag();
|
|
1039
|
+
selectionMenu.reset();
|
|
1040
|
+
}
|
|
651
1041
|
render();
|
|
652
1042
|
if (changed)
|
|
653
1043
|
options.onChange?.(snapshot.state.document, snapshot.latex);
|
|
654
1044
|
});
|
|
655
|
-
const reposition = () => {
|
|
1045
|
+
const reposition = (event) => {
|
|
1046
|
+
// Scrolling the candidate list must not rebuild it and reset its scroll position.
|
|
1047
|
+
if (event.target && 'nodeType' in event.target && menu.contains(event.target))
|
|
1048
|
+
return;
|
|
656
1049
|
if (!menu.hidden)
|
|
657
1050
|
drawMenu();
|
|
658
1051
|
};
|
|
659
|
-
|
|
660
|
-
|
|
1052
|
+
win.addEventListener('resize', reposition);
|
|
1053
|
+
win.addEventListener('scroll', reposition, true);
|
|
661
1054
|
rebuildToolbar();
|
|
662
1055
|
render();
|
|
663
1056
|
return {
|
|
@@ -665,6 +1058,8 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
665
1058
|
focus() {
|
|
666
1059
|
editing = true;
|
|
667
1060
|
render();
|
|
1061
|
+
if (session.getSnapshot().matrixRange)
|
|
1062
|
+
surface.focus();
|
|
668
1063
|
},
|
|
669
1064
|
update(next) {
|
|
670
1065
|
options = { ...options, ...next };
|
|
@@ -676,11 +1071,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
676
1071
|
if (destroyed)
|
|
677
1072
|
return;
|
|
678
1073
|
destroyed = true;
|
|
1074
|
+
cancelDrag();
|
|
679
1075
|
unsubscribe();
|
|
1076
|
+
latexPanel?.destroy();
|
|
680
1077
|
toolbar?.destroy();
|
|
681
1078
|
root.removeEventListener('focusout', blur);
|
|
682
|
-
|
|
683
|
-
|
|
1079
|
+
win.removeEventListener('resize', reposition);
|
|
1080
|
+
win.removeEventListener('scroll', reposition, true);
|
|
1081
|
+
doc.removeEventListener('pointermove', movePointer, true);
|
|
1082
|
+
doc.removeEventListener('pointerup', endPointer, true);
|
|
1083
|
+
doc.removeEventListener('pointercancel', cancelPointer, true);
|
|
1084
|
+
win.removeEventListener('blur', cancelDrag);
|
|
1085
|
+
stopTheme();
|
|
684
1086
|
menu.remove();
|
|
685
1087
|
root.remove();
|
|
686
1088
|
if (ownsSession)
|
|
@@ -688,4 +1090,3 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
688
1090
|
},
|
|
689
1091
|
};
|
|
690
1092
|
}
|
|
691
|
-
//# sourceMappingURL=dom.js.map
|