@pie-lib/math-input-mathlive 1.0.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.
- package/README.md +119 -0
- package/lib/__tests__/index.test.js +118 -0
- package/lib/__tests__/index.test.js.map +1 -0
- package/lib/__tests__/latex-bridge.test.js +186 -0
- package/lib/__tests__/latex-bridge.test.js.map +1 -0
- package/lib/__tests__/mathlive-instance.test.js +137 -0
- package/lib/__tests__/mathlive-instance.test.js.map +1 -0
- package/lib/horizontal-keypad.js +101 -0
- package/lib/horizontal-keypad.js.map +1 -0
- package/lib/index.js +143 -0
- package/lib/index.js.map +1 -0
- package/lib/keypad/index.js +365 -0
- package/lib/keypad/index.js.map +1 -0
- package/lib/keypad/keys-layout.js +22 -0
- package/lib/keypad/keys-layout.js.map +1 -0
- package/lib/keys/basic-operators.js +33 -0
- package/lib/keys/basic-operators.js.map +1 -0
- package/lib/keys/chars.js +12 -0
- package/lib/keys/chars.js.map +1 -0
- package/lib/keys/comparison.js +39 -0
- package/lib/keys/comparison.js.map +1 -0
- package/lib/keys/constants.js +37 -0
- package/lib/keys/constants.js.map +1 -0
- package/lib/keys/digits.js +46 -0
- package/lib/keys/digits.js.map +1 -0
- package/lib/keys/edit.js +14 -0
- package/lib/keys/edit.js.map +1 -0
- package/lib/keys/exponent.js +30 -0
- package/lib/keys/exponent.js.map +1 -0
- package/lib/keys/fractions.js +29 -0
- package/lib/keys/fractions.js.map +1 -0
- package/lib/keys/geometry.js +140 -0
- package/lib/keys/geometry.js.map +1 -0
- package/lib/keys/grades.js +259 -0
- package/lib/keys/grades.js.map +1 -0
- package/lib/keys/index.js +35 -0
- package/lib/keys/index.js.map +1 -0
- package/lib/keys/log.js +27 -0
- package/lib/keys/log.js.map +1 -0
- package/lib/keys/logic.js +19 -0
- package/lib/keys/logic.js.map +1 -0
- package/lib/keys/matrices.js +19 -0
- package/lib/keys/matrices.js.map +1 -0
- package/lib/keys/misc.js +62 -0
- package/lib/keys/misc.js.map +1 -0
- package/lib/keys/navigation.js +20 -0
- package/lib/keys/navigation.js.map +1 -0
- package/lib/keys/operators.js +15 -0
- package/lib/keys/operators.js.map +1 -0
- package/lib/keys/statistics.js +40 -0
- package/lib/keys/statistics.js.map +1 -0
- package/lib/keys/sub-sup.js +19 -0
- package/lib/keys/sub-sup.js.map +1 -0
- package/lib/keys/trigonometry.js +45 -0
- package/lib/keys/trigonometry.js.map +1 -0
- package/lib/keys/utils.js +87 -0
- package/lib/keys/utils.js.map +1 -0
- package/lib/keys/vars.js +24 -0
- package/lib/keys/vars.js.map +1 -0
- package/lib/latex-bridge.js +195 -0
- package/lib/latex-bridge.js.map +1 -0
- package/lib/math-input.js +143 -0
- package/lib/math-input.js.map +1 -0
- package/lib/mathlive-instance.js +319 -0
- package/lib/mathlive-instance.js.map +1 -0
- package/lib/mf/common-styles.js +111 -0
- package/lib/mf/common-styles.js.map +1 -0
- package/lib/mf/index.js +72 -0
- package/lib/mf/index.js.map +1 -0
- package/lib/mf/input.js +266 -0
- package/lib/mf/input.js.map +1 -0
- package/lib/mf/static.js +396 -0
- package/lib/mf/static.js.map +1 -0
- package/package.json +31 -0
- package/src/__tests__/index.test.jsx +109 -0
- package/src/__tests__/latex-bridge.test.js +151 -0
- package/src/__tests__/mathlive-instance.test.js +130 -0
- package/src/horizontal-keypad.jsx +73 -0
- package/src/index.jsx +63 -0
- package/src/keypad/index.jsx +335 -0
- package/src/keypad/keys-layout.js +16 -0
- package/src/keys/basic-operators.js +32 -0
- package/src/keys/chars.js +5 -0
- package/src/keys/comparison.js +28 -0
- package/src/keys/constants.js +35 -0
- package/src/keys/digits.js +40 -0
- package/src/keys/edit.js +3 -0
- package/src/keys/exponent.js +28 -0
- package/src/keys/fractions.js +26 -0
- package/src/keys/geometry.js +144 -0
- package/src/keys/grades.js +367 -0
- package/src/keys/index.js +20 -0
- package/src/keys/log.js +22 -0
- package/src/keys/logic.js +15 -0
- package/src/keys/matrices.js +15 -0
- package/src/keys/misc.js +65 -0
- package/src/keys/navigation.js +8 -0
- package/src/keys/operators.js +10 -0
- package/src/keys/statistics.js +38 -0
- package/src/keys/sub-sup.js +15 -0
- package/src/keys/trigonometry.js +15 -0
- package/src/keys/utils.js +66 -0
- package/src/keys/vars.js +19 -0
- package/src/latex-bridge.js +190 -0
- package/src/math-input.jsx +115 -0
- package/src/mathlive-instance.js +286 -0
- package/src/mf/common-styles.js +108 -0
- package/src/mf/index.js +26 -0
- package/src/mf/input.jsx +223 -0
- package/src/mf/static.jsx +358 -0
package/src/mf/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Input from './input';
|
|
2
|
+
import Static from './static';
|
|
3
|
+
import CommonMqStyles, {
|
|
4
|
+
commonMathLiveStyles,
|
|
5
|
+
commonKeyboardStyles,
|
|
6
|
+
commonMqFontStyles,
|
|
7
|
+
commonMqKeyboardStyles,
|
|
8
|
+
longdivStyles,
|
|
9
|
+
supsubStyles,
|
|
10
|
+
placeholderStyles,
|
|
11
|
+
} from './common-styles';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
Input,
|
|
15
|
+
Static,
|
|
16
|
+
// Same object shape as @pie-lib/math-input's `mq.CommonMqStyles`, so
|
|
17
|
+
// `const { commonMqFontStyles, ... } = mq.CommonMqStyles` keeps working.
|
|
18
|
+
CommonMqStyles,
|
|
19
|
+
commonMathLiveStyles,
|
|
20
|
+
commonKeyboardStyles,
|
|
21
|
+
commonMqFontStyles,
|
|
22
|
+
commonMqKeyboardStyles,
|
|
23
|
+
longdivStyles,
|
|
24
|
+
supsubStyles,
|
|
25
|
+
placeholderStyles,
|
|
26
|
+
};
|
package/src/mf/input.jsx
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { styled } from '@mui/material/styles';
|
|
4
|
+
import debug from 'debug';
|
|
5
|
+
import { loadMathLive, getMacros } from '../mathlive-instance';
|
|
6
|
+
import { toMathLive, fromMathLive } from '../latex-bridge';
|
|
7
|
+
|
|
8
|
+
const log = debug('pie-lib:math-input-mathlive:input');
|
|
9
|
+
|
|
10
|
+
const Holder = styled('span')({
|
|
11
|
+
display: 'inline-block',
|
|
12
|
+
'& math-field': {
|
|
13
|
+
display: 'block',
|
|
14
|
+
width: '100%',
|
|
15
|
+
},
|
|
16
|
+
// MathLive ships its own virtual keyboard toggle; pie supplies its own keypad.
|
|
17
|
+
'& math-field::part(virtual-keyboard-toggle)': {
|
|
18
|
+
display: 'none',
|
|
19
|
+
},
|
|
20
|
+
'& math-field::part(menu-toggle)': {
|
|
21
|
+
display: 'none',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Editable math field. Drop-in replacement for @pie-lib/math-input's `mq.Input`.
|
|
27
|
+
*
|
|
28
|
+
* Keeps the same imperative surface used by MathInput's keypad
|
|
29
|
+
* (`write`, `command`, `keystroke`, `clear`, `focus`, `blur`) so callers do not
|
|
30
|
+
* change, and the same `latex` / `onChange` props.
|
|
31
|
+
*/
|
|
32
|
+
export class Input extends React.Component {
|
|
33
|
+
static propTypes = {
|
|
34
|
+
className: PropTypes.string,
|
|
35
|
+
latex: PropTypes.string,
|
|
36
|
+
onChange: PropTypes.func,
|
|
37
|
+
onClick: PropTypes.func,
|
|
38
|
+
onFocus: PropTypes.func,
|
|
39
|
+
onBlur: PropTypes.func,
|
|
40
|
+
readOnly: PropTypes.bool,
|
|
41
|
+
innerRef: PropTypes.func,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
constructor(props) {
|
|
45
|
+
super(props);
|
|
46
|
+
this.holderRef = React.createRef();
|
|
47
|
+
// Tracks the last latex we emitted, so prop echoes don't reset the cursor.
|
|
48
|
+
this.lastEmitted = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async componentDidMount() {
|
|
52
|
+
const ml = await loadMathLive();
|
|
53
|
+
|
|
54
|
+
if (!ml || !this.holderRef.current) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.mathField = new ml.MathfieldElement({ macros: getMacros() });
|
|
59
|
+
this.mathField.value = toMathLive(this.props.latex || '');
|
|
60
|
+
|
|
61
|
+
if (this.props.readOnly) {
|
|
62
|
+
this.mathField.readOnly = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// pie drives input through its own keypad, so suppress MathLive's.
|
|
66
|
+
this.mathField.mathVirtualKeyboardPolicy = 'manual';
|
|
67
|
+
|
|
68
|
+
this.mathField.addEventListener('input', this.onInputEdit);
|
|
69
|
+
this.holderRef.current.appendChild(this.mathField);
|
|
70
|
+
|
|
71
|
+
if (this.props.innerRef) {
|
|
72
|
+
this.props.innerRef(this);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.setState?.({});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
componentDidUpdate(prevProps) {
|
|
79
|
+
if (!this.mathField) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { latex, readOnly } = this.props;
|
|
84
|
+
|
|
85
|
+
if (readOnly !== prevProps.readOnly) {
|
|
86
|
+
this.mathField.readOnly = !!readOnly;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Only push the prop back into the field when it genuinely differs from
|
|
90
|
+
// what the field already holds. This is the analogue of the old
|
|
91
|
+
// shouldComponentUpdate latex-equality guard and prevents cursor thrash.
|
|
92
|
+
if (latex !== undefined && latex !== null && latex !== this.lastEmitted) {
|
|
93
|
+
const next = toMathLive(latex);
|
|
94
|
+
|
|
95
|
+
if (next !== this.mathField.value) {
|
|
96
|
+
this.mathField.value = next;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
componentWillUnmount() {
|
|
102
|
+
if (this.mathField) {
|
|
103
|
+
this.mathField.removeEventListener('input', this.onInputEdit);
|
|
104
|
+
this.mathField.remove();
|
|
105
|
+
this.mathField = undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.props.innerRef) {
|
|
109
|
+
this.props.innerRef(null);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onInputEdit = () => {
|
|
114
|
+
const { onChange } = this.props;
|
|
115
|
+
|
|
116
|
+
if (!this.mathField) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const latex = fromMathLive(this.mathField.getValue('latex'));
|
|
121
|
+
|
|
122
|
+
this.lastEmitted = latex;
|
|
123
|
+
|
|
124
|
+
if (onChange) {
|
|
125
|
+
onChange(latex);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Current latex, in stored (MathQuill-compatible) form. */
|
|
130
|
+
latex() {
|
|
131
|
+
return this.mathField ? fromMathLive(this.mathField.getValue('latex')) : '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clear() {
|
|
135
|
+
if (this.mathField) {
|
|
136
|
+
this.mathField.value = '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
blur() {
|
|
143
|
+
log('blur mathfield');
|
|
144
|
+
this.mathField && this.mathField.blur();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
focus() {
|
|
148
|
+
log('focus mathfield...');
|
|
149
|
+
this.mathField && this.mathField.focus();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** MathQuill `cmd()` equivalent. */
|
|
153
|
+
command(v) {
|
|
154
|
+
log('command: ', v);
|
|
155
|
+
|
|
156
|
+
if (!this.mathField) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const values = Array.isArray(v) ? v : [v];
|
|
161
|
+
|
|
162
|
+
values.forEach((vv) => this.mathField.insert(vv, { focus: true }));
|
|
163
|
+
this.mathField.focus();
|
|
164
|
+
|
|
165
|
+
return this.latex();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** MathQuill `keystroke()` equivalent - takes a MathLive selector. */
|
|
169
|
+
keystroke(selector) {
|
|
170
|
+
if (!this.mathField) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.mathField.executeCommand(selector);
|
|
175
|
+
this.mathField.focus();
|
|
176
|
+
|
|
177
|
+
return this.latex();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** MathQuill `write()` equivalent. Accepts an array, as `cmd()` did. */
|
|
181
|
+
write(v) {
|
|
182
|
+
log('write: ', v);
|
|
183
|
+
|
|
184
|
+
if (!this.mathField) {
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const values = Array.isArray(v) ? v : [v];
|
|
189
|
+
|
|
190
|
+
values.forEach((vv) => this.mathField.insert(vv, { focus: true }));
|
|
191
|
+
this.mathField.focus();
|
|
192
|
+
|
|
193
|
+
return this.latex();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Enter must not insert a line break: math input is single-expression, and
|
|
198
|
+
* the MathQuill version swallowed charCode 13 for the same reason. Newlines
|
|
199
|
+
* are authored explicitly through the newLine embed, not the Enter key.
|
|
200
|
+
*/
|
|
201
|
+
onKeyDown = (event) => {
|
|
202
|
+
if (event.key === 'Enter') {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
render() {
|
|
208
|
+
const { className, onFocus, onBlur, onClick } = this.props;
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Holder
|
|
212
|
+
className={className}
|
|
213
|
+
onFocus={onFocus}
|
|
214
|
+
onBlur={onBlur}
|
|
215
|
+
onClick={onClick}
|
|
216
|
+
onKeyDown={this.onKeyDown}
|
|
217
|
+
ref={this.holderRef}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default Input;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { styled } from '@mui/material/styles';
|
|
4
|
+
import debug from 'debug';
|
|
5
|
+
import { loadMathLive, getMacros, latexToMarkup } from '../mathlive-instance';
|
|
6
|
+
import { toMathLive, fromMathLive, fieldIds } from '../latex-bridge';
|
|
7
|
+
|
|
8
|
+
const log = debug('pie-lib:math-input-mathlive:static');
|
|
9
|
+
|
|
10
|
+
const Holder = styled('span')({
|
|
11
|
+
display: 'inline-block',
|
|
12
|
+
'& math-field': {
|
|
13
|
+
display: 'inline-block',
|
|
14
|
+
border: 'none',
|
|
15
|
+
background: 'transparent',
|
|
16
|
+
},
|
|
17
|
+
'& math-field::part(virtual-keyboard-toggle)': {
|
|
18
|
+
display: 'none',
|
|
19
|
+
},
|
|
20
|
+
'& math-field::part(menu-toggle)': {
|
|
21
|
+
display: 'none',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Static math. Drop-in replacement for @pie-lib/math-input's `mq.Static`.
|
|
27
|
+
*
|
|
28
|
+
* Two modes, chosen automatically:
|
|
29
|
+
*
|
|
30
|
+
* - **display** (no answer blocks in the latex): rendered with
|
|
31
|
+
* `convertLatexToMarkup`. No mathfield instance, so it is cheap enough for
|
|
32
|
+
* keypad labels - the old implementation created a full MathQuill
|
|
33
|
+
* StaticMath per button.
|
|
34
|
+
*
|
|
35
|
+
* - **interactive** (latex contains `\MathQuillMathField[..]{}` /
|
|
36
|
+
* `\placeholder[..]{}`): a read-only `<math-field>` whose placeholders stay
|
|
37
|
+
* editable. Sub-field values are surfaced through the prompts API, replacing
|
|
38
|
+
* MathQuill's `innerFields`.
|
|
39
|
+
*/
|
|
40
|
+
export default class Static extends React.Component {
|
|
41
|
+
static propTypes = {
|
|
42
|
+
latex: PropTypes.string.isRequired,
|
|
43
|
+
className: PropTypes.string,
|
|
44
|
+
onFocus: PropTypes.func,
|
|
45
|
+
onBlur: PropTypes.func,
|
|
46
|
+
getFieldName: PropTypes.func,
|
|
47
|
+
onSubFieldChange: PropTypes.func,
|
|
48
|
+
onSubFieldFocus: PropTypes.func,
|
|
49
|
+
setInput: PropTypes.func,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
static defaultProps = {
|
|
53
|
+
getFieldName: () => {},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
constructor(props) {
|
|
57
|
+
super(props);
|
|
58
|
+
this.holderRef = React.createRef();
|
|
59
|
+
this.state = { markup: '' };
|
|
60
|
+
this.promptValues = {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async componentDidMount() {
|
|
64
|
+
if (!this.isInteractive()) {
|
|
65
|
+
await loadMathLive();
|
|
66
|
+
this.renderMarkup();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ml = await loadMathLive();
|
|
71
|
+
|
|
72
|
+
if (!ml || !this.holderRef.current) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.mathField = new ml.MathfieldElement({ macros: getMacros() });
|
|
77
|
+
this.mathField.readOnly = true;
|
|
78
|
+
this.mathField.mathVirtualKeyboardPolicy = 'manual';
|
|
79
|
+
this.mathField.value = toMathLive(this.props.latex);
|
|
80
|
+
|
|
81
|
+
this.mathField.addEventListener('input', this.onPromptInput);
|
|
82
|
+
this.mathField.addEventListener('focusin', this.onPromptFocus);
|
|
83
|
+
this.mathField.addEventListener('keydown', this.onKeyDown);
|
|
84
|
+
|
|
85
|
+
this.holderRef.current.appendChild(this.mathField);
|
|
86
|
+
|
|
87
|
+
this.createLiveRegion();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
componentDidUpdate(prevProps) {
|
|
91
|
+
if (prevProps.latex === this.props.latex) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this.mathField) {
|
|
96
|
+
this.mathField.value = toMathLive(this.props.latex);
|
|
97
|
+
} else {
|
|
98
|
+
this.renderMarkup();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
componentWillUnmount() {
|
|
103
|
+
if (this.mathField) {
|
|
104
|
+
this.mathField.removeEventListener('input', this.onPromptInput);
|
|
105
|
+
this.mathField.removeEventListener('focusin', this.onPromptFocus);
|
|
106
|
+
this.mathField.removeEventListener('keydown', this.onKeyDown);
|
|
107
|
+
this.mathField.remove();
|
|
108
|
+
this.mathField = undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.removeLiveRegion();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Off-screen aria-live region, ported from @pie-lib/math-input's Static.
|
|
116
|
+
*
|
|
117
|
+
* MathLive has its own accessibility layer, but it does not announce that
|
|
118
|
+
* typed input was converted into a math symbol, which screen-reader users
|
|
119
|
+
* relied on in the MathQuill implementation.
|
|
120
|
+
*/
|
|
121
|
+
createLiveRegion = () => {
|
|
122
|
+
if (typeof document === 'undefined' || this.liveRegion) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.liveRegion = document.createElement('div');
|
|
127
|
+
Object.assign(this.liveRegion.style, {
|
|
128
|
+
position: 'absolute',
|
|
129
|
+
width: '1px',
|
|
130
|
+
height: '1px',
|
|
131
|
+
marginTop: '-1px',
|
|
132
|
+
clip: 'rect(1px, 1px, 1px, 1px)',
|
|
133
|
+
overflow: 'hidden',
|
|
134
|
+
});
|
|
135
|
+
this.liveRegion.setAttribute('aria-live', 'polite');
|
|
136
|
+
this.liveRegion.setAttribute('aria-atomic', 'true');
|
|
137
|
+
|
|
138
|
+
document.body.appendChild(this.liveRegion);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
removeLiveRegion = () => {
|
|
142
|
+
if (this.liveRegion && this.liveRegion.parentNode) {
|
|
143
|
+
this.liveRegion.parentNode.removeChild(this.liveRegion);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.liveRegion = undefined;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
announce = (message) => {
|
|
150
|
+
if (!this.liveRegion) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.liveRegion.textContent = message;
|
|
155
|
+
|
|
156
|
+
// Clear once announced, so the same message can be announced again.
|
|
157
|
+
clearTimeout(this.announceTimeout);
|
|
158
|
+
this.announceTimeout = setTimeout(() => {
|
|
159
|
+
if (this.liveRegion) {
|
|
160
|
+
this.liveRegion.textContent = '';
|
|
161
|
+
}
|
|
162
|
+
}, 500);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
onKeyDown = (event) => {
|
|
166
|
+
// Deletions should not be announced as conversions.
|
|
167
|
+
this.isDeleting = event?.key === 'Backspace' || event?.key === 'Delete';
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Announce when typed input became a math symbol. The MathQuill version
|
|
172
|
+
* inferred this by counting braces; here a prompt whose latex grew by more
|
|
173
|
+
* than the single character typed means a command was expanded.
|
|
174
|
+
*/
|
|
175
|
+
announceConversion = (next) => {
|
|
176
|
+
if (this.isDeleting) {
|
|
177
|
+
this.isDeleting = false;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const converted = Object.keys(next).some((id) => {
|
|
182
|
+
const before = this.promptValues[id] || '';
|
|
183
|
+
const after = next[id] || '';
|
|
184
|
+
|
|
185
|
+
return after.length > before.length + 1 && /\\/.test(after.slice(before.length));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (converted) {
|
|
189
|
+
this.announce('Converted to math symbol');
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Does this latex contain answer blocks? */
|
|
194
|
+
isInteractive() {
|
|
195
|
+
const latex = this.props.latex || '';
|
|
196
|
+
|
|
197
|
+
return fieldIds(latex).length > 0 || latex.indexOf('\\placeholder') !== -1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
renderMarkup() {
|
|
201
|
+
const markup = latexToMarkup(this.props.latex);
|
|
202
|
+
|
|
203
|
+
if (markup !== this.state.markup) {
|
|
204
|
+
this.setState({ markup });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** MathQuill's `innerFields` equivalent, keyed by prompt id. */
|
|
209
|
+
values() {
|
|
210
|
+
if (!this.mathField) {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const out = {};
|
|
215
|
+
|
|
216
|
+
(this.mathField.getPrompts() || []).forEach((id) => {
|
|
217
|
+
out[id] = fromMathLive(this.mathField.getPromptValue(id));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* A MathQuill-inner-field-like handle for a prompt.
|
|
225
|
+
*
|
|
226
|
+
* The old API handed callers a MathQuill field object, and consumers commonly
|
|
227
|
+
* called `field.latex()` on it. There is no equivalent object in MathLive -
|
|
228
|
+
* prompts are addressed by id - so this adapter exposes the parts that were
|
|
229
|
+
* actually used: `id` and a get/set `latex()`.
|
|
230
|
+
*/
|
|
231
|
+
promptHandle(id) {
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
latex: (value) => {
|
|
235
|
+
if (!this.mathField) {
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (value === undefined) {
|
|
240
|
+
return fromMathLive(this.mathField.getPromptValue(id));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.mathField.setPromptValue(id, toMathLive(value), {});
|
|
244
|
+
|
|
245
|
+
return value;
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** All prompt handles, the analogue of `mathField.innerFields`. */
|
|
251
|
+
get innerFields() {
|
|
252
|
+
if (!this.mathField) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (this.mathField.getPrompts() || []).map((id) => this.promptHandle(id));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
onPromptInput = () => {
|
|
260
|
+
const { onSubFieldChange, getFieldName } = this.props;
|
|
261
|
+
|
|
262
|
+
if (!this.mathField) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const next = this.values();
|
|
267
|
+
|
|
268
|
+
this.announceConversion(next);
|
|
269
|
+
|
|
270
|
+
if (!onSubFieldChange) {
|
|
271
|
+
this.promptValues = next;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const fields = this.innerFields;
|
|
276
|
+
|
|
277
|
+
// Emit only the prompts whose value actually changed, mirroring the
|
|
278
|
+
// per-field granularity of the old onSubFieldChange(name, latex).
|
|
279
|
+
Object.keys(next).forEach((id) => {
|
|
280
|
+
if (next[id] === this.promptValues[id]) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const name = getFieldName(this.promptHandle(id), fields) || id;
|
|
285
|
+
|
|
286
|
+
onSubFieldChange(name, next[id]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
this.promptValues = next;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
onPromptFocus = () => {
|
|
293
|
+
const { onSubFieldFocus, getFieldName } = this.props;
|
|
294
|
+
|
|
295
|
+
if (!this.mathField || !onSubFieldFocus) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const ids = this.mathField.getPrompts() || [];
|
|
301
|
+
// The focused prompt is the one containing the current selection.
|
|
302
|
+
const focused = ids.find((id) => {
|
|
303
|
+
const range = this.mathField.getPromptRange(id);
|
|
304
|
+
const selection = this.mathField.selection;
|
|
305
|
+
|
|
306
|
+
if (!range || !selection || !selection.ranges || !selection.ranges.length) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const [start, end] = range;
|
|
311
|
+
const [selStart] = selection.ranges[0];
|
|
312
|
+
|
|
313
|
+
return selStart >= start && selStart <= end;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (focused) {
|
|
317
|
+
const handle = this.promptHandle(focused);
|
|
318
|
+
const name = getFieldName(handle, this.innerFields) || focused;
|
|
319
|
+
|
|
320
|
+
if (this.props.setInput) {
|
|
321
|
+
this.props.setInput(handle);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onSubFieldFocus(name, handle);
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
log('error resolving focused prompt: %s', e.message);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
focus() {
|
|
332
|
+
this.mathField && this.mathField.focus();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
blur() {
|
|
336
|
+
this.mathField && this.mathField.blur();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
render() {
|
|
340
|
+
const { className, onFocus, onBlur } = this.props;
|
|
341
|
+
const { markup } = this.state;
|
|
342
|
+
|
|
343
|
+
if (this.mathField || this.isInteractive()) {
|
|
344
|
+
return <Holder className={className} onFocus={onFocus} onBlur={onBlur} ref={this.holderRef} />;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<Holder
|
|
349
|
+
className={className}
|
|
350
|
+
onFocus={onFocus}
|
|
351
|
+
onBlur={onBlur}
|
|
352
|
+
ref={this.holderRef}
|
|
353
|
+
// eslint-disable-next-line react/no-danger
|
|
354
|
+
dangerouslySetInnerHTML={{ __html: markup }}
|
|
355
|
+
/>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|