@khanacademy/math-input 17.0.3 → 17.0.5
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/es/index.js +2 -2
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/.eslintrc.js +0 -18
- package/CHANGELOG.md +0 -654
- package/less/main.less +0 -2
- package/less/overrides.less +0 -122
- package/src/components/__tests__/integration.test.tsx +0 -300
- package/src/components/aphrodite-css-transition-group/index.tsx +0 -78
- package/src/components/aphrodite-css-transition-group/transition-child.tsx +0 -192
- package/src/components/aphrodite-css-transition-group/types.ts +0 -20
- package/src/components/aphrodite-css-transition-group/util.ts +0 -97
- package/src/components/input/__tests__/context-tracking.test.ts +0 -176
- package/src/components/input/__tests__/mathquill-helpers.test.ts +0 -105
- package/src/components/input/__tests__/mathquill.test.ts +0 -747
- package/src/components/input/__tests__/test-math-wrapper.ts +0 -29
- package/src/components/input/cursor-contexts.ts +0 -37
- package/src/components/input/cursor-handle.tsx +0 -137
- package/src/components/input/cursor-styles.ts +0 -10
- package/src/components/input/drag-listener.ts +0 -79
- package/src/components/input/math-input.tsx +0 -1036
- package/src/components/input/math-wrapper.ts +0 -189
- package/src/components/input/mathquill-helpers.ts +0 -262
- package/src/components/input/mathquill-instance.ts +0 -106
- package/src/components/input/mathquill-types.ts +0 -32
- package/src/components/input/scroll-into-view.ts +0 -65
- package/src/components/key-handlers/__tests__/handle-jump-out.test.ts +0 -94
- package/src/components/key-handlers/handle-arrow.ts +0 -70
- package/src/components/key-handlers/handle-backspace.ts +0 -277
- package/src/components/key-handlers/handle-exponent.ts +0 -53
- package/src/components/key-handlers/handle-jump-out.ts +0 -107
- package/src/components/key-handlers/key-translator.ts +0 -222
- package/src/components/keypad/__tests__/__snapshots__/keypad.test.tsx.snap +0 -1913
- package/src/components/keypad/__tests__/__snapshots__/mobile-keypad.test.tsx.snap +0 -600
- package/src/components/keypad/__tests__/keypad-button.test.tsx +0 -84
- package/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx +0 -304
- package/src/components/keypad/__tests__/keypad-v2.cypress.ts +0 -16
- package/src/components/keypad/__tests__/keypad.test.tsx +0 -321
- package/src/components/keypad/__tests__/mobile-keypad.test.tsx +0 -115
- package/src/components/keypad/__tests__/test-data-tabs.ts +0 -21
- package/src/components/keypad/button-assets.tsx +0 -1880
- package/src/components/keypad/index.tsx +0 -2
- package/src/components/keypad/keypad-button.stories.tsx +0 -81
- package/src/components/keypad/keypad-button.tsx +0 -124
- package/src/components/keypad/keypad-mathquill.stories.tsx +0 -109
- package/src/components/keypad/keypad-pages/extras-page.tsx +0 -35
- package/src/components/keypad/keypad-pages/fractions-page.tsx +0 -125
- package/src/components/keypad/keypad-pages/geometry-page.tsx +0 -34
- package/src/components/keypad/keypad-pages/keypad-pages.stories.tsx +0 -37
- package/src/components/keypad/keypad-pages/numbers-page.tsx +0 -94
- package/src/components/keypad/keypad-pages/operators-page.tsx +0 -117
- package/src/components/keypad/keypad.tsx +0 -233
- package/src/components/keypad/mobile-keypad-internals.tsx +0 -240
- package/src/components/keypad/mobile-keypad.tsx +0 -24
- package/src/components/keypad/navigation-button.tsx +0 -127
- package/src/components/keypad/navigation-pad.stories.tsx +0 -26
- package/src/components/keypad/navigation-pad.tsx +0 -67
- package/src/components/keypad/shared-keys.tsx +0 -109
- package/src/components/keypad/utils.ts +0 -34
- package/src/components/keypad-context.tsx +0 -70
- package/src/components/prop-types.ts +0 -16
- package/src/components/tabbar/__tests__/tabbar.test.tsx +0 -105
- package/src/components/tabbar/icons.tsx +0 -122
- package/src/components/tabbar/index.ts +0 -1
- package/src/components/tabbar/item.tsx +0 -146
- package/src/components/tabbar/tabbar.stories.tsx +0 -83
- package/src/components/tabbar/tabbar.tsx +0 -65
- package/src/data/key-configs.ts +0 -770
- package/src/data/keys.ts +0 -123
- package/src/enums.ts +0 -27
- package/src/fake-react-native-web/index.ts +0 -11
- package/src/fake-react-native-web/text.tsx +0 -55
- package/src/fake-react-native-web/view.tsx +0 -91
- package/src/full-keypad.stories.tsx +0 -142
- package/src/full-mobile-input.stories.tsx +0 -115
- package/src/index.ts +0 -52
- package/src/types.ts +0 -70
- package/src/utils.test.ts +0 -33
- package/src/utils.ts +0 -61
- package/src/version.ts +0 -10
- package/tsconfig-build.json +0 -11
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// Notes about MathQuill
|
|
2
|
-
//
|
|
3
|
-
// MathQuill's stores its layout as nested linked lists. Each node in the
|
|
4
|
-
// list has MQ.L '-1' and MQ.R '1' properties that define links to
|
|
5
|
-
// the left and right nodes respectively. They also have
|
|
6
|
-
//
|
|
7
|
-
// ctrlSeq: contains the latex code snippet that defines that node.
|
|
8
|
-
// jQ: jQuery object for the DOM node(s) for this MathQuill node.
|
|
9
|
-
// ends: pointers to the nodes at the ends of the container.
|
|
10
|
-
// parent: parent node.
|
|
11
|
-
// blocks: an array containing one or more nodes that make up the node.
|
|
12
|
-
// sub?: subscript node if there is one as is the case in log_n
|
|
13
|
-
//
|
|
14
|
-
// All of the code below is super fragile. Please be especially careful
|
|
15
|
-
// when upgrading MathQuill.
|
|
16
|
-
|
|
17
|
-
import $ from "jquery";
|
|
18
|
-
|
|
19
|
-
import handleBackspace from "../key-handlers/handle-backspace";
|
|
20
|
-
import keyTranslator from "../key-handlers/key-translator";
|
|
21
|
-
|
|
22
|
-
import {getCursorContext, maybeFindCommand} from "./mathquill-helpers";
|
|
23
|
-
import {createMathField, mathQuillInstance} from "./mathquill-instance";
|
|
24
|
-
|
|
25
|
-
import type {
|
|
26
|
-
MathFieldInterface,
|
|
27
|
-
MathFieldUpdaterCallback,
|
|
28
|
-
} from "./mathquill-types";
|
|
29
|
-
import type Key from "../../data/keys";
|
|
30
|
-
|
|
31
|
-
const mobileKeyTranslator: Record<Key, MathFieldUpdaterCallback> = {
|
|
32
|
-
...keyTranslator,
|
|
33
|
-
// note(Matthew): our mobile backspace logic is really complicated
|
|
34
|
-
// and for some reason doesn't really work in the desktop experience.
|
|
35
|
-
// So we default to the basic backspace functionality in the
|
|
36
|
-
// key translator and overwrite it with the complicated logic here
|
|
37
|
-
// until we can unify the experiences (if we even want to).
|
|
38
|
-
// https://khanacademy.atlassian.net/browse/LC-906
|
|
39
|
-
BACKSPACE: handleBackspace,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* This file contains a wrapper around MathQuill so that we can provide a
|
|
44
|
-
* more regular interface for the functionality we need while insulating us
|
|
45
|
-
* from MathQuill changes.
|
|
46
|
-
*/
|
|
47
|
-
class MathWrapper {
|
|
48
|
-
mathField: MathFieldInterface; // MathQuill MathField input
|
|
49
|
-
callbacks: any;
|
|
50
|
-
|
|
51
|
-
constructor(element, callbacks = {}) {
|
|
52
|
-
this.mathField = createMathField(element, () => {
|
|
53
|
-
return {
|
|
54
|
-
// use a span instead of a textarea so that we don't bring up the
|
|
55
|
-
// native keyboard on mobile when selecting the input
|
|
56
|
-
substituteTextarea: function () {
|
|
57
|
-
return document.createElement("span");
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
this.callbacks = callbacks;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
focus() {
|
|
65
|
-
// HACK(charlie): We shouldn't reaching into MathQuill internals like
|
|
66
|
-
// this, but it's the easiest way to allow us to manage the focus state
|
|
67
|
-
// ourselves.
|
|
68
|
-
this.mathField.cursor().show();
|
|
69
|
-
|
|
70
|
-
// Set MathQuill's internal state to reflect the focus, otherwise it
|
|
71
|
-
// will consistently try to hide the cursor on key-press and introduce
|
|
72
|
-
// layout jank.
|
|
73
|
-
this.mathField.focus();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
blur() {
|
|
77
|
-
this.mathField.cursor().hide();
|
|
78
|
-
this.mathField.blur();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Handle a key press and return the resulting cursor state.
|
|
83
|
-
*
|
|
84
|
-
* @param {Key} key - an enum representing the key that was pressed
|
|
85
|
-
* @returns {object} a cursor object, consisting of a cursor context
|
|
86
|
-
*/
|
|
87
|
-
pressKey(key: Key) {
|
|
88
|
-
const cursor = this.getCursor();
|
|
89
|
-
const translator = mobileKeyTranslator[key];
|
|
90
|
-
|
|
91
|
-
if (translator) {
|
|
92
|
-
translator(this.mathField, key);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!cursor.selection) {
|
|
96
|
-
// don't show the cursor for selections
|
|
97
|
-
cursor.show();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (this.callbacks.onSelectionChanged) {
|
|
101
|
-
this.callbacks.onSelectionChanged(cursor.selection);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// NOTE(charlie): It's insufficient to do this as an `edited` handler
|
|
105
|
-
// on the MathField, as that handler isn't triggered on navigation
|
|
106
|
-
// events.
|
|
107
|
-
return {
|
|
108
|
-
context: this.contextForCursor(),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Place the cursor beside the node located at the given coordinates.
|
|
114
|
-
*
|
|
115
|
-
* @param {number} x - the x coordinate in the viewport
|
|
116
|
-
* @param {number} y - the y coordinate in the viewport
|
|
117
|
-
* @param {Node} hitNode - the node next to which the cursor should be
|
|
118
|
-
* placed; if provided, the coordinates will be used
|
|
119
|
-
* to determine on which side of the node the cursor
|
|
120
|
-
* should be placed
|
|
121
|
-
*/
|
|
122
|
-
setCursorPosition(x: number, y: number, hitNode: HTMLElement) {
|
|
123
|
-
const el = hitNode || document.elementFromPoint(x, y);
|
|
124
|
-
|
|
125
|
-
if (el) {
|
|
126
|
-
const cursor = this.getCursor();
|
|
127
|
-
|
|
128
|
-
if (el.hasAttribute("mq-root-block")) {
|
|
129
|
-
// If we're in the empty area place the cursor at the right
|
|
130
|
-
// end of the expression.
|
|
131
|
-
cursor.insAtRightEnd(this.mathField.controller().root);
|
|
132
|
-
} else {
|
|
133
|
-
// Otherwise place beside the element at x, y.
|
|
134
|
-
const controller = this.mathField.controller();
|
|
135
|
-
|
|
136
|
-
const pageX = x - document.body.scrollLeft;
|
|
137
|
-
const pageY = y - document.body.scrollTop;
|
|
138
|
-
controller.seek($(el), pageX, pageY).cursor.startSelection();
|
|
139
|
-
|
|
140
|
-
// Unless that would leave us mid-command, in which case, we
|
|
141
|
-
// need to adjust and place the cursor inside the parens
|
|
142
|
-
// following the command.
|
|
143
|
-
const command = maybeFindCommand(cursor[mathQuillInstance.L]);
|
|
144
|
-
if (command && command.endNode) {
|
|
145
|
-
// NOTE(charlie): endNode should definitely be \left(.
|
|
146
|
-
cursor.insLeftOf(command.endNode);
|
|
147
|
-
this.mathField.keystroke("Right");
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (this.callbacks.onCursorMove) {
|
|
152
|
-
this.callbacks.onCursorMove({
|
|
153
|
-
context: this.contextForCursor(),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// note(Matthew): extracted this logic to share it elsewhere,
|
|
160
|
-
// but it's part of the public MathWrapper API
|
|
161
|
-
getCursor() {
|
|
162
|
-
return this.mathField.cursor();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// note(Matthew): extracted this logic to keep this file focused,
|
|
166
|
-
// but it's part of the public MathWrapper API
|
|
167
|
-
contextForCursor() {
|
|
168
|
-
return getCursorContext(this.mathField);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
getSelection() {
|
|
172
|
-
return this.getCursor().selection;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
getContent() {
|
|
176
|
-
return this.mathField.latex();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
setContent(latex: string) {
|
|
180
|
-
this.mathField.latex(latex);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
isEmpty() {
|
|
184
|
-
const cursor = this.getCursor();
|
|
185
|
-
return cursor.parent.id === 1 && cursor[1] === 0 && cursor[-1] === 0;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export default MathWrapper;
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import {CursorContext} from "./cursor-contexts";
|
|
2
|
-
import {mathQuillInstance} from "./mathquill-instance";
|
|
3
|
-
import {MathFieldActionType} from "./mathquill-types";
|
|
4
|
-
|
|
5
|
-
import type {MathFieldInterface} from "./mathquill-types";
|
|
6
|
-
|
|
7
|
-
const Numerals = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
8
|
-
const GreekLetters = ["\\theta", "\\pi"];
|
|
9
|
-
const Letters = [
|
|
10
|
-
"A",
|
|
11
|
-
"B",
|
|
12
|
-
"C",
|
|
13
|
-
"D",
|
|
14
|
-
"E",
|
|
15
|
-
"F",
|
|
16
|
-
"G",
|
|
17
|
-
"H",
|
|
18
|
-
"I",
|
|
19
|
-
"J",
|
|
20
|
-
"K",
|
|
21
|
-
"L",
|
|
22
|
-
"M",
|
|
23
|
-
"N",
|
|
24
|
-
"O",
|
|
25
|
-
"P",
|
|
26
|
-
"Q",
|
|
27
|
-
"R",
|
|
28
|
-
"S",
|
|
29
|
-
"T",
|
|
30
|
-
"U",
|
|
31
|
-
"V",
|
|
32
|
-
"W",
|
|
33
|
-
"X",
|
|
34
|
-
"Y",
|
|
35
|
-
"Z",
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
// We only consider numerals, variables, and Greek Letters to be proper
|
|
39
|
-
// leaf nodes.
|
|
40
|
-
const ValidLeaves = [
|
|
41
|
-
...Numerals,
|
|
42
|
-
...GreekLetters,
|
|
43
|
-
...Letters.map((letter) => letter.toLowerCase()),
|
|
44
|
-
...Letters.map((letter) => letter.toUpperCase()),
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
const mqNodeHasClass = (node: any, className: string): boolean =>
|
|
48
|
-
node._el && (node._el as HTMLElement).classList.contains(className);
|
|
49
|
-
|
|
50
|
-
export function isFraction(node): boolean {
|
|
51
|
-
return mqNodeHasClass(node, "mq-fraction");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isNumerator(node): boolean {
|
|
55
|
-
return mqNodeHasClass(node, "mq-numerator");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function isDenominator(node): boolean {
|
|
59
|
-
return mqNodeHasClass(node, "mq-denominator");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function isSubScript(node): boolean {
|
|
63
|
-
// NOTE(charlie): MyScript has a structure whereby its superscripts seem
|
|
64
|
-
// to be represented as a parent node with 'mq-sup-only' containing a
|
|
65
|
-
// single child with 'mq-sup'.
|
|
66
|
-
return (
|
|
67
|
-
mqNodeHasClass(node, "mq-sub-only") || mqNodeHasClass(node, "mq-sub")
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function isSuperScript(node): boolean {
|
|
72
|
-
// NOTE(charlie): MyScript has a structure whereby its superscripts seem
|
|
73
|
-
// to be represented as a parent node with 'mq-sup-only' containing a
|
|
74
|
-
// single child with 'mq-sup'.
|
|
75
|
-
return (
|
|
76
|
-
mqNodeHasClass(node, "mq-sup-only") || mqNodeHasClass(node, "mq-sup")
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isParens(node): boolean {
|
|
81
|
-
return node && node.ctrlSeq === "\\left(";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function isLeaf(node): boolean {
|
|
85
|
-
return node && node.ctrlSeq && ValidLeaves.includes(node.ctrlSeq.trim());
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function isSquareRoot(node): boolean {
|
|
89
|
-
return node.blocks && mqNodeHasClass(node.blocks[0], "mq-sqrt-stem");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function isNthRoot(node): boolean {
|
|
93
|
-
return node.blocks && mqNodeHasClass(node.blocks[0], "mq-nthroot");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function isNthRootIndex(node): boolean {
|
|
97
|
-
return mqNodeHasClass(node, "mq-nthroot");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function isInsideLogIndex(cursor): boolean {
|
|
101
|
-
const grandparent = cursor.parent.parent;
|
|
102
|
-
|
|
103
|
-
if (grandparent && mqNodeHasClass(grandparent, "mq-supsub")) {
|
|
104
|
-
const command = maybeFindCommandBeforeParens(grandparent);
|
|
105
|
-
|
|
106
|
-
if (command && command.name === "\\log") {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function isInsideEmptyNode(cursor): boolean {
|
|
115
|
-
return (
|
|
116
|
-
cursor[mathQuillInstance.L] === MathFieldActionType.MQ_END &&
|
|
117
|
-
cursor[mathQuillInstance.R] === MathFieldActionType.MQ_END
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function selectNode(node, cursor) {
|
|
122
|
-
cursor.insLeftOf(node);
|
|
123
|
-
cursor.startSelection();
|
|
124
|
-
cursor.insRightOf(node);
|
|
125
|
-
cursor.select();
|
|
126
|
-
cursor.endSelection();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Return the start node, end node, and full name of the command of which
|
|
131
|
-
* the initial node is a part, or `null` if the node is not part of a
|
|
132
|
-
* command.
|
|
133
|
-
*
|
|
134
|
-
* @param {node} initialNode - the node to included as part of the command
|
|
135
|
-
* @returns {null|object} - `null` or an object containing the start node
|
|
136
|
-
* (`startNode`), end node (`endNode`), and full
|
|
137
|
-
* name (`name`) of the command
|
|
138
|
-
*/
|
|
139
|
-
export function maybeFindCommand(initialNode) {
|
|
140
|
-
if (!initialNode) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// MathQuill stores commands as separate characters so that
|
|
145
|
-
// users can delete commands one character at a time. We iterate over
|
|
146
|
-
// the nodes from right to left until we hit a sequence starting with a
|
|
147
|
-
// '\\', which signifies the start of a command; then we iterate from
|
|
148
|
-
// left to right until we hit a '\\left(', which signifies the end of a
|
|
149
|
-
// command. If we encounter any character that doesn't belong in a
|
|
150
|
-
// command, we return null. We match a single character at a time.
|
|
151
|
-
// Ex) ['\\l', 'o', 'g ', '\\left(', ...]
|
|
152
|
-
const commandCharRegex = /^[a-z]$/;
|
|
153
|
-
const commandStartRegex = /^\\[a-z]$/;
|
|
154
|
-
const commandEndSeq = "\\left(";
|
|
155
|
-
|
|
156
|
-
// Note: We allowlist the set of valid commands, since relying solely on
|
|
157
|
-
// a command being prefixed with a backslash leads to undesired
|
|
158
|
-
// behavior. For example, Greek symbols, left parentheses, and square
|
|
159
|
-
// roots all get treated as commands.
|
|
160
|
-
const validCommands = ["\\log", "\\ln", "\\cos", "\\sin", "\\tan"];
|
|
161
|
-
|
|
162
|
-
let name = "";
|
|
163
|
-
let startNode;
|
|
164
|
-
let endNode;
|
|
165
|
-
|
|
166
|
-
// Collect the portion of the command from the current node, leftwards
|
|
167
|
-
// until the start of the command.
|
|
168
|
-
let node = initialNode;
|
|
169
|
-
while (node !== 0) {
|
|
170
|
-
const ctrlSeq = node.ctrlSeq.trim();
|
|
171
|
-
if (commandCharRegex.test(ctrlSeq)) {
|
|
172
|
-
name = ctrlSeq + name;
|
|
173
|
-
} else if (commandStartRegex.test(ctrlSeq)) {
|
|
174
|
-
name = ctrlSeq + name;
|
|
175
|
-
startNode = node;
|
|
176
|
-
break;
|
|
177
|
-
} else {
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
node = node[mathQuillInstance.L];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// If we hit the start of a command, then grab the rest of it by
|
|
185
|
-
// iterating rightwards to compute the full name of the command, along
|
|
186
|
-
// with its terminal node.
|
|
187
|
-
if (startNode) {
|
|
188
|
-
// Next, iterate from the start to the right.
|
|
189
|
-
node = initialNode[mathQuillInstance.R];
|
|
190
|
-
while (node !== 0) {
|
|
191
|
-
const ctrlSeq = node.ctrlSeq.trim();
|
|
192
|
-
if (commandCharRegex.test(ctrlSeq)) {
|
|
193
|
-
// If we have a single character, add it to the command
|
|
194
|
-
// name.
|
|
195
|
-
name = name + ctrlSeq;
|
|
196
|
-
} else if (ctrlSeq === commandEndSeq) {
|
|
197
|
-
// If we hit the command end delimiter (the left
|
|
198
|
-
// parentheses surrounding its arguments), stop.
|
|
199
|
-
endNode = node;
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
node = node[mathQuillInstance.R];
|
|
204
|
-
}
|
|
205
|
-
if (validCommands.includes(name)) {
|
|
206
|
-
return {name, startNode, endNode};
|
|
207
|
-
} else {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Return the start node, end node, and full name of the command to the left
|
|
217
|
-
* of `\\left(`, or `null` if there is no command.
|
|
218
|
-
*
|
|
219
|
-
* @param {node} leftParenNode - node where .ctrlSeq == `\\left(`
|
|
220
|
-
* @returns {null|object} - `null` or an object containing the start node
|
|
221
|
-
* (`startNode`), end node (`endNode`), and full
|
|
222
|
-
* name (`name`) of the command
|
|
223
|
-
*/
|
|
224
|
-
export function maybeFindCommandBeforeParens(leftParenNode) {
|
|
225
|
-
return maybeFindCommand(leftParenNode[mathQuillInstance.L]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function getCursorContext(
|
|
229
|
-
mathField?: MathFieldInterface,
|
|
230
|
-
): typeof CursorContext[keyof typeof CursorContext] {
|
|
231
|
-
if (!mathField) {
|
|
232
|
-
return CursorContext.NONE;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// First, try to find any fraction to the right, unimpeded.
|
|
236
|
-
const cursor = mathField.cursor();
|
|
237
|
-
let visitor = cursor;
|
|
238
|
-
while (visitor[mathQuillInstance.R] !== MathFieldActionType.MQ_END) {
|
|
239
|
-
if (isFraction(visitor[mathQuillInstance.R])) {
|
|
240
|
-
return CursorContext.BEFORE_FRACTION;
|
|
241
|
-
} else if (!isLeaf(visitor[mathQuillInstance.R])) {
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
visitor = visitor[mathQuillInstance.R];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// If that didn't work, check if the parent or grandparent is a special
|
|
248
|
-
// context, so that we can jump outwards.
|
|
249
|
-
if (isParens(cursor.parent && cursor.parent.parent)) {
|
|
250
|
-
return CursorContext.IN_PARENS;
|
|
251
|
-
} else if (isNumerator(cursor.parent)) {
|
|
252
|
-
return CursorContext.IN_NUMERATOR;
|
|
253
|
-
} else if (isDenominator(cursor.parent)) {
|
|
254
|
-
return CursorContext.IN_DENOMINATOR;
|
|
255
|
-
} else if (isSubScript(cursor.parent)) {
|
|
256
|
-
return CursorContext.IN_SUB_SCRIPT;
|
|
257
|
-
} else if (isSuperScript(cursor.parent)) {
|
|
258
|
-
return CursorContext.IN_SUPER_SCRIPT;
|
|
259
|
-
} else {
|
|
260
|
-
return CursorContext.NONE;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
2
|
-
import MathQuill from "mathquill";
|
|
3
|
-
|
|
4
|
-
import type {
|
|
5
|
-
MathQuillInterface,
|
|
6
|
-
MathFieldConfig,
|
|
7
|
-
MathFieldInterface,
|
|
8
|
-
} from "./mathquill-types";
|
|
9
|
-
|
|
10
|
-
// We only need one MathQuill instance (referred to as MQ in the docs)
|
|
11
|
-
// and that contains some MQ constants and the MathField constructor
|
|
12
|
-
export const mathQuillInstance: MathQuillInterface = MathQuill.getInterface(3);
|
|
13
|
-
|
|
14
|
-
const createBaseConfig = (): MathFieldConfig => ({
|
|
15
|
-
// LaTeX commands that, when typed, are immediately replaced by the
|
|
16
|
-
// appropriate symbol. This does not include ln, log, or any of the
|
|
17
|
-
// trig functions; those are always interpreted as commands.
|
|
18
|
-
autoCommands: "pi theta phi sqrt nthroot",
|
|
19
|
-
// Most of these autoOperatorNames are simply the MathQuill defaults.
|
|
20
|
-
// We have to list them all in order to add the `sen` operator (see
|
|
21
|
-
// comment below).
|
|
22
|
-
autoOperatorNames: [
|
|
23
|
-
"arccos",
|
|
24
|
-
"arcsin",
|
|
25
|
-
"arctan",
|
|
26
|
-
"arg",
|
|
27
|
-
"cos",
|
|
28
|
-
"cosh",
|
|
29
|
-
"cot",
|
|
30
|
-
"coth",
|
|
31
|
-
"csc",
|
|
32
|
-
"deg",
|
|
33
|
-
"det",
|
|
34
|
-
"dim",
|
|
35
|
-
"exp",
|
|
36
|
-
"gcd",
|
|
37
|
-
"hom",
|
|
38
|
-
"inf",
|
|
39
|
-
"ker",
|
|
40
|
-
"lg",
|
|
41
|
-
"lim",
|
|
42
|
-
"liminf",
|
|
43
|
-
"limsup",
|
|
44
|
-
"ln",
|
|
45
|
-
"log",
|
|
46
|
-
"max",
|
|
47
|
-
"min",
|
|
48
|
-
"Pr",
|
|
49
|
-
"projlim",
|
|
50
|
-
"sec",
|
|
51
|
-
// sen is used instead of sin in e.g. Portuguese
|
|
52
|
-
"sen",
|
|
53
|
-
"sin",
|
|
54
|
-
"sinh",
|
|
55
|
-
"sup",
|
|
56
|
-
"tan",
|
|
57
|
-
"tanh",
|
|
58
|
-
].join(" "),
|
|
59
|
-
|
|
60
|
-
// Pop the cursor out of super/subscripts on arithmetic operators
|
|
61
|
-
// or (in)equalities.
|
|
62
|
-
charsThatBreakOutOfSupSub: "+-*/=<>≠≤≥",
|
|
63
|
-
|
|
64
|
-
// Prevent excessive super/subscripts or fractions from being
|
|
65
|
-
// created without operands, e.g. when somebody holds down a key
|
|
66
|
-
supSubsRequireOperand: true,
|
|
67
|
-
|
|
68
|
-
// The name of this option is somewhat misleading, as tabbing in
|
|
69
|
-
// MathQuill breaks you out of a nested context (fraction/script)
|
|
70
|
-
// if you're in one, but moves focus to the next input if you're
|
|
71
|
-
// not. Spaces (with this option enabled) are just ignored in the
|
|
72
|
-
// latter case.
|
|
73
|
-
//
|
|
74
|
-
// TODO(alex): In order to allow inputting mixed numbers, we will
|
|
75
|
-
// have to accept spaces in certain cases. The desired behavior is
|
|
76
|
-
// still to escape nested contexts if currently in one, but to
|
|
77
|
-
// insert a space if not (we don't expect mixed numbers in nested
|
|
78
|
-
// contexts). We should also limit to one consecutive space.
|
|
79
|
-
spaceBehavesLikeTab: true,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Creates a new [MathField](http://docs.mathquill.com/en/latest/Api_Methods/#mqmathfieldhtml_element-config)
|
|
84
|
-
* instance within the given `container`.
|
|
85
|
-
*
|
|
86
|
-
* An optional configuration callback can be provided to customize
|
|
87
|
-
* the created MathField. A default configuration is passed to this
|
|
88
|
-
* callback which can then be adjusted as needed. The configuration
|
|
89
|
-
* returned from this callback is used to create the MathField.
|
|
90
|
-
* This allows callers to do minimal configuration as only configs
|
|
91
|
-
* that vary from the default need to be provided.
|
|
92
|
-
*/
|
|
93
|
-
export function createMathField(
|
|
94
|
-
container: HTMLDivElement | HTMLSpanElement,
|
|
95
|
-
configCallback?: (baseConfig: MathFieldConfig) => MathFieldConfig,
|
|
96
|
-
): MathFieldInterface {
|
|
97
|
-
const baseConfig = createBaseConfig();
|
|
98
|
-
const config = configCallback ? configCallback(baseConfig) : baseConfig;
|
|
99
|
-
|
|
100
|
-
const mathField = mathQuillInstance
|
|
101
|
-
.MathField(container, config)
|
|
102
|
-
// translated in ./math-input.tsx
|
|
103
|
-
.setAriaLabel(i18n._("Math input box")) as MathFieldInterface;
|
|
104
|
-
|
|
105
|
-
return mathField;
|
|
106
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type Key from "../../data/keys";
|
|
2
|
-
import type MathQuill from "mathquill";
|
|
3
|
-
|
|
4
|
-
export type MathQuillInterface = MathQuill.v3.API;
|
|
5
|
-
|
|
6
|
-
export type MathFieldConfig = MathQuill.v3.Config;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Editable math fields have all of the above methods in addition to
|
|
10
|
-
* the ones listed here.
|
|
11
|
-
* https://docs.mathquill.com/en/latest/Api_Methods/
|
|
12
|
-
*/
|
|
13
|
-
export type MathFieldInterface = MathQuill.v3.EditableMathQuill;
|
|
14
|
-
|
|
15
|
-
export enum MathFieldActionType {
|
|
16
|
-
WRITE = "write",
|
|
17
|
-
CMD = "cmd",
|
|
18
|
-
KEYSTROKE = "keystroke",
|
|
19
|
-
MQ_END = 0,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The MathQuill MathField Cursor
|
|
24
|
-
* it's not part of the public API for MathQuill,
|
|
25
|
-
* we reach into the internals to get it
|
|
26
|
-
*/
|
|
27
|
-
export type MathFieldCursor = any;
|
|
28
|
-
|
|
29
|
-
export type MathFieldUpdaterCallback = (
|
|
30
|
-
mathField: MathFieldInterface,
|
|
31
|
-
key: Key,
|
|
32
|
-
) => void;
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A single function used to scroll a DOM node into view, optionally taking into
|
|
3
|
-
* account that it may be obscured by the custom keypad. The logic makes the
|
|
4
|
-
* strong assumption that the keypad will be anchored to the bottom of the page
|
|
5
|
-
* in calculating its height, as this method may be called before the keypad has
|
|
6
|
-
* animated into view.
|
|
7
|
-
*
|
|
8
|
-
* TODO(charlie): Move this scroll logic out of our components and into a higher
|
|
9
|
-
* level in the component tree--perhaps even into webapp, beyond Perseus.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// HACK(charlie): This should be injected by webapp somehow.
|
|
13
|
-
// TODO(charlie): Add a link to the webapp location as soon as the footer
|
|
14
|
-
// has settled down.
|
|
15
|
-
export const toolbarHeightPx = 60;
|
|
16
|
-
|
|
17
|
-
export const scrollIntoView = (containerNode, keypadNode) => {
|
|
18
|
-
// TODO(charlie): There's no need for us to be reading the keypad bounds
|
|
19
|
-
// here, since they're pre-determined by logic in the store. We should
|
|
20
|
-
// instead pass around an object that knows the bounds.
|
|
21
|
-
const containerBounds = containerNode.getBoundingClientRect();
|
|
22
|
-
const containerBottomPx = containerBounds.bottom;
|
|
23
|
-
const containerTopPx = containerBounds.top;
|
|
24
|
-
|
|
25
|
-
// Get the element that scrolls the document.
|
|
26
|
-
const scrollNode = document.scrollingElement;
|
|
27
|
-
|
|
28
|
-
const desiredMarginPx = 16;
|
|
29
|
-
|
|
30
|
-
if (keypadNode) {
|
|
31
|
-
// NOTE(charlie): We can't use the bounding rect of the keypad,
|
|
32
|
-
// as it is likely in the process of animating in. Instead, to
|
|
33
|
-
// calculate its top, we make the strong assumption that the
|
|
34
|
-
// keypad will end up anchored at the bottom of the page, but above the
|
|
35
|
-
// toolbar, and use its height, which is known at this point. Note that,
|
|
36
|
-
// in the native apps (where the toolbar is rendered natively), this
|
|
37
|
-
// will result in us leaving excess space between the input and the
|
|
38
|
-
// keypad, but that seems okay.
|
|
39
|
-
const pageHeightPx = window.innerHeight;
|
|
40
|
-
const keypadHeightPx = keypadNode.clientHeight;
|
|
41
|
-
const keypadTopPx = pageHeightPx - (keypadHeightPx + toolbarHeightPx);
|
|
42
|
-
|
|
43
|
-
if (containerBottomPx > keypadTopPx) {
|
|
44
|
-
// If the input would be obscured by the keypad, scroll such that
|
|
45
|
-
// the bottom of the input is just above the top of the keypad,
|
|
46
|
-
// taking care not to scroll the input out of view.
|
|
47
|
-
const scrollOffset = Math.min(
|
|
48
|
-
containerBottomPx - keypadTopPx + desiredMarginPx,
|
|
49
|
-
containerTopPx,
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
if (scrollNode) {
|
|
53
|
-
scrollNode.scrollTop += scrollOffset;
|
|
54
|
-
}
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Alternatively, if the input is out of the viewport or nearly out
|
|
60
|
-
// of the viewport, scroll it into view. We can do this regardless
|
|
61
|
-
// of whether the keypad has been provided.
|
|
62
|
-
if (scrollNode && containerTopPx < desiredMarginPx) {
|
|
63
|
-
scrollNode.scrollTop -= containerBounds.height + desiredMarginPx;
|
|
64
|
-
}
|
|
65
|
-
};
|