@pie-lib/math-input-mathlive 1.0.9-next.6618 → 1.0.9
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/README.md +15 -6
- package/lib/__tests__/composites.test.js +255 -0
- package/lib/__tests__/composites.test.js.map +1 -0
- package/lib/__tests__/drop-in-compat.test.js +133 -0
- package/lib/__tests__/drop-in-compat.test.js.map +1 -0
- package/lib/__tests__/index.test.js +118 -0
- package/lib/__tests__/index.test.js.map +1 -0
- package/lib/__tests__/input-imperative.test.js +155 -0
- package/lib/__tests__/input-imperative.test.js.map +1 -0
- package/lib/__tests__/keypad-label.test.js +44 -0
- package/lib/__tests__/keypad-label.test.js.map +1 -0
- package/lib/__tests__/keypad-render.test.js +206 -0
- package/lib/__tests__/keypad-render.test.js.map +1 -0
- package/lib/__tests__/latex-bridge.test.js +251 -0
- package/lib/__tests__/latex-bridge.test.js.map +1 -0
- package/lib/__tests__/mathlive-instance.test.js +237 -0
- package/lib/__tests__/mathlive-instance.test.js.map +1 -0
- package/lib/__tests__/mount-race.test.js +194 -0
- package/lib/__tests__/mount-race.test.js.map +1 -0
- package/lib/__tests__/static-prompts.test.js +282 -0
- package/lib/__tests__/static-prompts.test.js.map +1 -0
- package/lib/mathlive-instance.js +47 -10
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/input.js +18 -5
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +17 -4
- package/lib/mf/static.js.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/composites.test.jsx +220 -0
- package/src/__tests__/keypad-label.test.jsx +5 -13
- package/src/__tests__/keypad-render.test.jsx +123 -0
- package/src/__tests__/mathlive-instance.test.js +40 -0
- package/src/__tests__/mount-race.test.jsx +129 -0
- package/src/__tests__/static-prompts.test.jsx +251 -0
- package/src/mathlive-instance.js +50 -11
- package/src/mf/input.jsx +19 -5
- package/src/mf/static.jsx +20 -4
- package/LICENSE.md +0 -5
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import Static from '../mf/static';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The answer-block contract: `\MathQuillMathField[id]{}` in stored latex becomes
|
|
5
|
+
* a MathLive prompt, and sub-field values/focus are surfaced through the same
|
|
6
|
+
* callbacks @pie-lib/math-input exposed (`onSubFieldChange`, `onSubFieldFocus`,
|
|
7
|
+
* `getFieldName`, `setInput`).
|
|
8
|
+
*
|
|
9
|
+
* Driven against a stub mathfield: a real <math-field> needs a browser, and what
|
|
10
|
+
* matters here is the mapping, not MathLive's rendering.
|
|
11
|
+
*/
|
|
12
|
+
const stubField = (values = {}, selection = null) => ({
|
|
13
|
+
values: { ...values },
|
|
14
|
+
getPrompts() {
|
|
15
|
+
return Object.keys(this.values);
|
|
16
|
+
},
|
|
17
|
+
getPromptValue(id) {
|
|
18
|
+
return this.values[id];
|
|
19
|
+
},
|
|
20
|
+
setPromptValue(id, v) {
|
|
21
|
+
this.values[id] = v;
|
|
22
|
+
},
|
|
23
|
+
getPromptRange(id) {
|
|
24
|
+
const i = Object.keys(this.values).indexOf(id);
|
|
25
|
+
|
|
26
|
+
return [i * 10, i * 10 + 5];
|
|
27
|
+
},
|
|
28
|
+
selection,
|
|
29
|
+
addEventListener() {},
|
|
30
|
+
removeEventListener() {},
|
|
31
|
+
focus() {
|
|
32
|
+
this.focused = true;
|
|
33
|
+
},
|
|
34
|
+
blur() {
|
|
35
|
+
this.blurred = true;
|
|
36
|
+
},
|
|
37
|
+
remove() {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const make = (props = {}) => new Static({ latex: 'x=\\MathQuillMathField[r1]{}', getFieldName: () => {}, ...props });
|
|
41
|
+
|
|
42
|
+
describe('Static: answer blocks / prompts', () => {
|
|
43
|
+
describe('isInteractive', () => {
|
|
44
|
+
it('is interactive when the latex has answer blocks', () => {
|
|
45
|
+
expect(make({ latex: 'x=\\MathQuillMathField[r1]{}' }).isInteractive()).toBe(true);
|
|
46
|
+
expect(make({ latex: 'x=\\placeholder[r1]{}' }).isInteractive()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('is display-only for plain latex', () => {
|
|
50
|
+
expect(make({ latex: '\\frac{1}{2}' }).isInteractive()).toBe(false);
|
|
51
|
+
expect(make({ latex: '' }).isInteractive()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('values', () => {
|
|
56
|
+
it('returns {} before the field exists', () => {
|
|
57
|
+
expect(make().values()).toEqual({});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('maps each prompt id to its stored-form latex', () => {
|
|
61
|
+
const s = make();
|
|
62
|
+
|
|
63
|
+
s.mathField = stubField({ r1: '7', r2: '\\placeholder{}' });
|
|
64
|
+
|
|
65
|
+
// \placeholder must not leak into stored values
|
|
66
|
+
expect(s.values()).toEqual({ r1: '7', r2: '' });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('promptHandle / innerFields', () => {
|
|
71
|
+
it('exposes a MathQuill-field-like handle with get/set latex', () => {
|
|
72
|
+
const s = make();
|
|
73
|
+
|
|
74
|
+
s.mathField = stubField({ r1: '7' });
|
|
75
|
+
|
|
76
|
+
const h = s.promptHandle('r1');
|
|
77
|
+
|
|
78
|
+
expect(h.id).toEqual('r1');
|
|
79
|
+
expect(h.latex()).toEqual('7');
|
|
80
|
+
|
|
81
|
+
h.latex('9');
|
|
82
|
+
expect(s.mathField.values.r1).toEqual('9');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('handle latex() is safe with no field', () => {
|
|
86
|
+
expect(make().promptHandle('r1').latex()).toEqual('');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('innerFields returns one handle per prompt', () => {
|
|
90
|
+
const s = make();
|
|
91
|
+
|
|
92
|
+
s.mathField = stubField({ r1: '1', r2: '2' });
|
|
93
|
+
|
|
94
|
+
expect(s.innerFields.map((f) => f.id)).toEqual(['r1', 'r2']);
|
|
95
|
+
expect(make().innerFields).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('onSubFieldChange', () => {
|
|
100
|
+
it('fires only for prompts whose value changed', () => {
|
|
101
|
+
const onSubFieldChange = jest.fn();
|
|
102
|
+
const s = make({ onSubFieldChange });
|
|
103
|
+
|
|
104
|
+
s.mathField = stubField({ r1: '1', r2: '2' });
|
|
105
|
+
s.promptValues = { r1: '1', r2: '2' };
|
|
106
|
+
|
|
107
|
+
s.mathField.values.r2 = '5';
|
|
108
|
+
s.onPromptInput();
|
|
109
|
+
|
|
110
|
+
expect(onSubFieldChange).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(onSubFieldChange).toHaveBeenCalledWith('r2', '5');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('uses getFieldName when it returns a name', () => {
|
|
115
|
+
const onSubFieldChange = jest.fn();
|
|
116
|
+
const s = make({ onSubFieldChange, getFieldName: (handle) => 'named-' + handle.id });
|
|
117
|
+
|
|
118
|
+
s.mathField = stubField({ r1: '1' });
|
|
119
|
+
s.promptValues = {};
|
|
120
|
+
|
|
121
|
+
s.onPromptInput();
|
|
122
|
+
|
|
123
|
+
expect(onSubFieldChange).toHaveBeenCalledWith('named-r1', '1');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('still tracks values when no callback is supplied', () => {
|
|
127
|
+
const s = make();
|
|
128
|
+
|
|
129
|
+
s.mathField = stubField({ r1: '3' });
|
|
130
|
+
s.onPromptInput();
|
|
131
|
+
|
|
132
|
+
expect(s.promptValues).toEqual({ r1: '3' });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('onSubFieldFocus', () => {
|
|
137
|
+
it('reports the prompt containing the selection, plus setInput', () => {
|
|
138
|
+
const onSubFieldFocus = jest.fn();
|
|
139
|
+
const setInput = jest.fn();
|
|
140
|
+
const s = make({ onSubFieldFocus, setInput });
|
|
141
|
+
|
|
142
|
+
// r2 occupies offsets 10..15
|
|
143
|
+
s.mathField = stubField({ r1: '', r2: '' }, { ranges: [[11, 11]] });
|
|
144
|
+
s.onPromptFocus();
|
|
145
|
+
|
|
146
|
+
expect(onSubFieldFocus).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(onSubFieldFocus.mock.calls[0][0]).toEqual('r2');
|
|
148
|
+
expect(onSubFieldFocus.mock.calls[0][1].id).toEqual('r2');
|
|
149
|
+
expect(setInput).toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does nothing without a selection', () => {
|
|
153
|
+
const onSubFieldFocus = jest.fn();
|
|
154
|
+
const s = make({ onSubFieldFocus });
|
|
155
|
+
|
|
156
|
+
s.mathField = stubField({ r1: '' }, null);
|
|
157
|
+
s.onPromptFocus();
|
|
158
|
+
|
|
159
|
+
expect(onSubFieldFocus).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('does not throw when the field is missing', () => {
|
|
163
|
+
const s = make({ onSubFieldFocus: jest.fn() });
|
|
164
|
+
|
|
165
|
+
expect(() => s.onPromptFocus()).not.toThrow();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('accessibility announcements', () => {
|
|
170
|
+
afterEach(() => {
|
|
171
|
+
document.body.innerHTML = '';
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('creates a polite off-screen live region', () => {
|
|
175
|
+
const s = make();
|
|
176
|
+
|
|
177
|
+
s.createLiveRegion();
|
|
178
|
+
|
|
179
|
+
expect(s.liveRegion.getAttribute('aria-live')).toEqual('polite');
|
|
180
|
+
expect(s.liveRegion.getAttribute('aria-atomic')).toEqual('true');
|
|
181
|
+
expect(document.body.contains(s.liveRegion)).toBe(true);
|
|
182
|
+
|
|
183
|
+
s.removeLiveRegion();
|
|
184
|
+
expect(s.liveRegion).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('does not create a second region', () => {
|
|
188
|
+
const s = make();
|
|
189
|
+
|
|
190
|
+
s.createLiveRegion();
|
|
191
|
+
const first = s.liveRegion;
|
|
192
|
+
|
|
193
|
+
s.createLiveRegion();
|
|
194
|
+
expect(s.liveRegion).toBe(first);
|
|
195
|
+
s.removeLiveRegion();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('announces when typed input expands into a command', () => {
|
|
199
|
+
const s = make();
|
|
200
|
+
|
|
201
|
+
s.createLiveRegion();
|
|
202
|
+
s.promptValues = { r1: 'x' };
|
|
203
|
+
// grew by more than one char AND gained a backslash => a conversion
|
|
204
|
+
s.announceConversion({ r1: 'x\\frac{}{}' });
|
|
205
|
+
|
|
206
|
+
expect(s.liveRegion.textContent).toEqual('Converted to math symbol');
|
|
207
|
+
s.removeLiveRegion();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('does not announce ordinary typing', () => {
|
|
211
|
+
const s = make();
|
|
212
|
+
|
|
213
|
+
s.createLiveRegion();
|
|
214
|
+
s.promptValues = { r1: 'x' };
|
|
215
|
+
s.announceConversion({ r1: 'xy' });
|
|
216
|
+
|
|
217
|
+
expect(s.liveRegion.textContent).toEqual('');
|
|
218
|
+
s.removeLiveRegion();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('does not announce a deletion', () => {
|
|
222
|
+
const s = make();
|
|
223
|
+
|
|
224
|
+
s.createLiveRegion();
|
|
225
|
+
s.promptValues = { r1: 'x\\frac{}{}' };
|
|
226
|
+
s.onKeyDown({ key: 'Backspace' });
|
|
227
|
+
s.announceConversion({ r1: 'x' });
|
|
228
|
+
|
|
229
|
+
expect(s.liveRegion.textContent).toEqual('');
|
|
230
|
+
s.removeLiveRegion();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('focus / blur', () => {
|
|
235
|
+
it('delegates to the field and is safe without one', () => {
|
|
236
|
+
const s = make();
|
|
237
|
+
|
|
238
|
+
expect(() => {
|
|
239
|
+
s.focus();
|
|
240
|
+
s.blur();
|
|
241
|
+
}).not.toThrow();
|
|
242
|
+
|
|
243
|
+
s.mathField = stubField();
|
|
244
|
+
s.focus();
|
|
245
|
+
s.blur();
|
|
246
|
+
|
|
247
|
+
expect(s.mathField.focused).toBe(true);
|
|
248
|
+
expect(s.mathField.blurred).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
package/src/mathlive-instance.js
CHANGED
|
@@ -46,26 +46,63 @@ const absoluteFontsDirectory = (dir) => {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Version-pinned CDN fallback for the KaTeX fonts.
|
|
51
|
+
*
|
|
52
|
+
* Some symbols are composed from Private Use Area glyphs that exist only in
|
|
53
|
+
* those fonts: `\\neq` is U+E020 (a negation slash) overlapped on `=`, and
|
|
54
|
+
* `\\nsim` / `\\ncong` work the same way. With no fonts the browser substitutes
|
|
55
|
+
* an arbitrary glyph for U+E020, so those keys render as garbage rather than
|
|
56
|
+
* simply falling back to a plainer face.
|
|
57
|
+
*
|
|
58
|
+
* The version comes from MathLive itself, so the fonts always match the engine.
|
|
59
|
+
* Hosts that would rather not depend on a third party should serve the fonts and
|
|
60
|
+
* call `configureFonts('/their/path')`, or pass `null` to opt out.
|
|
61
|
+
*/
|
|
62
|
+
const FALLBACK_MATHLIVE_VERSION = '0.110.0';
|
|
63
|
+
|
|
64
|
+
const cdnFontsDirectory = () => {
|
|
65
|
+
const version = (mathlive && mathlive.version && mathlive.version.mathlive) || FALLBACK_MATHLIVE_VERSION;
|
|
66
|
+
|
|
67
|
+
// NOTE: `/fonts`, not `/dist/fonts` - the published package root holds them.
|
|
68
|
+
return `https://unpkg.com/mathlive@${version}/fonts`;
|
|
69
|
+
};
|
|
70
|
+
|
|
49
71
|
/**
|
|
50
72
|
* Tell the package where the MathLive fonts are served from.
|
|
51
73
|
*
|
|
52
|
-
* Call before `loadMathLive()`.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
74
|
+
* Call before `loadMathLive()`.
|
|
75
|
+
*
|
|
76
|
+
* - a path/URL: served by you, no third-party request
|
|
77
|
+
* - `null`: skip font loading; note `\\neq`, `\\nsim` and `\\ncong` then
|
|
78
|
+
* render incorrectly, being built from Private Use Area glyphs
|
|
79
|
+
* - not called: falls back to a version-pinned CDN
|
|
55
80
|
*
|
|
56
81
|
* The fonts ship inside the `mathlive` package (`node_modules/mathlive/fonts`)
|
|
57
|
-
*
|
|
82
|
+
* ready to copy into whatever the host app serves statically.
|
|
58
83
|
*
|
|
59
|
-
* @param {string|null} dir
|
|
84
|
+
* @param {string|null} [dir]
|
|
60
85
|
*/
|
|
61
86
|
export const configureFonts = (dir) => {
|
|
62
87
|
fontsDirectory = dir;
|
|
63
88
|
|
|
64
89
|
if (mathlive) {
|
|
65
|
-
mathlive.MathfieldElement.fontsDirectory = absoluteFontsDirectory(dir);
|
|
90
|
+
mathlive.MathfieldElement.fontsDirectory = dir === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(dir);
|
|
66
91
|
}
|
|
67
92
|
};
|
|
68
93
|
|
|
94
|
+
/**
|
|
95
|
+
* What the spacebar inserts in math mode.
|
|
96
|
+
*
|
|
97
|
+
* MathLive's `mathModeSpace` defaults to an empty string, which makes the
|
|
98
|
+
* spacebar a no-op - users cannot type a space at all. `\\ ` (a LaTeX control
|
|
99
|
+
* space) is what MathQuill serialised spaces as, so existing content and the
|
|
100
|
+
* MathJax renderer in @pie-lib/math-rendering both already understand it.
|
|
101
|
+
*
|
|
102
|
+
* A literal " " is not usable: LaTeX skips raw whitespace.
|
|
103
|
+
*/
|
|
104
|
+
export const MATH_MODE_SPACE = '\\ ';
|
|
105
|
+
|
|
69
106
|
/**
|
|
70
107
|
* Custom macros for the pie-specific commands that the MathQuill fork
|
|
71
108
|
* understood but stock LaTeX does not.
|
|
@@ -126,17 +163,19 @@ let loadWarned = false;
|
|
|
126
163
|
/** Apply post-load configuration exactly once. */
|
|
127
164
|
const configureLoaded = () => {
|
|
128
165
|
// Always replace MathLive's relative `'./fonts/'` default: it throws
|
|
129
|
-
// "Invalid base URL" when bundled.
|
|
130
|
-
//
|
|
166
|
+
// "Invalid base URL" when bundled. Use the host's copy if one was configured,
|
|
167
|
+
// otherwise the version-pinned CDN.
|
|
131
168
|
if (fontsDirectory === undefined) {
|
|
132
169
|
log(
|
|
133
|
-
'no fontsDirectory configured -
|
|
134
|
-
'
|
|
170
|
+
'no fontsDirectory configured - falling back to the CDN (%s). ' +
|
|
171
|
+
'Serve the fonts yourself and call configureFonts("/path/to/fonts") to avoid the network request, ' +
|
|
172
|
+
'or configureFonts(null) to opt out.',
|
|
173
|
+
cdnFontsDirectory(),
|
|
135
174
|
);
|
|
136
175
|
}
|
|
137
176
|
|
|
138
177
|
mathlive.MathfieldElement.fontsDirectory =
|
|
139
|
-
fontsDirectory === undefined ?
|
|
178
|
+
fontsDirectory === undefined ? cdnFontsDirectory() : absoluteFontsDirectory(fontsDirectory);
|
|
140
179
|
|
|
141
180
|
injectCoreStylesheet();
|
|
142
181
|
};
|
package/src/mf/input.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { styled } from '@mui/material/styles';
|
|
4
4
|
import debug from 'debug';
|
|
5
|
-
import { loadMathLive, getMacros } from '../mathlive-instance';
|
|
5
|
+
import { loadMathLive, getMacros, MATH_MODE_SPACE } from '../mathlive-instance';
|
|
6
6
|
import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
|
|
7
7
|
|
|
8
8
|
const log = debug('pie-lib:math-input-mathlive:input');
|
|
@@ -49,9 +49,16 @@ export class Input extends React.Component {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async componentDidMount() {
|
|
52
|
+
// MathLive loads asynchronously, so this method resumes after an await and
|
|
53
|
+
// has to know whether it is still the current mount. React StrictMode calls
|
|
54
|
+
// mount -> unmount -> mount on the SAME instance in dev, so a boolean
|
|
55
|
+
// "unmounted" flag would latch and block the remount from ever creating a
|
|
56
|
+
// field. A generation counter identifies the live mount instead: only the
|
|
57
|
+
// newest one proceeds, and a real unmount invalidates all pending ones.
|
|
58
|
+
const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);
|
|
52
59
|
const ml = await loadMathLive();
|
|
53
60
|
|
|
54
|
-
if (!ml || !this.holderRef.current) {
|
|
61
|
+
if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {
|
|
55
62
|
return;
|
|
56
63
|
}
|
|
57
64
|
|
|
@@ -64,15 +71,18 @@ export class Input extends React.Component {
|
|
|
64
71
|
|
|
65
72
|
// pie drives input through its own keypad, so suppress MathLive's.
|
|
66
73
|
this.mathField.mathVirtualKeyboardPolicy = 'manual';
|
|
74
|
+
// Without this the spacebar does nothing: MathLive's mathModeSpace
|
|
75
|
+
// defaults to an empty string.
|
|
76
|
+
this.mathField.mathModeSpace = MATH_MODE_SPACE;
|
|
67
77
|
|
|
68
78
|
this.mathField.addEventListener('input', this.onInputEdit);
|
|
69
|
-
|
|
79
|
+
|
|
80
|
+
// Belt and braces: drop anything a previous mount may have left behind.
|
|
81
|
+
this.holderRef.current.replaceChildren(this.mathField);
|
|
70
82
|
|
|
71
83
|
if (this.props.innerRef) {
|
|
72
84
|
this.props.innerRef(this);
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
this.setState?.({});
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
componentDidUpdate(prevProps) {
|
|
@@ -99,6 +109,10 @@ export class Input extends React.Component {
|
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
componentWillUnmount() {
|
|
112
|
+
// Invalidate any mount still awaiting the MathLive load, so it does not
|
|
113
|
+
// append a field into a holder that is going away.
|
|
114
|
+
this.mountGeneration = (this.mountGeneration || 0) + 1;
|
|
115
|
+
|
|
102
116
|
if (this.mathField) {
|
|
103
117
|
this.mathField.removeEventListener('input', this.onInputEdit);
|
|
104
118
|
this.mathField.remove();
|
package/src/mf/static.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { styled } from '@mui/material/styles';
|
|
4
4
|
import debug from 'debug';
|
|
5
|
-
import { loadMathLive, getMacros, latexToMarkup } from '../mathlive-instance';
|
|
5
|
+
import { loadMathLive, getMacros, latexToMarkup, MATH_MODE_SPACE } from '../mathlive-instance';
|
|
6
6
|
import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
|
|
7
7
|
import { placeholderStyles } from './common-styles';
|
|
8
8
|
|
|
@@ -64,28 +64,41 @@ export default class Static extends React.Component {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async componentDidMount() {
|
|
67
|
+
// See the note in mf/input.jsx: a generation counter, not a boolean, so a
|
|
68
|
+
// StrictMode mount -> unmount -> mount on the same instance still ends up
|
|
69
|
+
// with exactly one field.
|
|
70
|
+
const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);
|
|
71
|
+
|
|
67
72
|
if (!this.isInteractive()) {
|
|
68
73
|
await loadMathLive();
|
|
69
|
-
|
|
74
|
+
|
|
75
|
+
if (generation === this.mountGeneration) {
|
|
76
|
+
this.renderMarkup();
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
return;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
const ml = await loadMathLive();
|
|
74
83
|
|
|
75
|
-
if (!ml || !this.holderRef.current) {
|
|
84
|
+
if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {
|
|
76
85
|
return;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
this.mathField = new ml.MathfieldElement({ macros: getMacros() });
|
|
80
89
|
this.mathField.readOnly = true;
|
|
81
90
|
this.mathField.mathVirtualKeyboardPolicy = 'manual';
|
|
91
|
+
// Without this the spacebar does nothing: MathLive's mathModeSpace
|
|
92
|
+
// defaults to an empty string.
|
|
93
|
+
this.mathField.mathModeSpace = MATH_MODE_SPACE;
|
|
82
94
|
this.mathField.value = toMathLive(this.props.latex);
|
|
83
95
|
|
|
84
96
|
this.mathField.addEventListener('input', this.onPromptInput);
|
|
85
97
|
this.mathField.addEventListener('focusin', this.onPromptFocus);
|
|
86
98
|
this.mathField.addEventListener('keydown', this.onKeyDown);
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
// Belt and braces: drop anything a previous mount may have left behind.
|
|
101
|
+
this.holderRef.current.replaceChildren(this.mathField);
|
|
89
102
|
|
|
90
103
|
this.createLiveRegion();
|
|
91
104
|
}
|
|
@@ -103,6 +116,9 @@ export default class Static extends React.Component {
|
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
componentWillUnmount() {
|
|
119
|
+
// Invalidate any mount still awaiting the MathLive load.
|
|
120
|
+
this.mountGeneration = (this.mountGeneration || 0) + 1;
|
|
121
|
+
|
|
106
122
|
if (this.mathField) {
|
|
107
123
|
this.mathField.removeEventListener('input', this.onPromptInput);
|
|
108
124
|
this.mathField.removeEventListener('focusin', this.onPromptFocus);
|
package/LICENSE.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
Copyright 2019 CoreSpring Inc
|
|
2
|
-
|
|
3
|
-
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
4
|
-
|
|
5
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|