@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.
- package/lib/__tests__/input-selection.test.js +103 -0
- package/lib/__tests__/keypad-label.test.js +13 -1
- package/lib/__tests__/latex-bridge.test.js +29 -1
- package/lib/__tests__/longdiv-wiring.test.js +215 -0
- package/lib/__tests__/longdiv.test.js +281 -0
- package/lib/__tests__/selection.test.js +195 -0
- package/lib/keypad/index.js +9 -1
- package/lib/keypad/index.js.map +1 -1
- package/lib/longdiv.js +173 -0
- package/lib/longdiv.js.map +1 -0
- package/lib/mathlive-instance.js +4 -0
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/input.js +16 -0
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +13 -1
- package/lib/mf/static.js.map +1 -1
- package/lib/selection.js +148 -0
- package/lib/selection.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/input-selection.test.jsx +64 -0
- package/src/__tests__/longdiv-wiring.test.jsx +137 -0
- package/src/__tests__/longdiv.test.js +259 -0
- package/src/__tests__/selection.test.js +247 -0
- package/src/keypad/index.jsx +14 -2
- package/src/longdiv.js +172 -0
- package/src/mathlive-instance.js +4 -0
- package/src/mf/input.jsx +18 -0
- package/src/mf/static.jsx +15 -1
- package/src/selection.js +141 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { expandRange, trackWholeAtomSelection } from '../selection';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Offset tables captured from MathLive 0.110.0 `getElementInfo(i)` in Chrome,
|
|
5
|
+
* as [depth, latex] per offset. Offset `i` is the atom ending at caret `i`.
|
|
6
|
+
*/
|
|
7
|
+
const table = (rows) => (i) => (rows[i] ? { depth: rows[i][0], latex: rows[i][1] } : undefined);
|
|
8
|
+
|
|
9
|
+
// 5x^2+2x+\frac{\pi}{2}-4
|
|
10
|
+
const POLY = table([
|
|
11
|
+
[0, ''],
|
|
12
|
+
[0, '5'],
|
|
13
|
+
[0, 'x'],
|
|
14
|
+
[1, ''],
|
|
15
|
+
[1, '2'],
|
|
16
|
+
[0, '^2'],
|
|
17
|
+
[0, '+'],
|
|
18
|
+
[0, '2'],
|
|
19
|
+
[0, 'x'],
|
|
20
|
+
[0, '+'],
|
|
21
|
+
[1, ''],
|
|
22
|
+
[1, '\\pi'],
|
|
23
|
+
[1, ''],
|
|
24
|
+
[1, '2'],
|
|
25
|
+
[0, '\\frac{\\pi}{2}'],
|
|
26
|
+
[0, '-'],
|
|
27
|
+
[0, '4'],
|
|
28
|
+
]);
|
|
29
|
+
const POLY_LAST = 16;
|
|
30
|
+
|
|
31
|
+
// a+\sqrt{x+1}+b
|
|
32
|
+
const ROOT = table([
|
|
33
|
+
[0, ''],
|
|
34
|
+
[0, 'a'],
|
|
35
|
+
[0, '+'],
|
|
36
|
+
[1, ''],
|
|
37
|
+
[1, 'x'],
|
|
38
|
+
[1, '+'],
|
|
39
|
+
[1, '1'],
|
|
40
|
+
[0, '\\sqrt{x+1}'],
|
|
41
|
+
[0, '+'],
|
|
42
|
+
[0, 'b'],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// \frac{\frac{a}{b}}{c}
|
|
46
|
+
const NESTED = table([
|
|
47
|
+
[0, ''],
|
|
48
|
+
[1, ''],
|
|
49
|
+
[2, ''],
|
|
50
|
+
[2, 'a'],
|
|
51
|
+
[2, ''],
|
|
52
|
+
[2, 'b'],
|
|
53
|
+
[1, '\\frac{a}{b}'],
|
|
54
|
+
[1, ''],
|
|
55
|
+
[1, 'c'],
|
|
56
|
+
[0, '\\frac{\\frac{a}{b}}{c}'],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
describe('expandRange', () => {
|
|
60
|
+
const poly = (a, b) => expandRange(a, b, POLY, POLY_LAST);
|
|
61
|
+
|
|
62
|
+
it('leaves a selection of whole sibling atoms alone', () => {
|
|
63
|
+
expect(poly(6, 7)).toEqual([6, 7]); // 2
|
|
64
|
+
expect(poly(6, 9)).toEqual([6, 9]); // 2x+
|
|
65
|
+
expect(poly(6, 15)).toEqual([6, 15]); // 2x+\frac{\pi}{2}-
|
|
66
|
+
expect(poly(0, 16)).toEqual([0, 16]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// The reported case: five Shift+Right presses from before `2x` gave `2x+\pi`.
|
|
70
|
+
it('takes the whole fraction when the selection reaches into it', () => {
|
|
71
|
+
expect(poly(6, 10)).toEqual([6, 14]);
|
|
72
|
+
expect(poly(6, 11)).toEqual([6, 14]);
|
|
73
|
+
expect(poly(6, 13)).toEqual([6, 14]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('takes the whole fraction when the selection leaves it', () => {
|
|
77
|
+
expect(poly(11, 15)).toEqual([9, 15]); // from the pi to after the minus
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('crossing from numerator to denominator selects the fraction', () => {
|
|
81
|
+
expect(poly(11, 13)).toEqual([9, 14]);
|
|
82
|
+
expect(poly(11, 12)).toEqual([9, 14]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('a selection inside one branch stays inside it', () => {
|
|
86
|
+
expect(poly(10, 11)).toEqual([10, 11]); // \pi
|
|
87
|
+
expect(poly(12, 13)).toEqual([12, 13]); // the 2 of the denominator
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// MathLive stores x^2 as `x` then a separate `^2` atom.
|
|
91
|
+
it('keeps a superscript with its base', () => {
|
|
92
|
+
expect(poly(3, 7)).toEqual([1, 7]); // x^2+2, not ^2+2
|
|
93
|
+
expect(poly(4, 6)).toEqual([1, 6]); // x^2+
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('accepts a reversed range', () => {
|
|
97
|
+
expect(poly(11, 6)).toEqual([6, 14]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('leaves a collapsed range alone', () => {
|
|
101
|
+
expect(poly(11, 11)).toEqual([11, 11]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles roots the same way', () => {
|
|
105
|
+
expect(expandRange(2, 4, ROOT, 9)).toEqual([2, 7]);
|
|
106
|
+
expect(expandRange(4, 6, ROOT, 9)).toEqual([4, 6]); // +1 inside the radicand
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('climbs as many levels as needed in nested fractions', () => {
|
|
110
|
+
expect(expandRange(2, 3, NESTED, 9)).toEqual([2, 3]); // a
|
|
111
|
+
expect(expandRange(3, 5, NESTED, 9)).toEqual([1, 6]); // a/b crossing -> inner fraction
|
|
112
|
+
expect(expandRange(6, 7, NESTED, 9)).toEqual([0, 9]); // inner -> outer denominator
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('tolerates missing info', () => {
|
|
116
|
+
expect(expandRange(1, 3, () => undefined, 5)).toEqual([1, 3]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('trackWholeAtomSelection', () => {
|
|
121
|
+
const stubField = (range, direction = 'forward') => {
|
|
122
|
+
const listeners = {};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
selection: { ranges: [range], direction },
|
|
126
|
+
get selectionIsCollapsed() {
|
|
127
|
+
return this.selection.ranges[0][0] === this.selection.ranges[0][1];
|
|
128
|
+
},
|
|
129
|
+
lastOffset: POLY_LAST,
|
|
130
|
+
getElementInfo: POLY,
|
|
131
|
+
sets: 0,
|
|
132
|
+
addEventListener(type, fn) {
|
|
133
|
+
listeners[type] = fn;
|
|
134
|
+
},
|
|
135
|
+
removeEventListener(type, fn) {
|
|
136
|
+
if (listeners[type] === fn) {
|
|
137
|
+
delete listeners[type];
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
fire(type) {
|
|
141
|
+
if (listeners[type]) {
|
|
142
|
+
listeners[type]();
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
has(type) {
|
|
146
|
+
return !!listeners[type];
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const track = (field) => {
|
|
152
|
+
// count assignments without breaking the getter
|
|
153
|
+
let current = field.selection;
|
|
154
|
+
|
|
155
|
+
Object.defineProperty(field, 'selection', {
|
|
156
|
+
get: () => current,
|
|
157
|
+
set: (v) => {
|
|
158
|
+
field.sets += 1;
|
|
159
|
+
current = v;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return trackWholeAtomSelection(field);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
it('widens the selection on selection-change', () => {
|
|
167
|
+
const field = stubField([6, 11]);
|
|
168
|
+
|
|
169
|
+
track(field);
|
|
170
|
+
field.fire('selection-change');
|
|
171
|
+
|
|
172
|
+
expect(field.selection.ranges).toEqual([[6, 14]]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('preserves the direction, so Shift+Arrow keeps extending the same end', () => {
|
|
176
|
+
const field = stubField([11, 16], 'backward');
|
|
177
|
+
|
|
178
|
+
track(field);
|
|
179
|
+
field.fire('selection-change');
|
|
180
|
+
|
|
181
|
+
expect(field.selection).toEqual({ ranges: [[9, 16]], direction: 'backward' });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('does not touch a selection that is already whole', () => {
|
|
185
|
+
const field = stubField([6, 9]);
|
|
186
|
+
|
|
187
|
+
track(field);
|
|
188
|
+
field.fire('selection-change');
|
|
189
|
+
|
|
190
|
+
expect(field.sets).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('ignores a collapsed caret', () => {
|
|
194
|
+
const field = stubField([11, 11]);
|
|
195
|
+
|
|
196
|
+
track(field);
|
|
197
|
+
field.fire('selection-change');
|
|
198
|
+
|
|
199
|
+
expect(field.sets).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not recurse when its own assignment fires selection-change', () => {
|
|
203
|
+
const field = stubField([6, 11]);
|
|
204
|
+
let current = field.selection;
|
|
205
|
+
|
|
206
|
+
Object.defineProperty(field, 'selection', {
|
|
207
|
+
get: () => current,
|
|
208
|
+
set: (v) => {
|
|
209
|
+
field.sets += 1;
|
|
210
|
+
current = v;
|
|
211
|
+
field.fire('selection-change');
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
trackWholeAtomSelection(field);
|
|
216
|
+
field.fire('selection-change');
|
|
217
|
+
|
|
218
|
+
expect(field.sets).toBe(1);
|
|
219
|
+
expect(field.selection.ranges).toEqual([[6, 14]]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('survives a field that throws', () => {
|
|
223
|
+
const field = stubField([6, 11]);
|
|
224
|
+
|
|
225
|
+
field.getElementInfo = () => {
|
|
226
|
+
throw new Error('boom');
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
track(field);
|
|
230
|
+
|
|
231
|
+
expect(() => field.fire('selection-change')).not.toThrow();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('tears down its listener', () => {
|
|
235
|
+
const field = stubField([6, 11]);
|
|
236
|
+
const untrack = track(field);
|
|
237
|
+
|
|
238
|
+
expect(field.has('selection-change')).toBe(true);
|
|
239
|
+
untrack();
|
|
240
|
+
expect(field.has('selection-change')).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('is a no-op without a field', () => {
|
|
244
|
+
expect(trackWholeAtomSelection(undefined)).toBeUndefined();
|
|
245
|
+
expect(trackWholeAtomSelection({})).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
});
|
package/src/keypad/index.jsx
CHANGED
|
@@ -11,6 +11,7 @@ import { baseSet } from '../keys';
|
|
|
11
11
|
import { sortKeys } from './keys-layout';
|
|
12
12
|
import { commonKeyboardStyles, rootIndexStyles } from '../mf/common-styles';
|
|
13
13
|
import { loadMathLive, latexToMarkup } from '../mathlive-instance';
|
|
14
|
+
import { fixLongDivisionWhenReady } from '../longdiv';
|
|
14
15
|
|
|
15
16
|
const log = debug('pie-lib:math-input-mathlive:keypad');
|
|
16
17
|
|
|
@@ -81,11 +82,14 @@ export class LatexLabel extends React.Component {
|
|
|
81
82
|
constructor(props) {
|
|
82
83
|
super(props);
|
|
83
84
|
this.state = { markup: latexToMarkup(props.latex) };
|
|
85
|
+
this.labelRef = React.createRef();
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
async componentDidMount() {
|
|
87
89
|
// MathLive may not have finished loading when the keypad first mounts.
|
|
88
90
|
if (this.state.markup) {
|
|
91
|
+
// The long-division key's label needs MathLive's drawing corrected.
|
|
92
|
+
fixLongDivisionWhenReady(this.labelRef.current);
|
|
89
93
|
return;
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -111,10 +115,14 @@ export class LatexLabel extends React.Component {
|
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
componentDidUpdate(prevProps) {
|
|
118
|
+
componentDidUpdate(prevProps, prevState) {
|
|
115
119
|
if (prevProps.latex !== this.props.latex) {
|
|
116
120
|
this.setState({ markup: latexToMarkup(this.props.latex) });
|
|
117
121
|
}
|
|
122
|
+
|
|
123
|
+
if (this.state.markup !== (prevState && prevState.markup)) {
|
|
124
|
+
fixLongDivisionWhenReady(this.labelRef.current);
|
|
125
|
+
}
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
componentWillUnmount() {
|
|
@@ -127,7 +135,11 @@ export class LatexLabel extends React.Component {
|
|
|
127
135
|
// Fall back to the raw latex so a button is never blank.
|
|
128
136
|
return markup ? (
|
|
129
137
|
// eslint-disable-next-line react/no-danger
|
|
130
|
-
<LabelHolder
|
|
138
|
+
<LabelHolder
|
|
139
|
+
ref={this.labelRef}
|
|
140
|
+
slanted={SLANTED_LABELS.includes(this.props.latex)}
|
|
141
|
+
dangerouslySetInnerHTML={{ __html: markup }}
|
|
142
|
+
/>
|
|
131
143
|
) : (
|
|
132
144
|
<LabelHolder slanted={SLANTED_LABELS.includes(this.props.latex)}>{this.props.latex}</LabelHolder>
|
|
133
145
|
);
|
package/src/longdiv.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
|
|
3
|
+
const log = debug('pie-lib:math-input-mathlive:longdiv');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Long division (`\enclose{longdiv}{...}`, which `\longdiv` is mapped to).
|
|
7
|
+
*
|
|
8
|
+
* MathLive 0.110 draws it wrong in three ways, all in `EncloseAtom.render`:
|
|
9
|
+
*
|
|
10
|
+
* 1. It lowers the dividend by `padding + clearance` (0.45em) with
|
|
11
|
+
* `position: relative; top`, which moves it off the baseline, so the divisor
|
|
12
|
+
* looks too high.
|
|
13
|
+
* 2. The notation box's CSS width leaves out the room it made for the `)`,
|
|
14
|
+
* while the SVG's viewBox includes it. The SVG is stretched to 100% of the
|
|
15
|
+
* box, so the whole drawing is scaled down (to about 86%).
|
|
16
|
+
* 3. The bar is drawn `base.width` long from the `)`, but the dividend itself
|
|
17
|
+
* starts after the `)` and its gap, so the bar stops short of the end of
|
|
18
|
+
* the dividend. `base.width` also leaves out the spacing MathLive puts
|
|
19
|
+
* around `,` and `+`, so even the right length would come up short.
|
|
20
|
+
*
|
|
21
|
+
* This reads back the numbers MathLive wrote into inline styles and the SVG,
|
|
22
|
+
* and corrects them, so the rest of MathLive's drawing (the arc, the stroke
|
|
23
|
+
* weight) is kept. The bar's end is measured from the rendered dividend when
|
|
24
|
+
* the markup is laid out, and computed from MathLive's numbers otherwise.
|
|
25
|
+
*
|
|
26
|
+
* Every value set here is absolute, so running it again is harmless - it has
|
|
27
|
+
* to run after every render of a live field anyway.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** A length MathLive wrote as `prop:<n>em` in an inline style, or 0. */
|
|
31
|
+
const inlineEm = (style, prop) => {
|
|
32
|
+
const match = new RegExp(`(?:^|;)\\s*${prop}:\\s*(-?[\\d.]+)em`).exec(style || '');
|
|
33
|
+
|
|
34
|
+
return match ? parseFloat(match[1]) : 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Direct child with a given tag. Not `querySelector(':scope > tag')`: jsdom's
|
|
39
|
+
* selector engine does not support `:scope` inside SVG elements.
|
|
40
|
+
*/
|
|
41
|
+
const childTag = (el, tag) => el && Array.from(el.children || []).find((c) => c.tagName.toLowerCase() === tag);
|
|
42
|
+
|
|
43
|
+
/** Right edge of what is actually drawn inside an element, ignoring the caret. */
|
|
44
|
+
const drawnRight = (el) => {
|
|
45
|
+
let right = -Infinity;
|
|
46
|
+
|
|
47
|
+
el.querySelectorAll('*').forEach((child) => {
|
|
48
|
+
if (child.children.length || (child.classList && child.classList.contains('ML__caret'))) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rect = child.getBoundingClientRect();
|
|
53
|
+
|
|
54
|
+
if (rect.width > 0) {
|
|
55
|
+
right = Math.max(right, rect.right);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return right;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Correct every long division under `root`.
|
|
64
|
+
*
|
|
65
|
+
* @param {ParentNode} root a mathfield's shadow root, or an element holding
|
|
66
|
+
* static MathLive markup
|
|
67
|
+
* @returns {number} how many were corrected
|
|
68
|
+
*/
|
|
69
|
+
export const fixLongDivision = (root) => {
|
|
70
|
+
if (!root || typeof root.querySelectorAll !== 'function') {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let fixed = 0;
|
|
75
|
+
|
|
76
|
+
root.querySelectorAll('.ML__notation').forEach((notation) => {
|
|
77
|
+
try {
|
|
78
|
+
const svg = childTag(notation, 'svg');
|
|
79
|
+
const line = childTag(svg, 'line');
|
|
80
|
+
|
|
81
|
+
// Long division is the only notation MathLive draws with an arc (<path>).
|
|
82
|
+
if (!line || !childTag(svg, 'path')) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dividend = notation.nextElementSibling;
|
|
87
|
+
const style = dividend && dividend.getAttribute('style');
|
|
88
|
+
const width = inlineEm(style, 'width');
|
|
89
|
+
const gap = inlineEm(style, 'margin-left');
|
|
90
|
+
const drop = inlineEm(style, 'top');
|
|
91
|
+
const viewBox = (svg.getAttribute('viewBox') || '').trim().split(/\s+/).map(Number);
|
|
92
|
+
const start = parseFloat(line.getAttribute('x1'));
|
|
93
|
+
|
|
94
|
+
if (!width || viewBox.length !== 4 || Number.isNaN(start)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. size the box to the viewBox, so the drawing is not scaled down
|
|
99
|
+
notation.style.width = `calc(${viewBox[2]}em + 1px)`;
|
|
100
|
+
|
|
101
|
+
// 1. put the dividend back on the baseline. Growing the box upwards by the
|
|
102
|
+
// same amount keeps the drawing where it was relative to the dividend, and
|
|
103
|
+
// makes the line tall enough that the field does not clip the bar.
|
|
104
|
+
if (drop && notation.parentElement) {
|
|
105
|
+
dividend.style.top = '0';
|
|
106
|
+
notation.parentElement.style.paddingTop = `${drop}em`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. run the bar from the ")" to the end of the dividend
|
|
110
|
+
let end = start + gap + width;
|
|
111
|
+
const svgRect = svg.getBoundingClientRect();
|
|
112
|
+
const right = drawnRight(dividend);
|
|
113
|
+
|
|
114
|
+
if (svgRect.width > 0 && Number.isFinite(right)) {
|
|
115
|
+
end = (right - svgRect.left) * (viewBox[2] / svgRect.width);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
line.setAttribute('x2', String(end));
|
|
119
|
+
fixed += 1;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
log('could not correct a long division: %s', e && e.message);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return fixed;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Correct static markup now, and again once web fonts have loaded: the bar's
|
|
130
|
+
* end is measured, and glyph widths change when the math fonts arrive.
|
|
131
|
+
*
|
|
132
|
+
* @param {Element} element
|
|
133
|
+
*/
|
|
134
|
+
export const fixLongDivisionWhenReady = (element) => {
|
|
135
|
+
fixLongDivision(element);
|
|
136
|
+
|
|
137
|
+
if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
|
|
138
|
+
document.fonts.ready.then(() => fixLongDivision(element)).catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Keep a live mathfield's long divisions corrected. MathLive rebuilds its shadow
|
|
144
|
+
* DOM on every render (each keystroke, a resize, the fonts loading), which
|
|
145
|
+
* discards the corrections, so they are reapplied whenever it does.
|
|
146
|
+
*
|
|
147
|
+
* @param {HTMLElement} mathField a MathLive `<math-field>`
|
|
148
|
+
* @returns {function|undefined} teardown, to call on unmount
|
|
149
|
+
*/
|
|
150
|
+
export const trackLongDivision = (mathField) => {
|
|
151
|
+
const root = mathField && mathField.shadowRoot;
|
|
152
|
+
|
|
153
|
+
if (!root) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const run = () => fixLongDivision(root);
|
|
158
|
+
|
|
159
|
+
run();
|
|
160
|
+
|
|
161
|
+
if (typeof MutationObserver === 'undefined') {
|
|
162
|
+
return () => {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Only childList: the corrections themselves change attributes, so they do
|
|
166
|
+
// not retrigger this.
|
|
167
|
+
const observer = new MutationObserver(run);
|
|
168
|
+
|
|
169
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
170
|
+
|
|
171
|
+
return () => observer.disconnect();
|
|
172
|
+
};
|
package/src/mathlive-instance.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { NEWLINE_EMBED, toMathLive, withVisibleEmptySlots } from './latex-bridge';
|
|
3
3
|
import { ROOT_INDEX_CSS, PARALLEL_CSS } from './mf/common-styles';
|
|
4
|
+
import { fixLongDivisionWhenReady } from './longdiv';
|
|
4
5
|
|
|
5
6
|
const log = debug('pie-lib:math-input-mathlive:instance');
|
|
6
7
|
|
|
@@ -545,6 +546,9 @@ export function applyStaticMath(element, latex, options = {}) {
|
|
|
545
546
|
macros: { ...getMacros(), ...(options.macros || {}) },
|
|
546
547
|
});
|
|
547
548
|
|
|
549
|
+
// MathLive misdraws long division (see longdiv.js).
|
|
550
|
+
fixLongDivisionWhenReady(element);
|
|
551
|
+
|
|
548
552
|
return element;
|
|
549
553
|
}
|
|
550
554
|
|
package/src/mf/input.jsx
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
trackPlaceholderCaret,
|
|
11
11
|
} from '../mathlive-instance';
|
|
12
12
|
import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
|
|
13
|
+
import { trackWholeAtomSelection } from '../selection';
|
|
14
|
+
import { trackLongDivision } from '../longdiv';
|
|
13
15
|
|
|
14
16
|
const log = debug('pie-lib:math-input-mathlive:input');
|
|
15
17
|
|
|
@@ -93,6 +95,12 @@ export class Input extends React.Component {
|
|
|
93
95
|
// A selected `\placeholder{}` gets no MathLive caret; draw one.
|
|
94
96
|
this.untrackCaret = trackPlaceholderCaret(this.mathField);
|
|
95
97
|
|
|
98
|
+
// Shift+Arrow would otherwise select half a fraction; snap to whole atoms.
|
|
99
|
+
this.untrackSelection = trackWholeAtomSelection(this.mathField);
|
|
100
|
+
|
|
101
|
+
// MathLive misdraws long division; correct it after every render.
|
|
102
|
+
this.untrackLongDivision = trackLongDivision(this.mathField);
|
|
103
|
+
|
|
96
104
|
this.mathField.addEventListener('input', this.onInputEdit);
|
|
97
105
|
|
|
98
106
|
// Belt and braces: drop anything a previous mount may have left behind.
|
|
@@ -136,6 +144,16 @@ export class Input extends React.Component {
|
|
|
136
144
|
this.untrackCaret = undefined;
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
if (this.untrackSelection) {
|
|
148
|
+
this.untrackSelection();
|
|
149
|
+
this.untrackSelection = undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.untrackLongDivision) {
|
|
153
|
+
this.untrackLongDivision();
|
|
154
|
+
this.untrackLongDivision = undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
if (this.mathField) {
|
|
140
158
|
this.mathField.removeEventListener('input', this.onInputEdit);
|
|
141
159
|
this.mathField.remove();
|
package/src/mf/static.jsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
applyShadowStyles,
|
|
11
11
|
trackPlaceholderCaret,
|
|
12
12
|
} from '../mathlive-instance';
|
|
13
|
+
import { trackLongDivision, fixLongDivisionWhenReady } from '../longdiv';
|
|
13
14
|
import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
|
|
14
15
|
import { placeholderStyles, rootIndexStyles, parallelStyles } from './common-styles';
|
|
15
16
|
|
|
@@ -114,6 +115,9 @@ export default class Static extends React.Component {
|
|
|
114
115
|
// blocks are typed into with the same keypad, so they grow placeholders too.
|
|
115
116
|
this.untrackCaret = trackPlaceholderCaret(this.mathField);
|
|
116
117
|
|
|
118
|
+
// MathLive misdraws long division; correct it after every render.
|
|
119
|
+
this.untrackLongDivision = trackLongDivision(this.mathField);
|
|
120
|
+
|
|
117
121
|
this.mathField.addEventListener('input', this.onPromptInput);
|
|
118
122
|
this.mathField.addEventListener('focusin', this.onPromptFocus);
|
|
119
123
|
this.mathField.addEventListener('keydown', this.onKeyDown);
|
|
@@ -124,7 +128,12 @@ export default class Static extends React.Component {
|
|
|
124
128
|
this.createLiveRegion();
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
componentDidUpdate(prevProps) {
|
|
131
|
+
componentDidUpdate(prevProps, prevState) {
|
|
132
|
+
// Display mode: new markup was just written into the DOM.
|
|
133
|
+
if (!this.mathField && this.state.markup !== (prevState && prevState.markup)) {
|
|
134
|
+
fixLongDivisionWhenReady(this.holderRef.current);
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
if (prevProps.latex === this.props.latex) {
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
@@ -145,6 +154,11 @@ export default class Static extends React.Component {
|
|
|
145
154
|
this.untrackCaret = undefined;
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
if (this.untrackLongDivision) {
|
|
158
|
+
this.untrackLongDivision();
|
|
159
|
+
this.untrackLongDivision = undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
148
162
|
if (this.mathField) {
|
|
149
163
|
this.mathField.removeEventListener('input', this.onPromptInput);
|
|
150
164
|
this.mathField.removeEventListener('focusin', this.onPromptFocus);
|