@barocss/math-editor 0.4.0 → 0.6.1
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/API-SESSION.md +35 -1
- package/API-WEB-COMPONENT.md +2 -0
- package/CHANGELOG.md +109 -0
- package/CLIPBOARD.md +54 -0
- package/EDITING-SCENARIOS.md +209 -0
- package/GETTING-STARTED.md +54 -0
- package/IMPLEMENTATION.md +105 -0
- package/KEYBOARD.md +41 -0
- package/LATEX-GUIDE.md +52 -2
- package/LATEX-MODEL.md +12 -0
- package/LATEX-SCOPE.md +10 -1
- package/README.md +119 -23
- package/RELEASING.md +95 -10
- package/RENDERING-TESTS.md +79 -0
- package/ROADMAP.md +238 -6
- package/STYLING.md +28 -1
- package/TEXT-EDITORS.md +157 -0
- package/VALIDATION.md +433 -0
- package/dist/context-tools.d.ts +21 -0
- package/dist/context-tools.js +37 -0
- package/dist/dom/caret-geometry.js +1 -1
- package/dist/dom/context-keyboard.d.ts +2 -0
- package/dist/dom/context-keyboard.js +22 -0
- package/dist/dom/help.d.ts +5 -0
- package/dist/dom/help.js +104 -0
- package/dist/dom/menu-position.d.ts +3 -0
- package/dist/dom/menu-position.js +49 -4
- package/dist/dom/readable-layout.d.ts +3 -0
- package/dist/dom/readable-layout.js +52 -0
- package/dist/dom/toolbar-catalog.d.ts +1 -1
- package/dist/dom/toolbar.d.ts +2 -0
- package/dist/dom/toolbar.js +8 -1
- package/dist/dom.d.ts +13 -1
- package/dist/dom.js +323 -51
- package/dist/enter-policy.js +1 -1
- package/dist/latex.js +11 -0
- package/dist/lines.d.ts +2 -0
- package/dist/lines.js +15 -0
- package/dist/locales/en.js +33 -3
- package/dist/locales/en.json +33 -3
- package/dist/locales/ko.js +33 -3
- package/dist/locales/ko.json +33 -3
- package/dist/math-editor-toolbar.js +2 -1
- package/dist/math-editor.d.ts +4 -1
- package/dist/math-editor.js +506 -296
- package/dist/math-layout.d.ts +12 -0
- package/dist/math-layout.js +59 -0
- package/dist/math-spacing.d.ts +13 -0
- package/dist/math-spacing.js +87 -0
- package/dist/model.d.ts +6 -0
- package/dist/model.js +72 -5
- package/dist/range.d.ts +10 -2
- package/dist/range.js +31 -6
- package/dist/root-transform.d.ts +19 -0
- package/dist/root-transform.js +69 -0
- package/dist/selection-shortcuts.d.ts +13 -0
- package/dist/selection-shortcuts.js +35 -0
- package/dist/session.d.ts +1 -0
- package/dist/session.js +1 -1
- package/dist/suggestions.d.ts +9 -0
- package/dist/suggestions.js +53 -0
- package/dist/symbols.d.ts +1 -1
- package/dist/symbols.js +1 -0
- package/dist/vertical-navigation.d.ts +5 -0
- package/dist/vertical-navigation.js +33 -0
- package/dist/web-component.js +9 -1
- package/package.json +11 -3
- package/src/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/src/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/src/fonts/README.md +18 -0
- package/src/shapes/README.md +21 -0
- package/src/shapes/parenthesis-bottom.svg +1 -0
- package/src/shapes/parenthesis-top.svg +1 -0
- package/src/shapes/parenthesis.svg +1 -0
- package/src/shapes/radical.svg +1 -0
- package/src/style.css +882 -61
package/dist/dom.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { followReadableMathLayout } from './dom/readable-layout.js';
|
|
2
|
+
import { attachMathHelp, requestMathHelp } from './dom/help.js';
|
|
3
|
+
import { mathRunSpacing } from './math-spacing.js';
|
|
4
|
+
import { mathLayout } from './math-layout.js';
|
|
5
|
+
import { wrapSelectionShortcut } from './selection-shortcuts.js';
|
|
6
|
+
import { transformRoot } from './root-transform.js';
|
|
7
|
+
import { structureEditingContext, changeContextFence } from './context-tools.js';
|
|
8
|
+
import { focusContextAction } from './dom/context-keyboard.js';
|
|
1
9
|
import { mountLatexPanel } from './dom/latex-panel.js';
|
|
2
10
|
import { followMathTheme, syncMathTheme } from './dom/theme.js';
|
|
3
11
|
import { matrixPointerRange } from './dom/matrix-selection.js';
|
|
4
|
-
import { positionMathMenu } from './dom/menu-position.js';
|
|
5
|
-
import { fences } from './fences.js';
|
|
12
|
+
import { positionMathMenu, followMathMenuPosition } from './dom/menu-position.js';
|
|
13
|
+
import { fences, fencePairs } from './fences.js';
|
|
6
14
|
import { createEditorLabels } from './editor-labels.js';
|
|
7
15
|
import { mathEnterAction } from './enter-policy.js';
|
|
8
16
|
export { mountMathLatex, mountMathPreview, } from './outputs.js';
|
|
@@ -11,16 +19,16 @@ import { mountMathToolbar } from './dom/toolbar.js';
|
|
|
11
19
|
export { mountMathToolbar } from './dom/toolbar.js';
|
|
12
20
|
import { createMathSession, } from './session.js';
|
|
13
21
|
import { translate, mathLocaleDirection } from './i18n.js';
|
|
14
|
-
import { documentRows, isLiteralText, textNodes, setText, moveCaret, unwrapPrevious, gridDeletionTarget, removeGrid, isGrid, toLatex, } from './model.js';
|
|
22
|
+
import { documentRows, isLiteralText, textNodes, setText, moveCaret, unwrapPrevious, unwrapNext, unwrapEmptySlot, gridDeletionTarget, removeGrid, isGrid, toLatex, } from './model.js';
|
|
15
23
|
import { activeGrid, activeMatrix, resizeMatrix } from './matrix.js';
|
|
16
24
|
import { MATH_MATRIX_CLIPBOARD_TYPE, parseMatrixFragment, matrixFragment, matrixFromTSV, matrixRangeBetween, resolveMatrixRange, extendMatrixRange, focusMatrixCell, } from './matrix-range.js';
|
|
17
|
-
import { joinPreviousLine } from './lines.js';
|
|
18
|
-
import {
|
|
25
|
+
import { joinPreviousLine, joinNextLine } from './lines.js';
|
|
26
|
+
import { createVerticalNavigation } from './vertical-navigation.js';
|
|
19
27
|
import { renderedCaretGeometry } from './dom/caret-geometry.js';
|
|
20
28
|
import { extendsMathSelection, nativeSelectionAnchor, createSelectionMenuState, textSelectionBounds, } from './dom/selection.js';
|
|
21
|
-
import { acceptSuggestion,
|
|
29
|
+
import { acceptSuggestion, findStateSuggestions, mathStructures, } from './suggestions.js';
|
|
22
30
|
import { tokenizeMathText, tokenIndexAt } from './tokens.js';
|
|
23
|
-
import { fragmentLatex, parseFragment, MATH_CLIPBOARD_TYPE, extendKeyboardRange, collapseKeyboardRange, resolveRange, copyRange, wrappingKinds, } from './range.js';
|
|
31
|
+
import { fragmentLatex, parseFragment, MATH_CLIPBOARD_TYPE, extendKeyboardRange, isTokenNavigationKey, collapseKeyboardRange, resolveRange, copyRange, wrappingKinds, } from './range.js';
|
|
24
32
|
/** Native DOM renderer. No React or framework runtime is imported by this module. */
|
|
25
33
|
export function mountMathEditor(host, initial = {}) {
|
|
26
34
|
const doc = host.ownerDocument;
|
|
@@ -37,8 +45,8 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
37
45
|
const ownsSession = !initial.session;
|
|
38
46
|
if (initial.session)
|
|
39
47
|
session.configure({ locale: initial.locale, mode: initial.mode });
|
|
40
|
-
const root = element('span', 'me-editor me-dom-editor'), tools = element('span'), surface = element('span', 'me-surface'), status = element('span', 'me-dom-status');
|
|
41
|
-
root.append(tools, surface, status);
|
|
48
|
+
const root = element('span', 'me-editor me-dom-editor'), tools = element('span'), surface = element('span', 'me-surface'), status = element('span', 'me-dom-status'), contextFooter = element('span', 'me-context-footer');
|
|
49
|
+
root.append(tools, surface, contextFooter, status);
|
|
42
50
|
host.append(root);
|
|
43
51
|
surface.tabIndex = 0;
|
|
44
52
|
surface.setAttribute('role', 'group');
|
|
@@ -70,6 +78,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
70
78
|
onClose: closeLatex,
|
|
71
79
|
});
|
|
72
80
|
};
|
|
81
|
+
const stopHelp = attachMathHelp(root, () => session.getSnapshot().locale);
|
|
73
82
|
const t = (value) => translate(session.getSnapshot().locale, value);
|
|
74
83
|
const currentInput = () => surface.querySelector('input');
|
|
75
84
|
const cancelDrag = () => {
|
|
@@ -82,6 +91,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
82
91
|
const notify = (key) => {
|
|
83
92
|
status.textContent = t(key);
|
|
84
93
|
};
|
|
94
|
+
const verticalNavigation = createVerticalNavigation();
|
|
85
95
|
const apply = (next) => {
|
|
86
96
|
editing = true;
|
|
87
97
|
selectedGrid = undefined;
|
|
@@ -96,13 +106,16 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
96
106
|
};
|
|
97
107
|
const candidates = () => {
|
|
98
108
|
const snapshot = session.getSnapshot(), state = snapshot.state, active = textNodes(state.document).find((n) => n.id === state.caret.id);
|
|
99
|
-
const result =
|
|
100
|
-
if (
|
|
109
|
+
const result = findStateSuggestions(state, snapshot.locale);
|
|
110
|
+
if (options.suggestionMenu === false ||
|
|
111
|
+
!editing ||
|
|
101
112
|
isLiteralText(state.document, state.caret.id) ||
|
|
102
113
|
snapshot.range ||
|
|
103
114
|
snapshot.matrixRange ||
|
|
104
115
|
dismissed === `${active.id}:${active.text}:${state.caret.start}`)
|
|
105
116
|
result.candidates = [];
|
|
117
|
+
else if (options.filterSuggestions)
|
|
118
|
+
result.candidates = [...options.filterSuggestions(result.candidates, result.query, state)];
|
|
106
119
|
return result;
|
|
107
120
|
};
|
|
108
121
|
const choose = (index) => {
|
|
@@ -162,12 +175,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
162
175
|
const itemRect = option.getBoundingClientRect();
|
|
163
176
|
const listRect = list.getBoundingClientRect();
|
|
164
177
|
const scaleY = listRect.height / list.offsetHeight || 1;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
list.scrollTop +=
|
|
179
|
+
(itemRect.top + itemRect.height / 2 - listRect.top) / scaleY -
|
|
180
|
+
list.clientTop -
|
|
181
|
+
list.clientHeight / 2;
|
|
169
182
|
};
|
|
170
183
|
const renderMenu = () => {
|
|
184
|
+
if (options.suggestionMenu === false) {
|
|
185
|
+
menu.hidden = true;
|
|
186
|
+
menu.replaceChildren();
|
|
187
|
+
currentInput()?.setAttribute('aria-expanded', 'false');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
171
190
|
syncMathTheme(root, menu);
|
|
172
191
|
const result = candidates(), input = currentInput();
|
|
173
192
|
menu.replaceChildren();
|
|
@@ -196,8 +215,9 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
196
215
|
const list = element('div', 'me-suggestions');
|
|
197
216
|
list.setAttribute('role', 'listbox');
|
|
198
217
|
list.setAttribute('aria-label', t('selection.toolbar'));
|
|
199
|
-
for (const item of mathStructures.filter((item) => wrappingKinds.includes(item.kind)
|
|
200
|
-
|
|
218
|
+
for (const item of mathStructures.filter((item) => wrappingKinds.includes(item.kind) &&
|
|
219
|
+
(!options.selectionKinds || options.selectionKinds.includes(item.kind)))) {
|
|
220
|
+
const button = suggestionButton(item.glyph, t(item.kind === 'fraction' ? 'selection.asNumerator' : item.label), t(item.detail));
|
|
201
221
|
button.setAttribute('aria-selected', String(list.children.length === candidateIndex));
|
|
202
222
|
button.disabled = !canWrap;
|
|
203
223
|
button.setAttribute('aria-label', translate(snapshot.locale, 'selection.wrap', { kind: t(item.label) }));
|
|
@@ -207,6 +227,18 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
207
227
|
perform({ type: 'structure', kind: item.kind });
|
|
208
228
|
};
|
|
209
229
|
list.append(button);
|
|
230
|
+
if (item.kind === 'fraction') {
|
|
231
|
+
const denominator = suggestionButton('□/▧', t('selection.asDenominator'), t(item.detail));
|
|
232
|
+
denominator.setAttribute('aria-selected', String(list.children.length === candidateIndex));
|
|
233
|
+
denominator.setAttribute('aria-label', t('selection.asDenominator'));
|
|
234
|
+
denominator.disabled = !canWrap;
|
|
235
|
+
denominator.onclick = () => {
|
|
236
|
+
if (!snapshot.range)
|
|
237
|
+
session.selectRange(selectedRange);
|
|
238
|
+
perform({ type: 'structure', kind: 'fraction', fractionSlot: 'denominator' });
|
|
239
|
+
};
|
|
240
|
+
list.append(denominator);
|
|
241
|
+
}
|
|
210
242
|
}
|
|
211
243
|
menu.append(list);
|
|
212
244
|
const selected = surface.querySelectorAll('.me-dom-selected, .me-range-highlight');
|
|
@@ -262,6 +294,90 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
262
294
|
event.stopPropagation();
|
|
263
295
|
(currentInput() ?? surface).focus();
|
|
264
296
|
};
|
|
297
|
+
const renderContextFooter = () => {
|
|
298
|
+
contextFooter.replaceChildren();
|
|
299
|
+
const snapshot = session.getSnapshot();
|
|
300
|
+
const structureContext = editing && options.contextTools !== false && !snapshot.range && !snapshot.matrixRange
|
|
301
|
+
? structureEditingContext(snapshot.state)
|
|
302
|
+
: undefined;
|
|
303
|
+
contextFooter.hidden = !structureContext;
|
|
304
|
+
if (!structureContext)
|
|
305
|
+
return;
|
|
306
|
+
contextFooter.setAttribute('role', 'group');
|
|
307
|
+
contextFooter.setAttribute('aria-label', t('context.label'));
|
|
308
|
+
const fence = structureContext.fence;
|
|
309
|
+
if (fence) {
|
|
310
|
+
const pair = fences(fence);
|
|
311
|
+
contextFooter.append(element('span', 'me-context-label', translate(snapshot.locale, 'context.current', {
|
|
312
|
+
kind: t('presentation.fence') + ' ' + pair.join(' … '),
|
|
313
|
+
})));
|
|
314
|
+
const actions = element('span', 'me-context-actions');
|
|
315
|
+
for (const [kind, pair] of Object.entries(fencePairs)) {
|
|
316
|
+
const label = t(mathStructures.find((item) => item.kind === kind).label);
|
|
317
|
+
const button = element('button', 'me-context-button', pair.join(' □ '));
|
|
318
|
+
button.type = 'button';
|
|
319
|
+
button.disabled = composing;
|
|
320
|
+
button.title = label;
|
|
321
|
+
button.setAttribute('aria-label', label);
|
|
322
|
+
button.setAttribute('aria-pressed', String(fence.type === kind));
|
|
323
|
+
button.onclick = () => {
|
|
324
|
+
if (!composing) {
|
|
325
|
+
apply(changeContextFence(session.getSnapshot().state, fence.id, kind));
|
|
326
|
+
currentInput()?.focus({ preventScroll: true });
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
actions.append(button);
|
|
330
|
+
}
|
|
331
|
+
contextFooter.append(actions, element('span', 'me-context-hint', t('context.fenceKeyboardHint')));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const context = structureContext.root;
|
|
335
|
+
contextFooter.append(element('span', 'me-context-label', translate(snapshot.locale, 'context.current', { kind: t(`context.${context.kind}`) })));
|
|
336
|
+
const actions = element('span', 'me-context-actions');
|
|
337
|
+
if (context.indexCaret) {
|
|
338
|
+
const index = element('button', 'me-context-button', t('context.editIndex'));
|
|
339
|
+
index.type = 'button';
|
|
340
|
+
index.disabled = composing;
|
|
341
|
+
index.onclick = () => {
|
|
342
|
+
if (!composing)
|
|
343
|
+
session.select(context.indexCaret);
|
|
344
|
+
};
|
|
345
|
+
actions.append(index);
|
|
346
|
+
}
|
|
347
|
+
const convert = element('button', 'me-context-button', translate(snapshot.locale, 'suggestion.transform', {
|
|
348
|
+
kind: t(`context.${context.target}`),
|
|
349
|
+
}));
|
|
350
|
+
convert.type = 'button';
|
|
351
|
+
convert.disabled = composing || !context.canTransform;
|
|
352
|
+
convert.onclick = () => {
|
|
353
|
+
if (!composing)
|
|
354
|
+
apply(transformRoot(session.getSnapshot().state, context.id, context.target));
|
|
355
|
+
};
|
|
356
|
+
actions.append(convert);
|
|
357
|
+
contextFooter.append(actions, element('span', 'me-context-hint', t(context.canTransform ? 'context.keyboardHint' : 'context.squareIndexRequired')));
|
|
358
|
+
};
|
|
359
|
+
contextFooter.onmousedown = (event) => event.preventDefault();
|
|
360
|
+
contextFooter.onkeydown = (event) => {
|
|
361
|
+
event.stopPropagation();
|
|
362
|
+
if (composing || event.isComposing)
|
|
363
|
+
return;
|
|
364
|
+
if (focusContextAction(contextFooter, event.key === 'F6' ? '' : event.key)) {
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (event.key === 'Escape' || event.key === 'F6') {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
currentInput()?.focus({ preventScroll: true });
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
const focusContextFooter = (event) => {
|
|
374
|
+
if (event.key !== 'F6' || contextFooter.hidden || composing || event.isComposing)
|
|
375
|
+
return false;
|
|
376
|
+
if (!focusContextAction(contextFooter, 'F6'))
|
|
377
|
+
return false;
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
return true;
|
|
380
|
+
};
|
|
265
381
|
const render = () => {
|
|
266
382
|
if (destroyed || composing || rendering)
|
|
267
383
|
return;
|
|
@@ -288,8 +404,11 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
288
404
|
status.textContent = '';
|
|
289
405
|
const selected = (r) => spans?.find((span) => span.row.id === r.id);
|
|
290
406
|
const labels = createEditorLabels(snapshot.locale);
|
|
407
|
+
const layout = mathLayout(state.document, snapshot.mode);
|
|
291
408
|
const renderRow = (r, name, inheritedSelection = false) => {
|
|
292
409
|
const view = element('span', 'me-row');
|
|
410
|
+
const spacing = mathRunSpacing(r);
|
|
411
|
+
view.dataset.layoutStyle = layout.rows.get(r.id);
|
|
293
412
|
const selection = selected(r);
|
|
294
413
|
let offset = 0;
|
|
295
414
|
for (const child of r.children) {
|
|
@@ -311,6 +430,14 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
311
430
|
run.dataset.runId = child.id;
|
|
312
431
|
run.dataset.value = token.text;
|
|
313
432
|
run.dataset.tokenKind = token.kind;
|
|
433
|
+
if (!isLiteralText(state.document, child.id)) {
|
|
434
|
+
const gap = spacing.get(`${child.id}:${token.start}`);
|
|
435
|
+
run.dataset.mathSpacing = 'true';
|
|
436
|
+
run.style.setProperty('--me-space-before', String(gap?.before ?? 0));
|
|
437
|
+
run.style.setProperty('--me-space-after', String(gap?.after ?? 0));
|
|
438
|
+
run.style.setProperty('--me-tight-before', String(gap?.tightBefore ?? 0));
|
|
439
|
+
run.style.setProperty('--me-tight-after', String(gap?.tightAfter ?? 0));
|
|
440
|
+
}
|
|
314
441
|
run.dataset.tokenStart = String(token.start);
|
|
315
442
|
run.dataset.tokenEnd = String(token.end);
|
|
316
443
|
const mirror = element('span', 'me-measure', token.text || (boundary ? '' : '□'));
|
|
@@ -366,6 +493,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
366
493
|
cancelDrag();
|
|
367
494
|
composing = true;
|
|
368
495
|
toolbar?.setDisabled(true);
|
|
496
|
+
renderContextFooter();
|
|
369
497
|
menu.hidden = true;
|
|
370
498
|
});
|
|
371
499
|
input.addEventListener('compositionend', () => {
|
|
@@ -374,7 +502,12 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
374
502
|
dismissed = '';
|
|
375
503
|
apply(replaceToken());
|
|
376
504
|
});
|
|
377
|
-
input.onkeydown =
|
|
505
|
+
input.onkeydown = (event) => {
|
|
506
|
+
// Native arrow movement can precede select/keyup, especially during key repeat.
|
|
507
|
+
// Read the live caret before testing token and structure boundaries.
|
|
508
|
+
syncCaret();
|
|
509
|
+
keydown(event);
|
|
510
|
+
};
|
|
378
511
|
run.append(input);
|
|
379
512
|
}
|
|
380
513
|
else {
|
|
@@ -424,6 +557,19 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
424
557
|
}
|
|
425
558
|
const structure = element('span', `me-structure me-${child.type}`);
|
|
426
559
|
structure.dataset.structure = child.type;
|
|
560
|
+
const structureSpacing = spacing.get(child.id);
|
|
561
|
+
if (structureSpacing) {
|
|
562
|
+
structure.dataset.mathSpacing = 'true';
|
|
563
|
+
structure.style.setProperty('--me-space-before', String(structureSpacing.before));
|
|
564
|
+
structure.style.setProperty('--me-space-after', String(structureSpacing.after));
|
|
565
|
+
structure.style.setProperty('--me-tight-before', String(structureSpacing.tightBefore ?? 0));
|
|
566
|
+
structure.style.setProperty('--me-tight-after', String(structureSpacing.tightAfter ?? 0));
|
|
567
|
+
}
|
|
568
|
+
const operatorLayout = layout.operators.get(child.id);
|
|
569
|
+
if (operatorLayout) {
|
|
570
|
+
structure.dataset.operatorStyle = operatorLayout.style;
|
|
571
|
+
structure.dataset.layoutLimits = operatorLayout.placement;
|
|
572
|
+
}
|
|
427
573
|
if (child.mathStyle)
|
|
428
574
|
structure.dataset.mathStyle = child.mathStyle;
|
|
429
575
|
if (['limit', 'limsup', 'liminf'].includes(child.type)) {
|
|
@@ -507,6 +653,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
507
653
|
const start = Number(input.dataset.tokenStart);
|
|
508
654
|
input.setSelectionRange(state.caret.start - start, state.caret.end - start);
|
|
509
655
|
}
|
|
656
|
+
renderContextFooter();
|
|
510
657
|
rendering = false;
|
|
511
658
|
drawMenu();
|
|
512
659
|
};
|
|
@@ -548,6 +695,38 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
548
695
|
}
|
|
549
696
|
return false;
|
|
550
697
|
}
|
|
698
|
+
function tokenNavigation(event) {
|
|
699
|
+
if (composing || !isTokenNavigationKey(event, win.navigator.platform))
|
|
700
|
+
return false;
|
|
701
|
+
const snapshot = session.getSnapshot();
|
|
702
|
+
if (snapshot.matrixRange)
|
|
703
|
+
return false;
|
|
704
|
+
event.preventDefault();
|
|
705
|
+
const state = snapshot.state;
|
|
706
|
+
const selection = snapshot.range ??
|
|
707
|
+
(state.caret.start !== state.caret.end
|
|
708
|
+
? {
|
|
709
|
+
anchor: { id: state.caret.id, offset: state.caret.start },
|
|
710
|
+
focus: { id: state.caret.id, offset: state.caret.end },
|
|
711
|
+
}
|
|
712
|
+
: undefined);
|
|
713
|
+
if (event.shiftKey) {
|
|
714
|
+
session.selectRange(extendKeyboardRange(state, selection, event.key, 'token'));
|
|
715
|
+
surface.focus();
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const point = collapseKeyboardRange(state.document, selection, event.key) ??
|
|
719
|
+
extendKeyboardRange(state, undefined, event.key, 'token').focus;
|
|
720
|
+
editing = true;
|
|
721
|
+
session.select({
|
|
722
|
+
id: point.id,
|
|
723
|
+
start: point.offset,
|
|
724
|
+
end: point.offset,
|
|
725
|
+
affinity: event.key === 'ArrowLeft' ? 'forward' : 'backward',
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
551
730
|
function keyboardSelect(event) {
|
|
552
731
|
if (composing ||
|
|
553
732
|
event.isComposing ||
|
|
@@ -563,6 +742,40 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
563
742
|
surface.focus();
|
|
564
743
|
return true;
|
|
565
744
|
}
|
|
745
|
+
function directSelectionKey(event) {
|
|
746
|
+
if (composing || event.isComposing)
|
|
747
|
+
return false;
|
|
748
|
+
const snapshot = session.getSnapshot();
|
|
749
|
+
const next = wrapSelectionShortcut(snapshot.state, snapshot.range, event);
|
|
750
|
+
if (!next)
|
|
751
|
+
return false;
|
|
752
|
+
event.preventDefault();
|
|
753
|
+
if (next !== snapshot.state)
|
|
754
|
+
apply(next);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
function selectionMenuKey(event) {
|
|
758
|
+
if (menu.hidden ||
|
|
759
|
+
menu.dataset.kind !== 'selection' ||
|
|
760
|
+
event.shiftKey ||
|
|
761
|
+
event.ctrlKey ||
|
|
762
|
+
event.metaKey ||
|
|
763
|
+
!['ArrowUp', 'ArrowDown', 'Enter'].includes(event.key))
|
|
764
|
+
return false;
|
|
765
|
+
const items = [...menu.querySelectorAll('[role="option"]')];
|
|
766
|
+
if (!items.length)
|
|
767
|
+
return false;
|
|
768
|
+
// The visible menu owns vertical arrows before a model range can collapse.
|
|
769
|
+
event.preventDefault();
|
|
770
|
+
if (event.key === 'Enter')
|
|
771
|
+
items[candidateIndex % items.length].click();
|
|
772
|
+
else {
|
|
773
|
+
candidateIndex =
|
|
774
|
+
(candidateIndex + (event.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length;
|
|
775
|
+
drawMenu();
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
566
779
|
function keyboardCollapse(event) {
|
|
567
780
|
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)
|
|
568
781
|
return false;
|
|
@@ -576,15 +789,43 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
576
789
|
return true;
|
|
577
790
|
}
|
|
578
791
|
function keydown(event) {
|
|
792
|
+
if (!['ArrowUp', 'ArrowDown', 'Escape'].includes(event.key) ||
|
|
793
|
+
event.shiftKey ||
|
|
794
|
+
event.altKey ||
|
|
795
|
+
event.ctrlKey ||
|
|
796
|
+
event.metaKey)
|
|
797
|
+
verticalNavigation.reset();
|
|
579
798
|
event.stopPropagation();
|
|
580
799
|
if (event.isComposing || composing)
|
|
581
800
|
return;
|
|
801
|
+
if (focusContextFooter(event))
|
|
802
|
+
return;
|
|
803
|
+
if (event.altKey &&
|
|
804
|
+
!event.shiftKey &&
|
|
805
|
+
!event.ctrlKey &&
|
|
806
|
+
!event.metaKey &&
|
|
807
|
+
event.key === 'ArrowDown') {
|
|
808
|
+
event.preventDefault();
|
|
809
|
+
dismissed = '';
|
|
810
|
+
const result = candidates();
|
|
811
|
+
candidateIndex = Math.max(0, result.candidates.findIndex((item) => item.transformFenceId || item.transformRootId));
|
|
812
|
+
const snapshot = session.getSnapshot();
|
|
813
|
+
const active = textNodes(snapshot.state.document).find((node) => node.id === snapshot.state.caret.id);
|
|
814
|
+
operandMenuKey = `${active.id}:${active.text}:${snapshot.state.caret.start}`;
|
|
815
|
+
drawMenu();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
582
818
|
if (event.altKey && event.shiftKey && event.code === 'KeyV') {
|
|
583
819
|
event.preventDefault();
|
|
584
820
|
openLatex();
|
|
585
821
|
return;
|
|
586
822
|
}
|
|
587
|
-
if (matrixSelectionKey(event) ||
|
|
823
|
+
if (matrixSelectionKey(event) ||
|
|
824
|
+
tokenNavigation(event) ||
|
|
825
|
+
keyboardSelect(event) ||
|
|
826
|
+
directSelectionKey(event) ||
|
|
827
|
+
selectionMenuKey(event) ||
|
|
828
|
+
keyboardCollapse(event))
|
|
588
829
|
return;
|
|
589
830
|
const snapshot = session.getSnapshot(), state = snapshot.state, c = state.caret, active = textNodes(state.document).find((n) => n.id === c.id);
|
|
590
831
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'z') {
|
|
@@ -598,7 +839,10 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
598
839
|
if (result.candidates.length && ['ArrowDown', 'ArrowUp'].includes(event.key) && !event.altKey) {
|
|
599
840
|
event.preventDefault();
|
|
600
841
|
const key = `${active.id}:${active.text}:${c.start}`;
|
|
601
|
-
const initialOperandChoice = result.candidates[0]?.wrapOperand
|
|
842
|
+
const initialOperandChoice = (result.candidates[0]?.wrapOperand ||
|
|
843
|
+
result.candidates[0]?.transformRootId ||
|
|
844
|
+
result.candidates[0]?.transformFenceId) &&
|
|
845
|
+
operandMenuKey !== key;
|
|
602
846
|
operandMenuKey = key;
|
|
603
847
|
candidateIndex = initialOperandChoice
|
|
604
848
|
? event.key === 'ArrowDown'
|
|
@@ -629,6 +873,17 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
629
873
|
}
|
|
630
874
|
return;
|
|
631
875
|
}
|
|
876
|
+
const acceptsSuggestion = event.key === 'Enter' &&
|
|
877
|
+
!event.shiftKey &&
|
|
878
|
+
result.candidates.length > 0 &&
|
|
879
|
+
((!result.candidates[0].wrapOperand &&
|
|
880
|
+
!result.candidates[0].transformRootId &&
|
|
881
|
+
!result.candidates[0].transformFenceId) ||
|
|
882
|
+
operandMenuKey === `${active.id}:${active.text}:${c.start}`);
|
|
883
|
+
if (options.onEditKeyDown?.(event, snapshot, acceptsSuggestion)) {
|
|
884
|
+
event.preventDefault();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
632
887
|
if (event.key === 'Enter') {
|
|
633
888
|
event.preventDefault();
|
|
634
889
|
const action = mathEnterAction({
|
|
@@ -636,7 +891,9 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
636
891
|
behavior: options.enterBehavior,
|
|
637
892
|
shiftKey: event.shiftKey,
|
|
638
893
|
hasSuggestion: result.candidates.length > 0 &&
|
|
639
|
-
(!result.candidates[0].wrapOperand
|
|
894
|
+
((!result.candidates[0].wrapOperand &&
|
|
895
|
+
!result.candidates[0].transformRootId &&
|
|
896
|
+
!result.candidates[0].transformFenceId) ||
|
|
640
897
|
operandMenuKey === `${active.id}:${active.text}:${c.start}`),
|
|
641
898
|
gridKind: activeGrid(state)?.matrix.type,
|
|
642
899
|
});
|
|
@@ -657,7 +914,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
657
914
|
if (activeGrid(state) &&
|
|
658
915
|
event.altKey &&
|
|
659
916
|
event.shiftKey &&
|
|
660
|
-
['ArrowUp', '
|
|
917
|
+
['ArrowUp', 'Backspace'].includes(event.key)) {
|
|
661
918
|
event.preventDefault();
|
|
662
919
|
apply(resizeMatrix(state, event.key === 'ArrowUp' ? 'row' : 'column', 'delete'));
|
|
663
920
|
return;
|
|
@@ -696,7 +953,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
696
953
|
}
|
|
697
954
|
if (!event.shiftKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
|
698
955
|
const direction = event.key === 'ArrowUp' ? -1 : 1;
|
|
699
|
-
const next =
|
|
956
|
+
const next = verticalNavigation.move(state, direction, renderedCaretGeometry(surface));
|
|
700
957
|
if (next !== state) {
|
|
701
958
|
event.preventDefault();
|
|
702
959
|
session.select(next.caret);
|
|
@@ -704,6 +961,12 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
704
961
|
return;
|
|
705
962
|
}
|
|
706
963
|
if (c.start === c.end && ['Backspace', 'Delete'].includes(event.key)) {
|
|
964
|
+
const unwrapped = unwrapEmptySlot(state);
|
|
965
|
+
if (unwrapped !== state) {
|
|
966
|
+
event.preventDefault();
|
|
967
|
+
apply(unwrapped);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
707
970
|
const input = currentInput();
|
|
708
971
|
const left = event.key === 'Backspace';
|
|
709
972
|
if ((left && c.start > 0 && c.start === Number(input?.dataset.tokenStart)) ||
|
|
@@ -728,6 +991,14 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
728
991
|
}
|
|
729
992
|
return;
|
|
730
993
|
}
|
|
994
|
+
if (event.key === 'Delete' && c.end === active.text.length) {
|
|
995
|
+
const joined = joinNextLine(state);
|
|
996
|
+
const next = joined !== state ? joined : unwrapNext(state);
|
|
997
|
+
if (next !== state) {
|
|
998
|
+
event.preventDefault();
|
|
999
|
+
apply(next);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
731
1002
|
if (event.key === 'Backspace' && c.start === 0) {
|
|
732
1003
|
const joined = joinPreviousLine(state), next = joined !== state ? joined : unwrapPrevious(state);
|
|
733
1004
|
if (next !== state) {
|
|
@@ -777,6 +1048,7 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
777
1048
|
};
|
|
778
1049
|
};
|
|
779
1050
|
surface.onpointerdown = (event) => {
|
|
1051
|
+
verticalNavigation.reset();
|
|
780
1052
|
if (event.button || composing || drag || event.target.closest('.me-line-number'))
|
|
781
1053
|
return;
|
|
782
1054
|
const point = pointAt(event.clientX, event.clientY);
|
|
@@ -882,37 +1154,28 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
882
1154
|
doc.addEventListener('pointercancel', cancelPointer, true);
|
|
883
1155
|
win.addEventListener('blur', cancelDrag);
|
|
884
1156
|
surface.onkeydown = (event) => {
|
|
1157
|
+
if (event.target !== currentInput())
|
|
1158
|
+
verticalNavigation.reset();
|
|
885
1159
|
if (event.target === currentInput())
|
|
886
1160
|
return;
|
|
887
1161
|
event.stopPropagation();
|
|
888
1162
|
if (composing || event.isComposing)
|
|
889
1163
|
return;
|
|
1164
|
+
if (focusContextFooter(event))
|
|
1165
|
+
return;
|
|
890
1166
|
if (event.altKey && event.shiftKey && event.code === 'KeyV') {
|
|
891
1167
|
event.preventDefault();
|
|
892
1168
|
openLatex();
|
|
893
1169
|
return;
|
|
894
1170
|
}
|
|
895
|
-
if (matrixSelectionKey(event) ||
|
|
1171
|
+
if (matrixSelectionKey(event) ||
|
|
1172
|
+
tokenNavigation(event) ||
|
|
1173
|
+
keyboardSelect(event) ||
|
|
1174
|
+
directSelectionKey(event) ||
|
|
1175
|
+
selectionMenuKey(event) ||
|
|
1176
|
+
keyboardCollapse(event))
|
|
896
1177
|
return;
|
|
897
1178
|
const snapshot = session.getSnapshot();
|
|
898
|
-
if (snapshot.range &&
|
|
899
|
-
!event.ctrlKey &&
|
|
900
|
-
!event.metaKey &&
|
|
901
|
-
!event.shiftKey &&
|
|
902
|
-
(event.key === 'Enter' || (event.altKey && ['ArrowDown', 'ArrowUp'].includes(event.key)))) {
|
|
903
|
-
const items = [...menu.querySelectorAll('[role="option"]')];
|
|
904
|
-
if (items.length) {
|
|
905
|
-
event.preventDefault();
|
|
906
|
-
if (event.key === 'Enter')
|
|
907
|
-
items[candidateIndex % items.length].click();
|
|
908
|
-
else {
|
|
909
|
-
candidateIndex =
|
|
910
|
-
(candidateIndex + (event.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length;
|
|
911
|
-
drawMenu();
|
|
912
|
-
}
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
1179
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {
|
|
917
1180
|
event.preventDefault();
|
|
918
1181
|
const nodes = textNodes(snapshot.state.document);
|
|
@@ -974,6 +1237,12 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
974
1237
|
if (composing)
|
|
975
1238
|
return;
|
|
976
1239
|
const data = event.clipboardData, fragment = parseFragment(data?.getData(MATH_CLIPBOARD_TYPE) ?? ''), text = data?.getData('text/plain') ?? '';
|
|
1240
|
+
if (data?.getData(MATH_CLIPBOARD_TYPE) && !fragment) {
|
|
1241
|
+
event.preventDefault();
|
|
1242
|
+
event.stopPropagation();
|
|
1243
|
+
notify('clipboard.invalid');
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
977
1246
|
const matrixSource = data?.getData(MATH_MATRIX_CLIPBOARD_TYPE) ?? '';
|
|
978
1247
|
const matrix = matrixSource ? parseMatrixFragment(matrixSource) : matrixFromTSV(text);
|
|
979
1248
|
const snapshot = session.getSnapshot();
|
|
@@ -1034,6 +1303,8 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
1034
1303
|
toolbar?.setDisabled(composing);
|
|
1035
1304
|
};
|
|
1036
1305
|
const unsubscribe = session.subscribe((snapshot, changed) => {
|
|
1306
|
+
if (changed)
|
|
1307
|
+
verticalNavigation.reset();
|
|
1037
1308
|
if (changed) {
|
|
1038
1309
|
cancelDrag();
|
|
1039
1310
|
selectionMenu.reset();
|
|
@@ -1042,19 +1313,19 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
1042
1313
|
if (changed)
|
|
1043
1314
|
options.onChange?.(snapshot.state.document, snapshot.latex);
|
|
1044
1315
|
});
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
if (event.target && 'nodeType' in event.target && menu.contains(event.target))
|
|
1048
|
-
return;
|
|
1316
|
+
const stopReadableLayout = followReadableMathLayout(surface);
|
|
1317
|
+
const stopPosition = followMathMenuPosition(root, () => {
|
|
1049
1318
|
if (!menu.hidden)
|
|
1050
1319
|
drawMenu();
|
|
1051
|
-
};
|
|
1052
|
-
win.addEventListener('resize', reposition);
|
|
1053
|
-
win.addEventListener('scroll', reposition, true);
|
|
1320
|
+
}, menu);
|
|
1054
1321
|
rebuildToolbar();
|
|
1055
1322
|
render();
|
|
1056
1323
|
return {
|
|
1057
1324
|
session,
|
|
1325
|
+
showHelp() {
|
|
1326
|
+
if (!destroyed)
|
|
1327
|
+
requestMathHelp(root);
|
|
1328
|
+
},
|
|
1058
1329
|
focus() {
|
|
1059
1330
|
editing = true;
|
|
1060
1331
|
render();
|
|
@@ -1073,11 +1344,12 @@ export function mountMathEditor(host, initial = {}) {
|
|
|
1073
1344
|
destroyed = true;
|
|
1074
1345
|
cancelDrag();
|
|
1075
1346
|
unsubscribe();
|
|
1347
|
+
stopHelp();
|
|
1076
1348
|
latexPanel?.destroy();
|
|
1077
1349
|
toolbar?.destroy();
|
|
1078
1350
|
root.removeEventListener('focusout', blur);
|
|
1079
|
-
|
|
1080
|
-
|
|
1351
|
+
stopPosition();
|
|
1352
|
+
stopReadableLayout();
|
|
1081
1353
|
doc.removeEventListener('pointermove', movePointer, true);
|
|
1082
1354
|
doc.removeEventListener('pointerup', endPointer, true);
|
|
1083
1355
|
doc.removeEventListener('pointercancel', cancelPointer, true);
|
package/dist/enter-policy.js
CHANGED
|
@@ -4,7 +4,7 @@ export function mathEnterAction(context) {
|
|
|
4
4
|
return 'suggestion';
|
|
5
5
|
if (context.gridKind && context.shiftKey)
|
|
6
6
|
return 'grid-row';
|
|
7
|
-
if (context.mode === 'inline' || context.behavior === 'commit')
|
|
7
|
+
if (context.mode === 'inline' || (context.behavior === 'commit' && !context.shiftKey))
|
|
8
8
|
return 'commit';
|
|
9
9
|
if (context.gridKind)
|
|
10
10
|
return context.gridKind === 'matrix' ? 'none' : 'grid-row';
|
package/dist/latex.js
CHANGED
|
@@ -391,6 +391,17 @@ class LatexParser {
|
|
|
391
391
|
return row(command);
|
|
392
392
|
if (command === 'backslash')
|
|
393
393
|
return row('\\');
|
|
394
|
+
if (command === 'char') {
|
|
395
|
+
// Canonical math-mode literal caret/tilde output. This is intentionally
|
|
396
|
+
// bounded; arbitrary TeX character codes are not part of the model API.
|
|
397
|
+
this.skip();
|
|
398
|
+
const match = this.source.slice(this.at, this.end).match(/^"([0-9a-f]+)/i);
|
|
399
|
+
const code = match ? Number.parseInt(match[1], 16) : NaN;
|
|
400
|
+
if (code !== 0x5e && code !== 0x7e)
|
|
401
|
+
this.fail('unsupported-command', 'Only literal caret and tilde character codes are supported.', start);
|
|
402
|
+
this.at += match[0].length;
|
|
403
|
+
return row(String.fromCodePoint(code));
|
|
404
|
+
}
|
|
394
405
|
if (command === 'textasciicircum' || command === 'textasciitilde') {
|
|
395
406
|
this.skip();
|
|
396
407
|
if (!this.starts('{}'))
|
package/dist/lines.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ import { type MathState } from './model.js';
|
|
|
3
3
|
export declare function splitLine(state: MathState): MathState;
|
|
4
4
|
/** Backspace at the start of a top-level line merges it with the previous line. */
|
|
5
5
|
export declare function joinPreviousLine(state: MathState): MathState;
|
|
6
|
+
/** Delete at the end of a top-level line joins the following line without moving the caret. */
|
|
7
|
+
export declare function joinNextLine(state: MathState): MathState;
|
|
6
8
|
export declare function moveLineVertical(state: MathState, direction: -1 | 1): MathState;
|