@pie-lib/math-input-mathlive 1.1.5-beta.11 → 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 +8 -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.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/longdiv-wiring.test.jsx +137 -0
- package/src/__tests__/longdiv.test.js +259 -0
- package/src/__tests__/selection.test.js +39 -4
- 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 +9 -0
- package/src/mf/static.jsx +15 -1
- package/src/selection.js +1 -6
package/lib/selection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection.js","names":["_debug","_interopRequireDefault","require","log","debug","expandRange","exports","start","end","info","lastOffset","at","i","depth","isBranchStart","latex","_ref","target","Infinity","Math","min","max","first","test","trackWholeAtomSelection","mathField","addEventListener","undefined","applying","update","selectionIsCollapsed","getElementInfo","selection","range","ranges","_range","_slicedToArray2","lo","hi","_expandRange","_expandRange2","nextStart","nextEnd","direction","e","message","removeEventListener"],"sources":["../src/selection.js"],"sourcesContent":["import debug from 'debug';\n\nconst log = debug('pie-lib:math-input-mathlive:selection');\n\n/**\n * Widen a selection so it covers whole atoms of a single parent.\n *\n * MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into\n * a fraction instead of over it: from before `2x` in `5x^2+2x+\\frac{\\pi}{2}-4`,\n * five presses select `2x+` plus the `\\pi` of the numerator, and copying gives\n * `2x+\\pi`. MathQuill treated a fraction (root, superscript...) as one unit,\n * which is what authors expect.\n *\n * The tree is recovered from MathLive's public `getElementInfo(offset)`, where\n * offset `i` describes the atom ending at caret position `i`:\n *\n * - a composite atom (fraction, root, script) owns the offset right *after*\n * its children; the children are one level deeper;\n * - each branch (numerator, denominator, radicand) opens with an empty-latex\n * \"start\" atom.\n *\n * The rule: find the shallowest depth the selection touches, counting the caret\n * it starts from. If the range crosses into a sibling branch at that depth, the\n * real common parent is one level up. Then push both ends out of anything\n * deeper. A script atom (`^2`) is kept with its base, since MathLive stores\n * `x^2` as `x` followed by a separate `^2` atom.\n *\n * Pure, so it can be tested without a browser.\n *\n * @param {number} start\n * @param {number} end\n * @param {function(number): {depth?: number, latex?: string}} info\n * @param {number} lastOffset\n * @returns {[number, number]} the widened range, start <= end\n */\nexport const expandRange = (start, end, info, lastOffset) => {\n const at = (i) => info(i) || {};\n const depth = (i) => at(i).depth || 0;\n const isBranchStart = (i) => depth(i) > 0 && at(i).latex === '';\n\n if (end < start) {\n [start, end] = [end, start];\n }\n\n if (end - start < 1) {\n return [start, end];\n }\n\n let target = Infinity;\n\n for (let i = start; i <= end; i++) {\n target = Math.min(target, depth(i));\n }\n\n for (let i = start + 1; i <= end; i++) {\n if (depth(i) === target && isBranchStart(i)) {\n target -= 1;\n break;\n }\n }\n\n target = Math.max(target, 0);\n\n while (start > 0 && depth(start) > target) {\n start -= 1;\n }\n\n while (end < lastOffset && depth(end) > target) {\n end += 1;\n }\n\n // Keep a superscript / subscript with its base.\n let first = start + 1;\n\n while (first <= end && depth(first) > target) {\n first += 1;\n }\n\n if (start > 0 && first <= end && /^[\\^_]/.test(at(first).latex || '') && depth(start) === target) {\n start -= 1;\n }\n\n return [start, end];\n};\n\n/**\n * Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).\n *\n * This fixes both what the user sees highlighted and what copy/cut put on the\n * clipboard, since both come from the selection. The selection direction is\n * preserved, so further Shift+Arrow presses keep extending the same end.\n *\n * @param {HTMLElement} mathField a MathLive `<math-field>`\n * @returns {function|undefined} teardown, to call on unmount\n */\nexport const trackWholeAtomSelection = (mathField) => {\n if (!mathField || typeof mathField.addEventListener !== 'function') {\n return undefined;\n }\n\n let applying = false;\n\n const update = () => {\n if (applying) {\n return;\n }\n\n try {\n if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {\n return;\n }\n\n const selection = mathField.selection;\n const range = selection && selection.ranges && selection.ranges[0];\n\n if (!range) {\n return;\n }\n\n const [start, end] = range;\n const lo = Math.min(start, end);\n const hi = Math.max(start, end);\n const [nextStart, nextEnd] = expandRange(
|
|
1
|
+
{"version":3,"file":"selection.js","names":["_debug","_interopRequireDefault","require","log","debug","expandRange","exports","start","end","info","lastOffset","at","i","depth","isBranchStart","latex","_ref","target","Infinity","Math","min","max","first","test","trackWholeAtomSelection","mathField","addEventListener","undefined","applying","update","selectionIsCollapsed","getElementInfo","selection","range","ranges","_range","_slicedToArray2","lo","hi","_expandRange","_expandRange2","nextStart","nextEnd","direction","e","message","removeEventListener"],"sources":["../src/selection.js"],"sourcesContent":["import debug from 'debug';\n\nconst log = debug('pie-lib:math-input-mathlive:selection');\n\n/**\n * Widen a selection so it covers whole atoms of a single parent.\n *\n * MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into\n * a fraction instead of over it: from before `2x` in `5x^2+2x+\\frac{\\pi}{2}-4`,\n * five presses select `2x+` plus the `\\pi` of the numerator, and copying gives\n * `2x+\\pi`. MathQuill treated a fraction (root, superscript...) as one unit,\n * which is what authors expect.\n *\n * The tree is recovered from MathLive's public `getElementInfo(offset)`, where\n * offset `i` describes the atom ending at caret position `i`:\n *\n * - a composite atom (fraction, root, script) owns the offset right *after*\n * its children; the children are one level deeper;\n * - each branch (numerator, denominator, radicand) opens with an empty-latex\n * \"start\" atom.\n *\n * The rule: find the shallowest depth the selection touches, counting the caret\n * it starts from. If the range crosses into a sibling branch at that depth, the\n * real common parent is one level up. Then push both ends out of anything\n * deeper. A script atom (`^2`) is kept with its base, since MathLive stores\n * `x^2` as `x` followed by a separate `^2` atom.\n *\n * Pure, so it can be tested without a browser.\n *\n * @param {number} start\n * @param {number} end\n * @param {function(number): {depth?: number, latex?: string}} info\n * @param {number} lastOffset\n * @returns {[number, number]} the widened range, start <= end\n */\nexport const expandRange = (start, end, info, lastOffset) => {\n const at = (i) => info(i) || {};\n const depth = (i) => at(i).depth || 0;\n const isBranchStart = (i) => depth(i) > 0 && at(i).latex === '';\n\n if (end < start) {\n [start, end] = [end, start];\n }\n\n if (end - start < 1) {\n return [start, end];\n }\n\n let target = Infinity;\n\n for (let i = start; i <= end; i++) {\n target = Math.min(target, depth(i));\n }\n\n for (let i = start + 1; i <= end; i++) {\n if (depth(i) === target && isBranchStart(i)) {\n target -= 1;\n break;\n }\n }\n\n target = Math.max(target, 0);\n\n while (start > 0 && depth(start) > target) {\n start -= 1;\n }\n\n while (end < lastOffset && depth(end) > target) {\n end += 1;\n }\n\n // Keep a superscript / subscript with its base.\n let first = start + 1;\n\n while (first <= end && depth(first) > target) {\n first += 1;\n }\n\n if (start > 0 && first <= end && /^[\\^_]/.test(at(first).latex || '') && depth(start) === target) {\n start -= 1;\n }\n\n return [start, end];\n};\n\n/**\n * Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).\n *\n * This fixes both what the user sees highlighted and what copy/cut put on the\n * clipboard, since both come from the selection. The selection direction is\n * preserved, so further Shift+Arrow presses keep extending the same end.\n *\n * @param {HTMLElement} mathField a MathLive `<math-field>`\n * @returns {function|undefined} teardown, to call on unmount\n */\nexport const trackWholeAtomSelection = (mathField) => {\n if (!mathField || typeof mathField.addEventListener !== 'function') {\n return undefined;\n }\n\n let applying = false;\n\n const update = () => {\n if (applying) {\n return;\n }\n\n try {\n if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {\n return;\n }\n\n const selection = mathField.selection;\n const range = selection && selection.ranges && selection.ranges[0];\n\n if (!range) {\n return;\n }\n\n const [start, end] = range;\n const lo = Math.min(start, end);\n const hi = Math.max(start, end);\n const [nextStart, nextEnd] = expandRange(lo, hi, (i) => mathField.getElementInfo(i), mathField.lastOffset);\n\n if (nextStart === lo && nextEnd === hi) {\n return;\n }\n\n applying = true;\n mathField.selection = { ranges: [[nextStart, nextEnd]], direction: selection.direction };\n } catch (e) {\n log('could not normalise the selection: %s', e && e.message);\n } finally {\n applying = false;\n }\n };\n\n mathField.addEventListener('selection-change', update);\n\n return () => mathField.removeEventListener('selection-change', update);\n};\n"],"mappings":";;;;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AAEA,IAAMC,GAAG,GAAG,IAAAC,iBAAK,EAAC,uCAAuC,CAAC;;AAE1D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,IAAMC,WAAW,GAAAC,OAAA,CAAAD,WAAA,GAAG,SAAdA,WAAWA,CAAIE,KAAK,EAAEC,GAAG,EAAEC,IAAI,EAAEC,UAAU,EAAK;EAC3D,IAAMC,EAAE,GAAG,SAALA,EAAEA,CAAIC,CAAC;IAAA,OAAKH,IAAI,CAACG,CAAC,CAAC,IAAI,CAAC,CAAC;EAAA;EAC/B,IAAMC,KAAK,GAAG,SAARA,KAAKA,CAAID,CAAC;IAAA,OAAKD,EAAE,CAACC,CAAC,CAAC,CAACC,KAAK,IAAI,CAAC;EAAA;EACrC,IAAMC,aAAa,GAAG,SAAhBA,aAAaA,CAAIF,CAAC;IAAA,OAAKC,KAAK,CAACD,CAAC,CAAC,GAAG,CAAC,IAAID,EAAE,CAACC,CAAC,CAAC,CAACG,KAAK,KAAK,EAAE;EAAA;EAE/D,IAAIP,GAAG,GAAGD,KAAK,EAAE;IAAA,IAAAS,IAAA,GACA,CAACR,GAAG,EAAED,KAAK,CAAC;IAA1BA,KAAK,GAAAS,IAAA;IAAER,GAAG,GAAAQ,IAAA;EACb;EAEA,IAAIR,GAAG,GAAGD,KAAK,GAAG,CAAC,EAAE;IACnB,OAAO,CAACA,KAAK,EAAEC,GAAG,CAAC;EACrB;EAEA,IAAIS,MAAM,GAAGC,QAAQ;EAErB,KAAK,IAAIN,CAAC,GAAGL,KAAK,EAAEK,CAAC,IAAIJ,GAAG,EAAEI,CAAC,EAAE,EAAE;IACjCK,MAAM,GAAGE,IAAI,CAACC,GAAG,CAACH,MAAM,EAAEJ,KAAK,CAACD,CAAC,CAAC,CAAC;EACrC;EAEA,KAAK,IAAIA,EAAC,GAAGL,KAAK,GAAG,CAAC,EAAEK,EAAC,IAAIJ,GAAG,EAAEI,EAAC,EAAE,EAAE;IACrC,IAAIC,KAAK,CAACD,EAAC,CAAC,KAAKK,MAAM,IAAIH,aAAa,CAACF,EAAC,CAAC,EAAE;MAC3CK,MAAM,IAAI,CAAC;MACX;IACF;EACF;EAEAA,MAAM,GAAGE,IAAI,CAACE,GAAG,CAACJ,MAAM,EAAE,CAAC,CAAC;EAE5B,OAAOV,KAAK,GAAG,CAAC,IAAIM,KAAK,CAACN,KAAK,CAAC,GAAGU,MAAM,EAAE;IACzCV,KAAK,IAAI,CAAC;EACZ;EAEA,OAAOC,GAAG,GAAGE,UAAU,IAAIG,KAAK,CAACL,GAAG,CAAC,GAAGS,MAAM,EAAE;IAC9CT,GAAG,IAAI,CAAC;EACV;;EAEA;EACA,IAAIc,KAAK,GAAGf,KAAK,GAAG,CAAC;EAErB,OAAOe,KAAK,IAAId,GAAG,IAAIK,KAAK,CAACS,KAAK,CAAC,GAAGL,MAAM,EAAE;IAC5CK,KAAK,IAAI,CAAC;EACZ;EAEA,IAAIf,KAAK,GAAG,CAAC,IAAIe,KAAK,IAAId,GAAG,IAAI,QAAQ,CAACe,IAAI,CAACZ,EAAE,CAACW,KAAK,CAAC,CAACP,KAAK,IAAI,EAAE,CAAC,IAAIF,KAAK,CAACN,KAAK,CAAC,KAAKU,MAAM,EAAE;IAChGV,KAAK,IAAI,CAAC;EACZ;EAEA,OAAO,CAACA,KAAK,EAAEC,GAAG,CAAC;AACrB,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,IAAMgB,uBAAuB,GAAAlB,OAAA,CAAAkB,uBAAA,GAAG,SAA1BA,uBAAuBA,CAAIC,SAAS,EAAK;EACpD,IAAI,CAACA,SAAS,IAAI,OAAOA,SAAS,CAACC,gBAAgB,KAAK,UAAU,EAAE;IAClE,OAAOC,SAAS;EAClB;EAEA,IAAIC,QAAQ,GAAG,KAAK;EAEpB,IAAMC,MAAM,GAAG,SAATA,MAAMA,CAAA,EAAS;IACnB,IAAID,QAAQ,EAAE;MACZ;IACF;IAEA,IAAI;MACF,IAAIH,SAAS,CAACK,oBAAoB,IAAI,OAAOL,SAAS,CAACM,cAAc,KAAK,UAAU,EAAE;QACpF;MACF;MAEA,IAAMC,SAAS,GAAGP,SAAS,CAACO,SAAS;MACrC,IAAMC,KAAK,GAAGD,SAAS,IAAIA,SAAS,CAACE,MAAM,IAAIF,SAAS,CAACE,MAAM,CAAC,CAAC,CAAC;MAElE,IAAI,CAACD,KAAK,EAAE;QACV;MACF;MAEA,IAAAE,MAAA,OAAAC,eAAA,aAAqBH,KAAK;QAAnB1B,KAAK,GAAA4B,MAAA;QAAE3B,GAAG,GAAA2B,MAAA;MACjB,IAAME,EAAE,GAAGlB,IAAI,CAACC,GAAG,CAACb,KAAK,EAAEC,GAAG,CAAC;MAC/B,IAAM8B,EAAE,GAAGnB,IAAI,CAACE,GAAG,CAACd,KAAK,EAAEC,GAAG,CAAC;MAC/B,IAAA+B,YAAA,GAA6BlC,WAAW,CAACgC,EAAE,EAAEC,EAAE,EAAE,UAAC1B,CAAC;UAAA,OAAKa,SAAS,CAACM,cAAc,CAACnB,CAAC,CAAC;QAAA,GAAEa,SAAS,CAACf,UAAU,CAAC;QAAA8B,aAAA,OAAAJ,eAAA,aAAAG,YAAA;QAAnGE,SAAS,GAAAD,aAAA;QAAEE,OAAO,GAAAF,aAAA;MAEzB,IAAIC,SAAS,KAAKJ,EAAE,IAAIK,OAAO,KAAKJ,EAAE,EAAE;QACtC;MACF;MAEAV,QAAQ,GAAG,IAAI;MACfH,SAAS,CAACO,SAAS,GAAG;QAAEE,MAAM,EAAE,CAAC,CAACO,SAAS,EAAEC,OAAO,CAAC,CAAC;QAAEC,SAAS,EAAEX,SAAS,CAACW;MAAU,CAAC;IAC1F,CAAC,CAAC,OAAOC,CAAC,EAAE;MACVzC,GAAG,CAAC,uCAAuC,EAAEyC,CAAC,IAAIA,CAAC,CAACC,OAAO,CAAC;IAC9D,CAAC,SAAS;MACRjB,QAAQ,GAAG,KAAK;IAClB;EACF,CAAC;EAEDH,SAAS,CAACC,gBAAgB,CAAC,kBAAkB,EAAEG,MAAM,CAAC;EAEtD,OAAO;IAAA,OAAMJ,SAAS,CAACqB,mBAAmB,CAAC,kBAAkB,EAAEjB,MAAM,CAAC;EAAA;AACxE,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every place that draws MathLive output must apply the long-division
|
|
3
|
+
* correction (see longdiv.js): live fields after every render, static markup
|
|
4
|
+
* after it is written into the DOM.
|
|
5
|
+
*/
|
|
6
|
+
jest.mock('../mathlive-instance', () => {
|
|
7
|
+
class FakeMathfieldElement {
|
|
8
|
+
constructor() {
|
|
9
|
+
const el = global.document.createElement('math-field');
|
|
10
|
+
|
|
11
|
+
el.value = '';
|
|
12
|
+
el.getValue = () => el.value;
|
|
13
|
+
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
loadMathLive: () => Promise.resolve({ MathfieldElement: FakeMathfieldElement }),
|
|
20
|
+
getMacros: () => ({}),
|
|
21
|
+
MATH_MODE_SPACE: '\\ ',
|
|
22
|
+
applyShadowStyles: () => {},
|
|
23
|
+
trackPlaceholderCaret: () => () => {},
|
|
24
|
+
latexToMarkup: (l) => `<span>${l}</span>`,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const mockUntrack = jest.fn();
|
|
29
|
+
|
|
30
|
+
jest.mock('../longdiv', () => ({
|
|
31
|
+
trackLongDivision: jest.fn(() => mockUntrack),
|
|
32
|
+
fixLongDivisionWhenReady: jest.fn(),
|
|
33
|
+
fixLongDivision: jest.fn(),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
/* eslint-disable import/first */
|
|
37
|
+
import Input from '../mf/input';
|
|
38
|
+
import Static from '../mf/static';
|
|
39
|
+
import { LatexLabel } from '../keypad';
|
|
40
|
+
import { trackLongDivision, fixLongDivisionWhenReady } from '../longdiv';
|
|
41
|
+
/* eslint-enable import/first */
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
trackLongDivision.mockClear();
|
|
45
|
+
fixLongDivisionWhenReady.mockClear();
|
|
46
|
+
mockUntrack.mockClear();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Input (the editor)', () => {
|
|
50
|
+
it('tracks the mathfield on mount and stops on unmount', async () => {
|
|
51
|
+
const input = new Input({});
|
|
52
|
+
|
|
53
|
+
input.holderRef = { current: document.createElement('span') };
|
|
54
|
+
await input.componentDidMount();
|
|
55
|
+
|
|
56
|
+
expect(trackLongDivision).toHaveBeenCalledWith(input.mathField);
|
|
57
|
+
|
|
58
|
+
input.componentWillUnmount();
|
|
59
|
+
|
|
60
|
+
expect(mockUntrack).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(input.untrackLongDivision).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Static', () => {
|
|
66
|
+
it('tracks the answer-block field on mount and stops on unmount', async () => {
|
|
67
|
+
const s = new Static({ latex: 'x=\\MathQuillMathField[r1]{}' });
|
|
68
|
+
|
|
69
|
+
s.holderRef = { current: document.createElement('span') };
|
|
70
|
+
s.createLiveRegion = () => {};
|
|
71
|
+
await s.componentDidMount();
|
|
72
|
+
|
|
73
|
+
expect(trackLongDivision).toHaveBeenCalledWith(s.mathField);
|
|
74
|
+
|
|
75
|
+
s.componentWillUnmount();
|
|
76
|
+
|
|
77
|
+
expect(mockUntrack).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Display mode: the markup reaches the DOM through state, so it is corrected
|
|
81
|
+
// once React has written it.
|
|
82
|
+
it('corrects static markup after it is rendered', () => {
|
|
83
|
+
const s = new Static({ latex: '5\\longdiv{144}' });
|
|
84
|
+
const holder = document.createElement('span');
|
|
85
|
+
|
|
86
|
+
s.holderRef = { current: holder };
|
|
87
|
+
s.state = { markup: '<span>new</span>' };
|
|
88
|
+
s.componentDidUpdate({ latex: '5\\longdiv{144}' }, { markup: '' });
|
|
89
|
+
|
|
90
|
+
expect(fixLongDivisionWhenReady).toHaveBeenCalledWith(holder);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not redo it when the markup did not change', () => {
|
|
94
|
+
const s = new Static({ latex: 'x' });
|
|
95
|
+
|
|
96
|
+
s.holderRef = { current: document.createElement('span') };
|
|
97
|
+
s.state = { markup: '<span>same</span>' };
|
|
98
|
+
s.componentDidUpdate({ latex: 'x' }, { markup: '<span>same</span>' });
|
|
99
|
+
|
|
100
|
+
expect(fixLongDivisionWhenReady).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('leaves a live field to the tracker', () => {
|
|
104
|
+
const s = new Static({ latex: 'x' });
|
|
105
|
+
|
|
106
|
+
s.holderRef = { current: document.createElement('span') };
|
|
107
|
+
s.mathField = document.createElement('math-field');
|
|
108
|
+
s.state = { markup: '<span>new</span>' };
|
|
109
|
+
s.componentDidUpdate({ latex: 'x' }, { markup: '' });
|
|
110
|
+
|
|
111
|
+
expect(fixLongDivisionWhenReady).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('keypad label', () => {
|
|
116
|
+
it('corrects the label once mounted', async () => {
|
|
117
|
+
const label = new LatexLabel({ latex: '\\longdiv{}' });
|
|
118
|
+
const el = document.createElement('span');
|
|
119
|
+
|
|
120
|
+
label.labelRef = { current: el };
|
|
121
|
+
await label.componentDidMount();
|
|
122
|
+
|
|
123
|
+
expect(fixLongDivisionWhenReady).toHaveBeenCalledWith(el);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('corrects it again when the label changes', () => {
|
|
127
|
+
const label = new LatexLabel({ latex: '\\longdiv{}' });
|
|
128
|
+
const el = document.createElement('span');
|
|
129
|
+
|
|
130
|
+
label.labelRef = { current: el };
|
|
131
|
+
label.setState = () => {};
|
|
132
|
+
label.state = { markup: '<span>other</span>' };
|
|
133
|
+
label.componentDidUpdate({ latex: '\\longdiv{}' }, { markup: '<span>first</span>' });
|
|
134
|
+
|
|
135
|
+
expect(fixLongDivisionWhenReady).toHaveBeenCalledWith(el);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { fixLongDivision, fixLongDivisionWhenReady, trackLongDivision } from '../longdiv';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Markup captured verbatim from MathLive 0.110.0 `convertLatexToMarkup` in
|
|
5
|
+
* Chrome. In `12\enclose{longdiv}{144}` MathLive drops the dividend 0.45em
|
|
6
|
+
* (`top:0.45em`), sizes the box 2.1em wide while the viewBox is 2.54 wide, and
|
|
7
|
+
* ends the bar at 1.8 although the dividend runs to 0.3 + 0.45 + 1.5 = 2.25.
|
|
8
|
+
*/
|
|
9
|
+
const LONGDIV =
|
|
10
|
+
'<span class="ML__latex"><span class="ML__strut" style="height:0.8em"></span><span class="ML__strut--bottom" style="height:1.25em;vertical-align:-0.44em"></span><span class="ML__base"><span class="ML__cmr">12</span><span style="position:relative;height:1.24em;display:inline-block;margin-left:0.3em;margin-right:0.3em;width:1.95em"><span class="ML__notation" style="box-sizing:border-box;left:calc(-1 * 1px - 0.3em);height:calc( 1.24444em + 1px);top:0em;width:calc(2.1em + 1px);background-color:transparent"><span style="display: inline-block;height:1.24em;vertical-align:0.44em;"></span><svg style="position:absolute;overflow:visible;height:1.24em;top:0;left:0;width:100%;z-index:2;" stroke-width="0.06em" stroke="currentColor" stroke-linecap="round" viewBox="0 0 2.54 1.24"><line x1="0.3" y1="0.3" x2="1.8" y2="0.3" vector-effect="non-scaling-stroke"></line><path d="M 0.3 0.3 a0.3 0.46997, 0, 1, 1, 0 0.93994 " stroke-width="0.004em" stroke="currentColor" fill="none"/></svg></span><span style="position:relative;display:inline-block;height:0.65em;width:1.5em;margin-left:0.45em;top:0.45em"><span class="ML__cmr">144</span></span></span></span></span>';
|
|
11
|
+
|
|
12
|
+
// \enclose{box}{x}: another notation, which must be left alone
|
|
13
|
+
const BOX =
|
|
14
|
+
'<span class="ML__latex"><span class="ML__strut" style="height:1.04em"></span><span class="ML__base"><span style="position:relative;height:1.03em;display:inline-block;margin-left:0.3em;margin-right:0.3em;width:0.58em"><span class="ML__notation" style="box-sizing:border-box;left:calc(-1 * 1px - 0.3em);height:calc( 1.03056em + 1px);top:0em;width:calc(1.17153em + 1px);background-color:transparent;border:1px solid red"></span><span style="position:relative;display:inline-block;height:0.44em;width:0.58em"><span class="ML__mathit">x</span></span></span></span></span>';
|
|
15
|
+
|
|
16
|
+
const mount = (html) => {
|
|
17
|
+
const el = document.createElement('div');
|
|
18
|
+
|
|
19
|
+
el.innerHTML = html;
|
|
20
|
+
document.body.appendChild(el);
|
|
21
|
+
|
|
22
|
+
return el;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parts = (el) => {
|
|
26
|
+
const notation = el.querySelector('.ML__notation');
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
notation,
|
|
30
|
+
box: notation.parentElement,
|
|
31
|
+
dividend: notation.nextElementSibling,
|
|
32
|
+
line: notation.querySelector('line'),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
document.body.innerHTML = '';
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('fixLongDivision', () => {
|
|
41
|
+
it('puts the dividend back on the baseline', () => {
|
|
42
|
+
const el = mount(LONGDIV);
|
|
43
|
+
|
|
44
|
+
fixLongDivision(el);
|
|
45
|
+
|
|
46
|
+
const { dividend, box } = parts(el);
|
|
47
|
+
|
|
48
|
+
expect(dividend.style.top).toEqual('0px');
|
|
49
|
+
// the drawing keeps its place relative to the dividend, and the line grows
|
|
50
|
+
// so the field does not clip the bar
|
|
51
|
+
expect(box.style.paddingTop).toEqual('0.45em');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('sizes the box to the viewBox, so the drawing is not scaled down', () => {
|
|
55
|
+
const el = mount(LONGDIV);
|
|
56
|
+
|
|
57
|
+
fixLongDivision(el);
|
|
58
|
+
|
|
59
|
+
expect(parts(el).notation.style.width).toEqual('calc(2.54em + 1px)');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('runs the bar to the end of the dividend', () => {
|
|
63
|
+
const el = mount(LONGDIV);
|
|
64
|
+
|
|
65
|
+
fixLongDivision(el);
|
|
66
|
+
|
|
67
|
+
// no layout under jsdom, so from MathLive's numbers: start + gap + width
|
|
68
|
+
expect(parseFloat(parts(el).line.getAttribute('x2'))).toBeCloseTo(0.3 + 0.45 + 1.5, 6);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// MathLive's width leaves out the spacing around `,` and `+`, so when the
|
|
72
|
+
// markup is laid out the end is measured instead.
|
|
73
|
+
it('measures the bar end from the rendered dividend when laid out', () => {
|
|
74
|
+
const el = mount(LONGDIV);
|
|
75
|
+
const { notation, dividend } = parts(el);
|
|
76
|
+
const svg = notation.querySelector('svg');
|
|
77
|
+
const glyph = dividend.querySelector('.ML__cmr');
|
|
78
|
+
|
|
79
|
+
// the svg is 254px wide for a 2.54-unit viewBox: 100px per unit
|
|
80
|
+
svg.getBoundingClientRect = () => ({ left: 100, width: 254, right: 354 });
|
|
81
|
+
glyph.getBoundingClientRect = () => ({ left: 145, right: 340, width: 195 });
|
|
82
|
+
|
|
83
|
+
fixLongDivision(el);
|
|
84
|
+
|
|
85
|
+
expect(parseFloat(parts(el).line.getAttribute('x2'))).toBeCloseTo(2.4, 6);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('ignores the caret when measuring', () => {
|
|
89
|
+
const el = mount(LONGDIV);
|
|
90
|
+
const { notation, dividend } = parts(el);
|
|
91
|
+
const caret = document.createElement('span');
|
|
92
|
+
|
|
93
|
+
caret.className = 'ML__caret';
|
|
94
|
+
dividend.appendChild(caret);
|
|
95
|
+
notation.querySelector('svg').getBoundingClientRect = () => ({ left: 100, width: 254, right: 354 });
|
|
96
|
+
dividend.querySelector('.ML__cmr').getBoundingClientRect = () => ({ left: 145, right: 340, width: 195 });
|
|
97
|
+
caret.getBoundingClientRect = () => ({ left: 400, right: 402, width: 2 });
|
|
98
|
+
|
|
99
|
+
fixLongDivision(el);
|
|
100
|
+
|
|
101
|
+
expect(parseFloat(parts(el).line.getAttribute('x2'))).toBeCloseTo(2.4, 6);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('can run again without drifting', () => {
|
|
105
|
+
const el = mount(LONGDIV);
|
|
106
|
+
|
|
107
|
+
fixLongDivision(el);
|
|
108
|
+
const first = { ...parts(el), x2: parts(el).line.getAttribute('x2') };
|
|
109
|
+
|
|
110
|
+
fixLongDivision(el);
|
|
111
|
+
const second = parts(el);
|
|
112
|
+
|
|
113
|
+
expect(second.line.getAttribute('x2')).toEqual(first.x2);
|
|
114
|
+
expect(second.box.style.paddingTop).toEqual('0.45em');
|
|
115
|
+
expect(second.notation.style.width).toEqual('calc(2.54em + 1px)');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('leaves other enclose notations alone', () => {
|
|
119
|
+
const el = mount(BOX);
|
|
120
|
+
const before = el.innerHTML;
|
|
121
|
+
|
|
122
|
+
expect(fixLongDivision(el)).toBe(0);
|
|
123
|
+
expect(el.innerHTML).toEqual(before);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('counts what it corrected', () => {
|
|
127
|
+
expect(fixLongDivision(mount(LONGDIV + LONGDIV))).toBe(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is safe on nothing', () => {
|
|
131
|
+
expect(fixLongDivision(null)).toBe(0);
|
|
132
|
+
expect(fixLongDivision(mount('<span>plain</span>'))).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('fixLongDivisionWhenReady', () => {
|
|
137
|
+
it('corrects now, and again once the fonts have loaded', async () => {
|
|
138
|
+
let fontsLoaded;
|
|
139
|
+
const original = document.fonts;
|
|
140
|
+
|
|
141
|
+
Object.defineProperty(document, 'fonts', {
|
|
142
|
+
configurable: true,
|
|
143
|
+
value: { ready: new Promise((resolve) => (fontsLoaded = resolve)) },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const el = mount(LONGDIV);
|
|
148
|
+
|
|
149
|
+
fixLongDivisionWhenReady(el);
|
|
150
|
+
expect(parts(el).dividend.style.top).toEqual('0px');
|
|
151
|
+
|
|
152
|
+
// a re-render before the fonts arrive...
|
|
153
|
+
el.innerHTML = LONGDIV;
|
|
154
|
+
fontsLoaded();
|
|
155
|
+
await Promise.resolve();
|
|
156
|
+
await Promise.resolve();
|
|
157
|
+
|
|
158
|
+
// ...is corrected once they do
|
|
159
|
+
expect(parts(el).dividend.style.top).toEqual('0px');
|
|
160
|
+
} finally {
|
|
161
|
+
Object.defineProperty(document, 'fonts', { configurable: true, value: original });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('trackLongDivision', () => {
|
|
167
|
+
const field = () => {
|
|
168
|
+
const host = document.createElement('div');
|
|
169
|
+
|
|
170
|
+
host.attachShadow({ mode: 'open' });
|
|
171
|
+
host.shadowRoot.innerHTML = LONGDIV;
|
|
172
|
+
document.body.appendChild(host);
|
|
173
|
+
|
|
174
|
+
return host;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
it('corrects the field straight away', () => {
|
|
178
|
+
const host = field();
|
|
179
|
+
|
|
180
|
+
trackLongDivision(host);
|
|
181
|
+
|
|
182
|
+
expect(parts(host.shadowRoot).dividend.style.top).toEqual('0px');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// jest.setup.js replaces MutationObserver with a no-op for every test, so this
|
|
186
|
+
// installs one that records what it was asked to observe and can be fired.
|
|
187
|
+
describe('re-rendering', () => {
|
|
188
|
+
let observers;
|
|
189
|
+
const RealObserver = global.MutationObserver;
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
observers = [];
|
|
193
|
+
global.MutationObserver = class {
|
|
194
|
+
constructor(callback) {
|
|
195
|
+
this.callback = callback;
|
|
196
|
+
this.disconnected = false;
|
|
197
|
+
observers.push(this);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
observe(target, options) {
|
|
201
|
+
this.target = target;
|
|
202
|
+
this.options = options;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
disconnect() {
|
|
206
|
+
this.disconnected = true;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
global.MutationObserver = RealObserver;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('watches the whole shadow tree for MathLive rebuilding it', () => {
|
|
216
|
+
const host = field();
|
|
217
|
+
|
|
218
|
+
trackLongDivision(host);
|
|
219
|
+
|
|
220
|
+
expect(observers).toHaveLength(1);
|
|
221
|
+
expect(observers[0].target).toBe(host.shadowRoot);
|
|
222
|
+
expect(observers[0].options).toEqual({ childList: true, subtree: true });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// MathLive rebuilds its shadow DOM on every keystroke, resize and font load.
|
|
226
|
+
it('re-corrects after MathLive re-renders', () => {
|
|
227
|
+
const host = field();
|
|
228
|
+
|
|
229
|
+
trackLongDivision(host);
|
|
230
|
+
host.shadowRoot.innerHTML = LONGDIV;
|
|
231
|
+
expect(parts(host.shadowRoot).dividend.style.top).toEqual('0.45em');
|
|
232
|
+
|
|
233
|
+
observers[0].callback([]);
|
|
234
|
+
|
|
235
|
+
expect(parts(host.shadowRoot).dividend.style.top).toEqual('0px');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// The corrections change attributes only, so observing childList alone
|
|
239
|
+
// cannot loop.
|
|
240
|
+
it('does not observe attributes', () => {
|
|
241
|
+
trackLongDivision(field());
|
|
242
|
+
|
|
243
|
+
expect(observers[0].options.attributes).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('stops after teardown', () => {
|
|
247
|
+
const untrack = trackLongDivision(field());
|
|
248
|
+
|
|
249
|
+
untrack();
|
|
250
|
+
|
|
251
|
+
expect(observers[0].disconnected).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('is a no-op without a shadow root', () => {
|
|
256
|
+
expect(trackLongDivision(undefined)).toBeUndefined();
|
|
257
|
+
expect(trackLongDivision(document.createElement('div'))).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -8,17 +8,52 @@ const table = (rows) => (i) => (rows[i] ? { depth: rows[i][0], latex: rows[i][1]
|
|
|
8
8
|
|
|
9
9
|
// 5x^2+2x+\frac{\pi}{2}-4
|
|
10
10
|
const POLY = table([
|
|
11
|
-
[0, ''],
|
|
12
|
-
[
|
|
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'],
|
|
13
28
|
]);
|
|
14
29
|
const POLY_LAST = 16;
|
|
15
30
|
|
|
16
31
|
// a+\sqrt{x+1}+b
|
|
17
|
-
const ROOT = table([
|
|
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
|
+
]);
|
|
18
44
|
|
|
19
45
|
// \frac{\frac{a}{b}}{c}
|
|
20
46
|
const NESTED = table([
|
|
21
|
-
[0, ''],
|
|
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}'],
|
|
22
57
|
]);
|
|
23
58
|
|
|
24
59
|
describe('expandRange', () => {
|
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
|
);
|