@barocss/math-editor 0.2.1 → 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 +45 -0
- package/EMBEDDING.md +4 -0
- package/IMPLEMENTATION.md +27 -2
- package/JSON-MODEL.md +1 -0
- package/LATEX-GUIDE.md +8 -1
- package/LATEX-SCOPE.md +2 -2
- package/LICENSE +1 -3
- package/README.md +145 -37
- package/ROADMAP.md +93 -11
- package/STYLING.md +157 -0
- package/SUPPORT.md +24 -12
- package/VALIDATION.md +113 -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 +510 -128
- 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 +27 -1
- package/dist/locales/en.json +27 -1
- package/dist/locales/ko.js +27 -1
- package/dist/locales/ko.json +27 -1
- 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 +285 -46
- 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 +2 -1
- package/dist/range.js +11 -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 +100 -12
- 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, extendKeyboardRange, 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,11 +504,50 @@ 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
|
+
}
|
|
349
551
|
function keyboardSelect(event) {
|
|
350
552
|
if (composing ||
|
|
351
553
|
event.isComposing ||
|
|
@@ -361,11 +563,28 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
361
563
|
surface.focus();
|
|
362
564
|
return true;
|
|
363
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
|
+
}
|
|
364
578
|
function keydown(event) {
|
|
365
579
|
event.stopPropagation();
|
|
366
580
|
if (event.isComposing || composing)
|
|
367
581
|
return;
|
|
368
|
-
if (
|
|
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))
|
|
369
588
|
return;
|
|
370
589
|
const snapshot = session.getSnapshot(), state = snapshot.state, c = state.caret, active = textNodes(state.document).find((n) => n.id === c.id);
|
|
371
590
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
|
|
@@ -392,7 +611,9 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
392
611
|
}
|
|
393
612
|
if (event.key === 'Escape') {
|
|
394
613
|
event.preventDefault();
|
|
395
|
-
if (
|
|
614
|
+
if (dismissSelectionMenu())
|
|
615
|
+
return;
|
|
616
|
+
if (!menu.hidden && result.candidates.length) {
|
|
396
617
|
dismissed = `${active.id}:${active.text}:${c.start}`;
|
|
397
618
|
drawMenu();
|
|
398
619
|
}
|
|
@@ -446,10 +667,23 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
446
667
|
if (event.key === 'Tab' ||
|
|
447
668
|
(!event.shiftKey &&
|
|
448
669
|
c.start === c.end &&
|
|
449
|
-
((event.key === 'ArrowLeft' && c.start ===
|
|
450
|
-
(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))))) {
|
|
451
672
|
const direction = event.key === 'Tab' ? (event.shiftKey ? -1 : 1) : event.key === 'ArrowLeft' ? -1 : 1;
|
|
452
|
-
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);
|
|
453
687
|
if (next !== state) {
|
|
454
688
|
event.preventDefault();
|
|
455
689
|
session.select(next.caret);
|
|
@@ -462,9 +696,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
462
696
|
}
|
|
463
697
|
if (!event.shiftKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
|
464
698
|
const direction = event.key === 'ArrowUp' ? -1 : 1;
|
|
465
|
-
const next =
|
|
466
|
-
? moveMatrixVertical(state, direction)
|
|
467
|
-
: moveLineVertical(state, direction);
|
|
699
|
+
const next = moveVertical(state, direction, renderedCaretGeometry(surface));
|
|
468
700
|
if (next !== state) {
|
|
469
701
|
event.preventDefault();
|
|
470
702
|
session.select(next.caret);
|
|
@@ -472,6 +704,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
472
704
|
return;
|
|
473
705
|
}
|
|
474
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
|
+
}
|
|
475
719
|
const grid = gridDeletionTarget(state, event.key === 'Backspace' ? -1 : 1);
|
|
476
720
|
if (grid) {
|
|
477
721
|
event.preventDefault();
|
|
@@ -506,9 +750,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
506
750
|
if (!best)
|
|
507
751
|
return;
|
|
508
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
|
+
};
|
|
509
760
|
let offset = 0, closest = Infinity;
|
|
510
|
-
|
|
511
|
-
|
|
761
|
+
let i = 0;
|
|
762
|
+
for (const character of ['', ...value]) {
|
|
763
|
+
i += character.length;
|
|
764
|
+
const range = doc.createRange();
|
|
512
765
|
range.setStart(mirror.firstChild, i);
|
|
513
766
|
range.collapse(true);
|
|
514
767
|
const d = Math.abs(range.getBoundingClientRect().left - x);
|
|
@@ -517,57 +770,136 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
517
770
|
offset = i;
|
|
518
771
|
}
|
|
519
772
|
}
|
|
520
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
id: best.dataset.runId,
|
|
775
|
+
offset: Number(best.dataset.tokenStart) + offset,
|
|
776
|
+
affinity: offset === 0 ? 'forward' : 'backward',
|
|
777
|
+
};
|
|
521
778
|
};
|
|
522
779
|
surface.onpointerdown = (event) => {
|
|
523
|
-
if (event.button || composing || event.target.closest('.me-line-number'))
|
|
524
|
-
return;
|
|
525
|
-
if (event.target instanceof HTMLInputElement)
|
|
780
|
+
if (event.button || composing || drag || event.target.closest('.me-line-number'))
|
|
526
781
|
return;
|
|
527
782
|
const point = pointAt(event.clientX, event.clientY);
|
|
528
783
|
if (point) {
|
|
529
|
-
event.
|
|
784
|
+
event.stopPropagation();
|
|
785
|
+
selectionMenu.reset();
|
|
530
786
|
candidateIndex = 0;
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
}
|
|
533
820
|
}
|
|
534
821
|
};
|
|
535
|
-
|
|
536
|
-
if (
|
|
822
|
+
const movePointer = (event) => {
|
|
823
|
+
if (composing ||
|
|
824
|
+
!drag ||
|
|
825
|
+
!extendsMathSelection(drag, event, drag.input?.getBoundingClientRect()))
|
|
537
826
|
return;
|
|
538
827
|
const point = pointAt(event.clientX, event.clientY);
|
|
539
828
|
if (point) {
|
|
829
|
+
event.preventDefault();
|
|
830
|
+
event.stopPropagation();
|
|
540
831
|
drag.moved = true;
|
|
832
|
+
// Transfer capture before rendering removes the native input that began this drag.
|
|
833
|
+
surface.setPointerCapture(event.pointerId);
|
|
541
834
|
editing = false;
|
|
542
|
-
|
|
543
|
-
|
|
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 });
|
|
544
846
|
}
|
|
545
847
|
};
|
|
546
|
-
|
|
848
|
+
const endPointer = (event) => {
|
|
547
849
|
const previous = drag;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
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) {
|
|
552
858
|
editing = true;
|
|
553
859
|
session.select({
|
|
554
860
|
id: previous.point.id,
|
|
555
861
|
start: previous.point.offset,
|
|
556
862
|
end: previous.point.offset,
|
|
863
|
+
affinity: previous.point.affinity,
|
|
557
864
|
});
|
|
558
865
|
}
|
|
559
866
|
};
|
|
560
|
-
|
|
561
|
-
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);
|
|
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();
|
|
562
879
|
};
|
|
880
|
+
doc.addEventListener('pointermove', movePointer, true);
|
|
881
|
+
doc.addEventListener('pointerup', endPointer, true);
|
|
882
|
+
doc.addEventListener('pointercancel', cancelPointer, true);
|
|
883
|
+
win.addEventListener('blur', cancelDrag);
|
|
563
884
|
surface.onkeydown = (event) => {
|
|
564
|
-
if (event.target
|
|
885
|
+
if (event.target === currentInput())
|
|
565
886
|
return;
|
|
566
887
|
event.stopPropagation();
|
|
567
|
-
if (
|
|
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))
|
|
568
896
|
return;
|
|
569
897
|
const snapshot = session.getSnapshot();
|
|
570
|
-
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)))) {
|
|
571
903
|
const items = [...menu.querySelectorAll('[role="option"]')];
|
|
572
904
|
if (items.length) {
|
|
573
905
|
event.preventDefault();
|
|
@@ -594,7 +926,11 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
594
926
|
perform({ type: event.shiftKey ? 'redo' : 'undo' });
|
|
595
927
|
}
|
|
596
928
|
else if (event.key === 'Escape') {
|
|
929
|
+
event.preventDefault();
|
|
930
|
+
if (dismissSelectionMenu())
|
|
931
|
+
return;
|
|
597
932
|
session.selectRange();
|
|
933
|
+
options.onCancel?.();
|
|
598
934
|
}
|
|
599
935
|
else if (snapshot.range &&
|
|
600
936
|
!event.metaKey &&
|
|
@@ -617,12 +953,17 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
617
953
|
}
|
|
618
954
|
};
|
|
619
955
|
const copy = (event, cut = false) => {
|
|
956
|
+
if (composing || !event.clipboardData)
|
|
957
|
+
return;
|
|
620
958
|
const fragment = session.copy();
|
|
621
959
|
if (!fragment)
|
|
622
960
|
return;
|
|
623
961
|
event.preventDefault();
|
|
624
962
|
event.stopPropagation();
|
|
625
|
-
|
|
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));
|
|
626
967
|
event.clipboardData?.setData('text/plain', toLatex(fragmentLatex(fragment)));
|
|
627
968
|
if (cut)
|
|
628
969
|
perform({ type: 'text', value: '' });
|
|
@@ -633,6 +974,23 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
633
974
|
if (composing)
|
|
634
975
|
return;
|
|
635
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
|
+
}
|
|
636
994
|
if (!fragment && !text)
|
|
637
995
|
return;
|
|
638
996
|
event.preventDefault();
|
|
@@ -647,6 +1005,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
647
1005
|
!composing &&
|
|
648
1006
|
!root.contains(event.relatedTarget) &&
|
|
649
1007
|
!menu.contains(event.relatedTarget)) {
|
|
1008
|
+
cancelDrag();
|
|
650
1009
|
editing = false;
|
|
651
1010
|
render();
|
|
652
1011
|
}
|
|
@@ -659,24 +1018,39 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
659
1018
|
? mountMathToolbar(tools, session, {
|
|
660
1019
|
kinds: Array.isArray(enabled) ? enabled : undefined,
|
|
661
1020
|
maxItems: options.toolbarMaxItems,
|
|
1021
|
+
onPasteLatex: openLatex,
|
|
1022
|
+
onOpenPanel: () => {
|
|
1023
|
+
latexPanel?.destroy();
|
|
1024
|
+
latexPanel = undefined;
|
|
1025
|
+
},
|
|
662
1026
|
onExecute: () => {
|
|
663
1027
|
editing = true;
|
|
664
1028
|
render();
|
|
1029
|
+
if (session.getSnapshot().matrixRange)
|
|
1030
|
+
surface.focus();
|
|
665
1031
|
},
|
|
666
1032
|
})
|
|
667
1033
|
: undefined;
|
|
1034
|
+
toolbar?.setDisabled(composing);
|
|
668
1035
|
};
|
|
669
1036
|
const unsubscribe = session.subscribe((snapshot, changed) => {
|
|
1037
|
+
if (changed) {
|
|
1038
|
+
cancelDrag();
|
|
1039
|
+
selectionMenu.reset();
|
|
1040
|
+
}
|
|
670
1041
|
render();
|
|
671
1042
|
if (changed)
|
|
672
1043
|
options.onChange?.(snapshot.state.document, snapshot.latex);
|
|
673
1044
|
});
|
|
674
|
-
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;
|
|
675
1049
|
if (!menu.hidden)
|
|
676
1050
|
drawMenu();
|
|
677
1051
|
};
|
|
678
|
-
|
|
679
|
-
|
|
1052
|
+
win.addEventListener('resize', reposition);
|
|
1053
|
+
win.addEventListener('scroll', reposition, true);
|
|
680
1054
|
rebuildToolbar();
|
|
681
1055
|
render();
|
|
682
1056
|
return {
|
|
@@ -684,6 +1058,8 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
684
1058
|
focus() {
|
|
685
1059
|
editing = true;
|
|
686
1060
|
render();
|
|
1061
|
+
if (session.getSnapshot().matrixRange)
|
|
1062
|
+
surface.focus();
|
|
687
1063
|
},
|
|
688
1064
|
update(next) {
|
|
689
1065
|
options = { ...options, ...next };
|
|
@@ -695,11 +1071,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
695
1071
|
if (destroyed)
|
|
696
1072
|
return;
|
|
697
1073
|
destroyed = true;
|
|
1074
|
+
cancelDrag();
|
|
698
1075
|
unsubscribe();
|
|
1076
|
+
latexPanel?.destroy();
|
|
699
1077
|
toolbar?.destroy();
|
|
700
1078
|
root.removeEventListener('focusout', blur);
|
|
701
|
-
|
|
702
|
-
|
|
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();
|
|
703
1086
|
menu.remove();
|
|
704
1087
|
root.remove();
|
|
705
1088
|
if (ownsSession)
|
|
@@ -707,4 +1090,3 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
707
1090
|
},
|
|
708
1091
|
};
|
|
709
1092
|
}
|
|
710
|
-
//# sourceMappingURL=dom.js.map
|