@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.
Files changed (180) hide show
  1. package/ADAPTERS.md +25 -14
  2. package/API-JAVASCRIPT.md +1 -1
  3. package/API-REACT.md +5 -1
  4. package/API-SESSION.md +168 -6
  5. package/API-SOLID.md +1 -1
  6. package/API-SVELTE.md +1 -1
  7. package/API-VUE.md +1 -1
  8. package/API-WEB-COMPONENT.md +1 -1
  9. package/CHANGELOG.md +45 -0
  10. package/EMBEDDING.md +4 -0
  11. package/IMPLEMENTATION.md +27 -2
  12. package/JSON-MODEL.md +1 -0
  13. package/LATEX-GUIDE.md +8 -1
  14. package/LATEX-SCOPE.md +2 -2
  15. package/LICENSE +1 -3
  16. package/README.md +145 -37
  17. package/ROADMAP.md +93 -11
  18. package/STYLING.md +157 -0
  19. package/SUPPORT.md +24 -12
  20. package/VALIDATION.md +113 -4
  21. package/dist/core.d.ts +5 -1
  22. package/dist/core.js +5 -1
  23. package/dist/document-codec.d.ts +0 -1
  24. package/dist/document-codec.js +0 -1
  25. package/dist/dom/caret-geometry.d.ts +3 -0
  26. package/dist/dom/caret-geometry.js +41 -0
  27. package/dist/dom/elements.d.ts +1 -2
  28. package/dist/dom/elements.js +2 -3
  29. package/dist/dom/latex-panel.d.ts +10 -0
  30. package/dist/dom/latex-panel.js +59 -0
  31. package/dist/dom/matrix-selection.d.ts +11 -0
  32. package/dist/dom/matrix-selection.js +30 -0
  33. package/dist/dom/menu-position.d.ts +14 -2
  34. package/dist/dom/menu-position.js +84 -15
  35. package/dist/dom/presentation-controls.d.ts +4 -0
  36. package/dist/dom/presentation-controls.js +56 -0
  37. package/dist/dom/quick-panel.d.ts +12 -0
  38. package/dist/dom/quick-panel.js +102 -0
  39. package/dist/dom/selection.d.ts +29 -0
  40. package/dist/dom/selection.js +53 -0
  41. package/dist/dom/symbol-panel.d.ts +9 -0
  42. package/dist/dom/symbol-panel.js +146 -0
  43. package/dist/dom/theme.d.ts +6 -0
  44. package/dist/dom/theme.js +68 -0
  45. package/dist/dom/toolbar-catalog.d.ts +10 -0
  46. package/dist/dom/toolbar-catalog.js +35 -0
  47. package/dist/dom/toolbar.d.ts +13 -2
  48. package/dist/dom/toolbar.js +295 -28
  49. package/dist/dom.d.ts +7 -1
  50. package/dist/dom.js +510 -128
  51. package/dist/editor-labels.d.ts +0 -1
  52. package/dist/editor-labels.js +0 -1
  53. package/dist/enter-policy.d.ts +0 -1
  54. package/dist/enter-policy.js +0 -1
  55. package/dist/fences.d.ts +0 -1
  56. package/dist/fences.js +0 -1
  57. package/dist/i18n.d.ts +0 -1
  58. package/dist/i18n.js +0 -1
  59. package/dist/index.d.ts +4 -1
  60. package/dist/index.js +4 -1
  61. package/dist/latex-insertion.d.ts +12 -0
  62. package/dist/latex-insertion.js +26 -0
  63. package/dist/latex-paste-panel.d.ts +3 -0
  64. package/dist/latex-paste-panel.js +18 -0
  65. package/dist/latex.d.ts +0 -1
  66. package/dist/latex.js +0 -1
  67. package/dist/lines.d.ts +0 -1
  68. package/dist/lines.js +0 -1
  69. package/dist/locales/en.js +27 -1
  70. package/dist/locales/en.json +27 -1
  71. package/dist/locales/ko.js +27 -1
  72. package/dist/locales/ko.json +27 -1
  73. package/dist/math-editor-toolbar.d.ts +4 -2
  74. package/dist/math-editor-toolbar.js +3 -4
  75. package/dist/math-editor.d.ts +5 -2
  76. package/dist/math-editor.js +285 -46
  77. package/dist/matrix-range.d.ts +40 -0
  78. package/dist/matrix-range.js +242 -0
  79. package/dist/matrix.d.ts +0 -1
  80. package/dist/matrix.js +0 -1
  81. package/dist/model.d.ts +0 -1
  82. package/dist/model.js +0 -1
  83. package/dist/outputs.d.ts +0 -1
  84. package/dist/outputs.js +0 -1
  85. package/dist/preferences.d.ts +24 -0
  86. package/dist/preferences.js +66 -0
  87. package/dist/presentation-controls.d.ts +10 -0
  88. package/dist/presentation-controls.js +10 -0
  89. package/dist/presentation.d.ts +13 -0
  90. package/dist/presentation.js +86 -0
  91. package/dist/quick-panel.d.ts +2 -0
  92. package/dist/quick-panel.js +17 -0
  93. package/dist/range.d.ts +2 -1
  94. package/dist/range.js +11 -1
  95. package/dist/react.d.ts +0 -1
  96. package/dist/react.js +0 -1
  97. package/dist/session.d.ts +31 -2
  98. package/dist/session.js +108 -7
  99. package/dist/solid.d.ts +0 -1
  100. package/dist/solid.js +0 -1
  101. package/dist/suggestions.d.ts +4 -1
  102. package/dist/suggestions.js +100 -12
  103. package/dist/svelte.d.ts +0 -1
  104. package/dist/svelte.js +0 -1
  105. package/dist/symbol-browser.d.ts +0 -1
  106. package/dist/symbol-browser.js +0 -1
  107. package/dist/symbols.d.ts +0 -1
  108. package/dist/symbols.js +0 -1
  109. package/dist/templates.d.ts +0 -1
  110. package/dist/templates.js +0 -1
  111. package/dist/tokens.d.ts +0 -1
  112. package/dist/tokens.js +0 -1
  113. package/dist/vertical-navigation.d.ts +15 -0
  114. package/dist/vertical-navigation.js +145 -0
  115. package/dist/vue.d.ts +0 -1
  116. package/dist/vue.js +0 -1
  117. package/dist/web-component.d.ts +0 -1
  118. package/dist/web-component.js +0 -1
  119. package/package.json +1 -1
  120. package/src/style.css +254 -129
  121. package/dist/core.d.ts.map +0 -1
  122. package/dist/core.js.map +0 -1
  123. package/dist/document-codec.d.ts.map +0 -1
  124. package/dist/document-codec.js.map +0 -1
  125. package/dist/dom/elements.d.ts.map +0 -1
  126. package/dist/dom/elements.js.map +0 -1
  127. package/dist/dom/menu-position.d.ts.map +0 -1
  128. package/dist/dom/menu-position.js.map +0 -1
  129. package/dist/dom/toolbar.d.ts.map +0 -1
  130. package/dist/dom/toolbar.js.map +0 -1
  131. package/dist/dom.d.ts.map +0 -1
  132. package/dist/dom.js.map +0 -1
  133. package/dist/editor-labels.d.ts.map +0 -1
  134. package/dist/editor-labels.js.map +0 -1
  135. package/dist/enter-policy.d.ts.map +0 -1
  136. package/dist/enter-policy.js.map +0 -1
  137. package/dist/fences.d.ts.map +0 -1
  138. package/dist/fences.js.map +0 -1
  139. package/dist/i18n.d.ts.map +0 -1
  140. package/dist/i18n.js.map +0 -1
  141. package/dist/index.d.ts.map +0 -1
  142. package/dist/index.js.map +0 -1
  143. package/dist/latex.d.ts.map +0 -1
  144. package/dist/latex.js.map +0 -1
  145. package/dist/lines.d.ts.map +0 -1
  146. package/dist/lines.js.map +0 -1
  147. package/dist/math-editor-toolbar.d.ts.map +0 -1
  148. package/dist/math-editor-toolbar.js.map +0 -1
  149. package/dist/math-editor.d.ts.map +0 -1
  150. package/dist/math-editor.js.map +0 -1
  151. package/dist/matrix.d.ts.map +0 -1
  152. package/dist/matrix.js.map +0 -1
  153. package/dist/model.d.ts.map +0 -1
  154. package/dist/model.js.map +0 -1
  155. package/dist/outputs.d.ts.map +0 -1
  156. package/dist/outputs.js.map +0 -1
  157. package/dist/range.d.ts.map +0 -1
  158. package/dist/range.js.map +0 -1
  159. package/dist/react.d.ts.map +0 -1
  160. package/dist/react.js.map +0 -1
  161. package/dist/session.d.ts.map +0 -1
  162. package/dist/session.js.map +0 -1
  163. package/dist/solid.d.ts.map +0 -1
  164. package/dist/solid.js.map +0 -1
  165. package/dist/suggestions.d.ts.map +0 -1
  166. package/dist/suggestions.js.map +0 -1
  167. package/dist/svelte.d.ts.map +0 -1
  168. package/dist/svelte.js.map +0 -1
  169. package/dist/symbol-browser.d.ts.map +0 -1
  170. package/dist/symbol-browser.js.map +0 -1
  171. package/dist/symbols.d.ts.map +0 -1
  172. package/dist/symbols.js.map +0 -1
  173. package/dist/templates.d.ts.map +0 -1
  174. package/dist/templates.js.map +0 -1
  175. package/dist/tokens.d.ts.map +0 -1
  176. package/dist/tokens.js.map +0 -1
  177. package/dist/vue.d.ts.map +0 -1
  178. package/dist/vue.js.map +0 -1
  179. package/dist/web-component.d.ts.map +0 -1
  180. 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 { element } from './dom/elements.js';
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, moveMatrixVertical, resizeMatrix } from './matrix.js';
13
- import { joinPreviousLine, moveLineVertical } from './lines.js';
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"]') ?? document.body).append(menu);
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 drawMenu = () => {
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.hidden = false;
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 = element('button', '', `${item.glyph} ${t(item.label)}`);
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
- positionMathMenu(menu, rect, 340);
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 = element('button', '', `${item.glyph} ${item.label}`);
133
- button.type = '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
- positionMathMenu(menu, rect, 230);
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 boundary = !child.text && r.children.length > 1;
172
- const run = element('span', `me-run${child.text ? '' : ' me-empty'}${boundary ? ' me-empty-boundary' : ''}`);
173
- run.dataset.runId = child.id;
174
- run.dataset.value = child.text;
175
- const mirror = element('span', 'me-measure', child.text || (boundary ? '' : '□'));
176
- mirror.setAttribute('aria-hidden', 'true');
177
- run.append(mirror);
178
- const active = editing && !snapshot.range && state.caret.id === child.id;
179
- if (active) {
180
- const input = element('input', 'me-input');
181
- input.value = child.text;
182
- input.placeholder = '□';
183
- input.setAttribute('aria-label', name);
184
- input.setAttribute('role', 'combobox');
185
- input.setAttribute('aria-expanded', 'false');
186
- input.autocomplete = 'off';
187
- input.spellcheck = false;
188
- const syncCaret = () => {
189
- if (input.isConnected && !rendering && !composing) {
190
- rendering = true;
191
- session.select({
192
- id: child.id,
193
- start: input.selectionStart ?? 0,
194
- end: input.selectionEnd ?? 0,
195
- });
196
- rendering = false;
197
- drawMenu();
198
- }
199
- };
200
- input.onselect = syncCaret;
201
- input.onkeyup = syncCaret;
202
- input.oninput = () => {
203
- if (composing) {
204
- mirror.textContent = input.value || '□';
205
- return;
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
- const next = setText(session.getSnapshot().state, input.value, input.selectionStart ?? 0, input.selectionEnd ?? 0);
208
- dismissed = '';
209
- candidateIndex = 0;
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
- if (highlighted)
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
- input.setSelectionRange(state.caret.start, state.caret.end);
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 (keyboardSelect(event))
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 (result.candidates.length) {
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 === 0) ||
450
- (event.key === 'ArrowRight' && c.end === active.text.length)))) {
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 next = moveCaret(state, direction);
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 = activeGrid(state)
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
- for (let i = 0; i <= value.length; i++) {
511
- const range = document.createRange();
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 { id: best.dataset.runId, offset };
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.preventDefault();
784
+ event.stopPropagation();
785
+ selectionMenu.reset();
530
786
  candidateIndex = 0;
531
- drag = { point, x: event.clientX, y: event.clientY, moved: false };
532
- surface.setPointerCapture(event.pointerId);
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
- surface.onpointermove = (event) => {
536
- if (!drag || Math.hypot(event.clientX - drag.x, event.clientY - drag.y) < 4)
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
- session.selectRange({ anchor: drag.point, focus: point });
543
- surface.focus();
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
- surface.onpointerup = (event) => {
848
+ const endPointer = (event) => {
547
849
  const previous = drag;
548
- drag = undefined;
549
- if (surface.hasPointerCapture(event.pointerId))
550
- surface.releasePointerCapture(event.pointerId);
551
- if (previous && !previous.moved) {
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
- surface.onpointercancel = () => {
561
- drag = undefined;
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 instanceof HTMLInputElement)
885
+ if (event.target === currentInput())
565
886
  return;
566
887
  event.stopPropagation();
567
- if (keyboardSelect(event))
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 && ['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)) {
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
- event.clipboardData?.setData(MATH_CLIPBOARD_TYPE, JSON.stringify(fragment));
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
- window.addEventListener('resize', reposition);
679
- window.addEventListener('scroll', reposition, true);
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
- window.removeEventListener('resize', reposition);
702
- window.removeEventListener('scroll', reposition, true);
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