@pie-lib/math-input-mathlive 1.1.5-beta.10 → 1.1.5-beta.12

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.
@@ -0,0 +1,141 @@
1
+ import debug from 'debug';
2
+
3
+ const log = debug('pie-lib:math-input-mathlive:selection');
4
+
5
+ /**
6
+ * Widen a selection so it covers whole atoms of a single parent.
7
+ *
8
+ * MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into
9
+ * a fraction instead of over it: from before `2x` in `5x^2+2x+\frac{\pi}{2}-4`,
10
+ * five presses select `2x+` plus the `\pi` of the numerator, and copying gives
11
+ * `2x+\pi`. MathQuill treated a fraction (root, superscript...) as one unit,
12
+ * which is what authors expect.
13
+ *
14
+ * The tree is recovered from MathLive's public `getElementInfo(offset)`, where
15
+ * offset `i` describes the atom ending at caret position `i`:
16
+ *
17
+ * - a composite atom (fraction, root, script) owns the offset right *after*
18
+ * its children; the children are one level deeper;
19
+ * - each branch (numerator, denominator, radicand) opens with an empty-latex
20
+ * "start" atom.
21
+ *
22
+ * The rule: find the shallowest depth the selection touches, counting the caret
23
+ * it starts from. If the range crosses into a sibling branch at that depth, the
24
+ * real common parent is one level up. Then push both ends out of anything
25
+ * deeper. A script atom (`^2`) is kept with its base, since MathLive stores
26
+ * `x^2` as `x` followed by a separate `^2` atom.
27
+ *
28
+ * Pure, so it can be tested without a browser.
29
+ *
30
+ * @param {number} start
31
+ * @param {number} end
32
+ * @param {function(number): {depth?: number, latex?: string}} info
33
+ * @param {number} lastOffset
34
+ * @returns {[number, number]} the widened range, start <= end
35
+ */
36
+ export const expandRange = (start, end, info, lastOffset) => {
37
+ const at = (i) => info(i) || {};
38
+ const depth = (i) => at(i).depth || 0;
39
+ const isBranchStart = (i) => depth(i) > 0 && at(i).latex === '';
40
+
41
+ if (end < start) {
42
+ [start, end] = [end, start];
43
+ }
44
+
45
+ if (end - start < 1) {
46
+ return [start, end];
47
+ }
48
+
49
+ let target = Infinity;
50
+
51
+ for (let i = start; i <= end; i++) {
52
+ target = Math.min(target, depth(i));
53
+ }
54
+
55
+ for (let i = start + 1; i <= end; i++) {
56
+ if (depth(i) === target && isBranchStart(i)) {
57
+ target -= 1;
58
+ break;
59
+ }
60
+ }
61
+
62
+ target = Math.max(target, 0);
63
+
64
+ while (start > 0 && depth(start) > target) {
65
+ start -= 1;
66
+ }
67
+
68
+ while (end < lastOffset && depth(end) > target) {
69
+ end += 1;
70
+ }
71
+
72
+ // Keep a superscript / subscript with its base.
73
+ let first = start + 1;
74
+
75
+ while (first <= end && depth(first) > target) {
76
+ first += 1;
77
+ }
78
+
79
+ if (start > 0 && first <= end && /^[\^_]/.test(at(first).latex || '') && depth(start) === target) {
80
+ start -= 1;
81
+ }
82
+
83
+ return [start, end];
84
+ };
85
+
86
+ /**
87
+ * Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).
88
+ *
89
+ * This fixes both what the user sees highlighted and what copy/cut put on the
90
+ * clipboard, since both come from the selection. The selection direction is
91
+ * preserved, so further Shift+Arrow presses keep extending the same end.
92
+ *
93
+ * @param {HTMLElement} mathField a MathLive `<math-field>`
94
+ * @returns {function|undefined} teardown, to call on unmount
95
+ */
96
+ export const trackWholeAtomSelection = (mathField) => {
97
+ if (!mathField || typeof mathField.addEventListener !== 'function') {
98
+ return undefined;
99
+ }
100
+
101
+ let applying = false;
102
+
103
+ const update = () => {
104
+ if (applying) {
105
+ return;
106
+ }
107
+
108
+ try {
109
+ if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {
110
+ return;
111
+ }
112
+
113
+ const selection = mathField.selection;
114
+ const range = selection && selection.ranges && selection.ranges[0];
115
+
116
+ if (!range) {
117
+ return;
118
+ }
119
+
120
+ const [start, end] = range;
121
+ const lo = Math.min(start, end);
122
+ const hi = Math.max(start, end);
123
+ const [nextStart, nextEnd] = expandRange(lo, hi, (i) => mathField.getElementInfo(i), mathField.lastOffset);
124
+
125
+ if (nextStart === lo && nextEnd === hi) {
126
+ return;
127
+ }
128
+
129
+ applying = true;
130
+ mathField.selection = { ranges: [[nextStart, nextEnd]], direction: selection.direction };
131
+ } catch (e) {
132
+ log('could not normalise the selection: %s', e && e.message);
133
+ } finally {
134
+ applying = false;
135
+ }
136
+ };
137
+
138
+ mathField.addEventListener('selection-change', update);
139
+
140
+ return () => mathField.removeEventListener('selection-change', update);
141
+ };