@pie-lib/math-toolbar 3.1.0-beta.8 → 4.0.0-next.41
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/dist/done-button.d.ts +30 -0
- package/dist/done-button.js +35 -0
- package/dist/editor-and-pad.d.ts +47 -0
- package/dist/editor-and-pad.js +352 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +158 -0
- package/dist/math-preview.d.ts +22 -0
- package/dist/math-preview.js +125 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +9 -0
- package/package.json +30 -26
- package/CHANGELOG.json +0 -17
- package/CHANGELOG.md +0 -1049
- package/lib/done-button.js +0 -51
- package/lib/done-button.js.map +0 -1
- package/lib/editor-and-pad.js +0 -536
- package/lib/editor-and-pad.js.map +0 -1
- package/lib/index.js +0 -241
- package/lib/index.js.map +0 -1
- package/lib/math-preview.js +0 -206
- package/lib/math-preview.js.map +0 -1
- package/lib/utils.js +0 -17
- package/lib/utils.js.map +0 -1
- package/src/__tests__/editor-and-pad-mathlive.test.js +0 -178
- package/src/__tests__/editor-and-pad.test.js +0 -74
- package/src/__tests__/index.test.js +0 -21
- package/src/__tests__/math-preview.test.js +0 -83
- package/src/done-button.jsx +0 -36
- package/src/editor-and-pad.jsx +0 -505
- package/src/index.jsx +0 -220
- package/src/math-preview.jsx +0 -174
- package/src/utils.js +0 -11
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { EditorAndPad } from '../editor-and-pad';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Covers the call sites that changed when this package moved from
|
|
5
|
-
* @pie-lib/math-input (MathQuill) to @pie-lib/math-input-mathlive.
|
|
6
|
-
*
|
|
7
|
-
* All three were MathQuill-shaped and silently changed meaning:
|
|
8
|
-
* - shouldComponentUpdate read `this.input.mathField.latex()`, which throws now
|
|
9
|
-
* (and the field loads asynchronously, so `this.input` can be null)
|
|
10
|
-
* - the mixed-number workaround poked `__controller.cursor`, a MathQuill
|
|
11
|
-
* internal with no MathLive equivalent
|
|
12
|
-
* - onClick routes keypad presses through the imperative field API
|
|
13
|
-
*/
|
|
14
|
-
describe('EditorAndPad after the MathLive migration', () => {
|
|
15
|
-
const props = (over = {}) => ({ classes: {}, classNames: {}, onChange: jest.fn(), ...over });
|
|
16
|
-
|
|
17
|
-
describe('shouldComponentUpdate', () => {
|
|
18
|
-
it('treats a missing input ref as different, so the first render is not skipped', () => {
|
|
19
|
-
const c = new EditorAndPad(props({ latex: 'x' }));
|
|
20
|
-
|
|
21
|
-
c.input = null;
|
|
22
|
-
|
|
23
|
-
expect(c.shouldComponentUpdate({ latex: 'x' }, {})).toBe(true);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('does not throw when the math engine has not loaded yet', () => {
|
|
27
|
-
const c = new EditorAndPad(props({ latex: 'x' }));
|
|
28
|
-
|
|
29
|
-
// latex() returns '' until MathLive is ready
|
|
30
|
-
c.input = { latex: () => '' };
|
|
31
|
-
|
|
32
|
-
expect(() => c.shouldComponentUpdate({ latex: 'x' }, {})).not.toThrow();
|
|
33
|
-
expect(c.shouldComponentUpdate({ latex: 'x' }, {})).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('skips the update when the field already holds the incoming latex', () => {
|
|
37
|
-
const c = new EditorAndPad(props({ latex: 'x', keypadMode: 'a', noDecimal: false }));
|
|
38
|
-
|
|
39
|
-
c.input = { latex: () => 'x' };
|
|
40
|
-
c.state = { equationEditor: 'geometry' };
|
|
41
|
-
|
|
42
|
-
expect(c.shouldComponentUpdate({ latex: 'x', keypadMode: 'a', noDecimal: false }, c.state)).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('re-renders when the field differs from the incoming latex', () => {
|
|
46
|
-
const c = new EditorAndPad(props({ latex: 'y' }));
|
|
47
|
-
|
|
48
|
-
c.input = { latex: () => 'x' };
|
|
49
|
-
|
|
50
|
-
expect(c.shouldComponentUpdate({ latex: 'y' }, {})).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('uses input.latex(), never input.mathField.latex()', () => {
|
|
54
|
-
const c = new EditorAndPad(props({ latex: 'x' }));
|
|
55
|
-
const latex = jest.fn(() => 'x');
|
|
56
|
-
|
|
57
|
-
// deliberately no `mathField` - the old code would have thrown here
|
|
58
|
-
c.input = { latex };
|
|
59
|
-
c.state = { equationEditor: 'geometry' };
|
|
60
|
-
c.shouldComponentUpdate({ latex: 'x' }, c.state);
|
|
61
|
-
|
|
62
|
-
expect(latex).toHaveBeenCalled();
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('onClick key routing', () => {
|
|
67
|
-
const withInput = (over = {}) => {
|
|
68
|
-
const c = new EditorAndPad(props(over));
|
|
69
|
-
const calls = { write: [], command: [], keystroke: [], clear: 0 };
|
|
70
|
-
|
|
71
|
-
c.input = {
|
|
72
|
-
write: (v) => calls.write.push(v),
|
|
73
|
-
command: (v) => calls.command.push(v),
|
|
74
|
-
keystroke: (v) => calls.keystroke.push(v),
|
|
75
|
-
clear: () => calls.clear++,
|
|
76
|
-
};
|
|
77
|
-
c.updateDisable = () => {};
|
|
78
|
-
|
|
79
|
-
return { c, calls };
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// toNodeData keys this off `value === 'clear'`, not `type`
|
|
83
|
-
it('clear', () => {
|
|
84
|
-
const { c, calls } = withInput();
|
|
85
|
-
|
|
86
|
-
c.onClick({ value: 'clear' });
|
|
87
|
-
expect(calls.clear).toBe(1);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('command', () => {
|
|
91
|
-
const { c, calls } = withInput();
|
|
92
|
-
|
|
93
|
-
c.onClick({ type: 'command', value: '\\frac' });
|
|
94
|
-
expect(calls.command).toEqual(['\\frac']);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('cursor keystroke', () => {
|
|
98
|
-
const { c, calls } = withInput();
|
|
99
|
-
|
|
100
|
-
c.onClick({ type: 'cursor', value: 'Left' });
|
|
101
|
-
expect(calls.keystroke).toEqual(['Left']);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('answer block writes the response token', () => {
|
|
105
|
-
const { c, calls } = withInput();
|
|
106
|
-
|
|
107
|
-
c.onClick({ type: 'answer' });
|
|
108
|
-
expect(calls.write).toEqual(['%response%']);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('plain value', () => {
|
|
112
|
-
const { c, calls } = withInput();
|
|
113
|
-
|
|
114
|
-
c.onClick({ value: 'x' });
|
|
115
|
-
expect(calls.write).toEqual(['x']);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('ignores a nullish key', () => {
|
|
119
|
-
const { c, calls } = withInput();
|
|
120
|
-
|
|
121
|
-
expect(() => c.onClick(null)).not.toThrow();
|
|
122
|
-
expect(calls.write).toEqual([]);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('onEditorChange', () => {
|
|
127
|
-
const withInput = (over = {}) => {
|
|
128
|
-
const onChange = jest.fn();
|
|
129
|
-
const c = new EditorAndPad(props({ onChange, ...over }));
|
|
130
|
-
const calls = { keystroke: [], clear: 0, write: [] };
|
|
131
|
-
|
|
132
|
-
c.input = {
|
|
133
|
-
keystroke: (v) => calls.keystroke.push(v),
|
|
134
|
-
clear: () => calls.clear++,
|
|
135
|
-
write: (v) => calls.write.push(v),
|
|
136
|
-
};
|
|
137
|
-
c.updateDisable = () => {};
|
|
138
|
-
|
|
139
|
-
return { c, onChange, calls };
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
it('propagates ordinary latex to onChange', () => {
|
|
143
|
-
const { c, onChange } = withInput();
|
|
144
|
-
|
|
145
|
-
c.onEditorChange('\\frac{1}{2}');
|
|
146
|
-
expect(onChange).toHaveBeenCalledWith('\\frac{1}{2}');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('strips decimals when noDecimal is set', () => {
|
|
150
|
-
const { c, onChange, calls } = withInput({ noDecimal: true });
|
|
151
|
-
|
|
152
|
-
c.onEditorChange('1.5');
|
|
153
|
-
|
|
154
|
-
expect(calls.clear).toBe(1);
|
|
155
|
-
expect(calls.write).toEqual(['15']);
|
|
156
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('undoes the empty fraction after a digit with a public delete, not MathQuill internals', () => {
|
|
160
|
-
const { c, onChange, calls } = withInput();
|
|
161
|
-
|
|
162
|
-
// the shape the old __controller hack targeted
|
|
163
|
-
c.onEditorChange('2\\ \\frac{1}{ }');
|
|
164
|
-
|
|
165
|
-
expect(calls.keystroke).toEqual(['Backspace']);
|
|
166
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('does not treat normal fractions as the mixed-number case', () => {
|
|
170
|
-
const { c, onChange, calls } = withInput();
|
|
171
|
-
|
|
172
|
-
c.onEditorChange('\\frac{1}{2}');
|
|
173
|
-
|
|
174
|
-
expect(calls.keystroke).toEqual([]);
|
|
175
|
-
expect(onChange).toHaveBeenCalled();
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { EditorAndPad } from '../editor-and-pad';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
describe('EditorAndPad', () => {
|
|
6
|
-
const defaultProps = {
|
|
7
|
-
classes: {},
|
|
8
|
-
classNames: {},
|
|
9
|
-
onBlur: jest.fn(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
jest.useFakeTimers();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
jest.useRealTimers();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('renders with default props', () => {
|
|
21
|
-
const { container } = render(<EditorAndPad {...defaultProps} />);
|
|
22
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('autoFocus', () => {
|
|
26
|
-
it('focuses input immediately via setTimeout when autoFocus is true', () => {
|
|
27
|
-
const mockFocus = jest.fn();
|
|
28
|
-
const component = new EditorAndPad({ ...defaultProps, autoFocus: true });
|
|
29
|
-
component.input = { focus: mockFocus };
|
|
30
|
-
|
|
31
|
-
component.componentDidMount();
|
|
32
|
-
|
|
33
|
-
expect(mockFocus).not.toHaveBeenCalled();
|
|
34
|
-
|
|
35
|
-
jest.runAllTimers();
|
|
36
|
-
|
|
37
|
-
expect(mockFocus).toHaveBeenCalledTimes(1);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('does not focus input when autoFocus is false', () => {
|
|
41
|
-
const mockFocus = jest.fn();
|
|
42
|
-
const component = new EditorAndPad({ ...defaultProps, autoFocus: false });
|
|
43
|
-
component.input = { focus: mockFocus };
|
|
44
|
-
|
|
45
|
-
component.componentDidMount();
|
|
46
|
-
jest.runAllTimers();
|
|
47
|
-
|
|
48
|
-
expect(mockFocus).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('does not focus when input ref is not set', () => {
|
|
52
|
-
const component = new EditorAndPad({ ...defaultProps, autoFocus: true });
|
|
53
|
-
component.input = null;
|
|
54
|
-
|
|
55
|
-
expect(() => {
|
|
56
|
-
component.componentDidMount();
|
|
57
|
-
jest.runAllTimers();
|
|
58
|
-
}).not.toThrow();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('uses setTimeout with 0ms delay to defer focus', () => {
|
|
62
|
-
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
63
|
-
const mockFocus = jest.fn();
|
|
64
|
-
const component = new EditorAndPad({ ...defaultProps, autoFocus: true });
|
|
65
|
-
component.input = { focus: mockFocus };
|
|
66
|
-
|
|
67
|
-
component.componentDidMount();
|
|
68
|
-
|
|
69
|
-
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 0);
|
|
70
|
-
|
|
71
|
-
setTimeoutSpy.mockRestore();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { RawPureToolbar } from '../index';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
describe('RawPureToolbar', () => {
|
|
6
|
-
const defaultProps = {
|
|
7
|
-
classes: {},
|
|
8
|
-
controlledKeypad: true,
|
|
9
|
-
showKeypad: true,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
it('renders with DONE button if hideDoneButton is not defined', () => {
|
|
13
|
-
const { container } = render(<RawPureToolbar {...defaultProps} />);
|
|
14
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('renders without DONE button if hideDoneButton value is true', () => {
|
|
18
|
-
const { container } = render(<RawPureToolbar {...defaultProps} hideDoneButton={true} />);
|
|
19
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import MathPreview from '../math-preview';
|
|
4
|
-
|
|
5
|
-
/** Every rule emotion has injected so far, as CSSStyleRule objects. */
|
|
6
|
-
const injectedRules = () =>
|
|
7
|
-
Array.from(document.querySelectorAll('style[data-emotion]'))
|
|
8
|
-
.flatMap((tag) => (tag.sheet ? Array.from(tag.sheet.cssRules) : []))
|
|
9
|
-
.filter((rule) => rule.selectorText);
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The `.ML__latex` rule that actually applies to `element`.
|
|
13
|
-
*
|
|
14
|
-
* Matching with `element.matches` rather than string comparison means the
|
|
15
|
-
* assertions cover the selector too: a rule written as `& > .ML__latex` would
|
|
16
|
-
* not match the nested markup MathLive emits, and these tests would fail.
|
|
17
|
-
*/
|
|
18
|
-
const mathLiveRuleFor = (element) =>
|
|
19
|
-
injectedRules().find((rule) => rule.selectorText.includes('.ML__latex') && element.matches(rule.selectorText));
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Stand in for MathLive's `convertLatexToMarkup` output, which is
|
|
23
|
-
* `<span class="ML__latex">...</span>` nested inside `mf.Static`'s holder span.
|
|
24
|
-
* MathLive does not load under jsdom, so the markup is added by hand.
|
|
25
|
-
*/
|
|
26
|
-
const addStaticMarkup = (container, depth = 1) => {
|
|
27
|
-
let parent = container.firstChild;
|
|
28
|
-
|
|
29
|
-
for (let i = 1; i < depth; i += 1) {
|
|
30
|
-
const wrapper = document.createElement('span');
|
|
31
|
-
|
|
32
|
-
parent.appendChild(wrapper);
|
|
33
|
-
parent = wrapper;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const markup = document.createElement('span');
|
|
37
|
-
|
|
38
|
-
markup.className = 'ML__latex';
|
|
39
|
-
parent.appendChild(markup);
|
|
40
|
-
|
|
41
|
-
return markup;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
describe('MathPreview', () => {
|
|
45
|
-
const defaultProps = {
|
|
46
|
-
latex: 'sqrt(5)',
|
|
47
|
-
classes: {},
|
|
48
|
-
isSelected: false,
|
|
49
|
-
onFocus: jest.fn(),
|
|
50
|
-
onBlur: jest.fn(),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
it('renders with default props', () => {
|
|
54
|
-
const { container } = render(<MathPreview {...defaultProps} />);
|
|
55
|
-
expect(container.firstChild).toBeInTheDocument();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('MathLive static markup', () => {
|
|
59
|
-
it('is outlined and padded when not selected', () => {
|
|
60
|
-
const { container } = render(<MathPreview {...defaultProps} isSelected={false} />);
|
|
61
|
-
const rule = mathLiveRuleFor(addStaticMarkup(container));
|
|
62
|
-
|
|
63
|
-
expect(rule).toBeDefined();
|
|
64
|
-
expect(rule.style.getPropertyValue('border')).toEqual('solid 1px lightgrey');
|
|
65
|
-
expect(rule.style.getPropertyValue('padding')).toEqual('5px');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('drops its outline and padding when selected, so only the container border shows', () => {
|
|
69
|
-
const { container } = render(<MathPreview {...defaultProps} isSelected />);
|
|
70
|
-
const rule = mathLiveRuleFor(addStaticMarkup(container));
|
|
71
|
-
|
|
72
|
-
expect(rule).toBeDefined();
|
|
73
|
-
expect(rule.style.getPropertyValue('border')).toEqual('solid 0px lightgrey');
|
|
74
|
-
expect(rule.style.getPropertyValue('padding')).toEqual('0px');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('is styled at any depth below the container, not just as a direct child', () => {
|
|
78
|
-
const { container } = render(<MathPreview {...defaultProps} isSelected={false} />);
|
|
79
|
-
|
|
80
|
-
expect(mathLiveRuleFor(addStaticMarkup(container, 3))).toBeDefined();
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
});
|
package/src/done-button.jsx
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import IconButton from '@mui/material/IconButton';
|
|
4
|
-
import Check from '@mui/icons-material/Check';
|
|
5
|
-
import { styled } from '@mui/material/styles';
|
|
6
|
-
import PropTypes from 'prop-types';
|
|
7
|
-
|
|
8
|
-
const StyledIconButton = styled(IconButton)(({ theme, hideBackground }) => ({
|
|
9
|
-
verticalAlign: 'top',
|
|
10
|
-
width: '28px',
|
|
11
|
-
height: '28px',
|
|
12
|
-
color: '#00bb00',
|
|
13
|
-
...(hideBackground && {
|
|
14
|
-
backgroundColor: theme.palette.common.white,
|
|
15
|
-
'&:hover': {
|
|
16
|
-
backgroundColor: theme.palette.grey[200],
|
|
17
|
-
},
|
|
18
|
-
}),
|
|
19
|
-
'& .MuiIconButton-label': {
|
|
20
|
-
position: 'absolute',
|
|
21
|
-
top: '2px',
|
|
22
|
-
},
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
export const RawDoneButton = ({ onClick, hideBackground }) => (
|
|
26
|
-
<StyledIconButton aria-label="Done" onClick={onClick} hideBackground={hideBackground} size="large">
|
|
27
|
-
<Check />
|
|
28
|
-
</StyledIconButton>
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
RawDoneButton.propTypes = {
|
|
32
|
-
onClick: PropTypes.func,
|
|
33
|
-
hideBackground: PropTypes.bool,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const DoneButton = RawDoneButton;
|