@khanacademy/math-input 0.3.2 → 0.5.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/{build/math-input.css → dist/es/index.css} +0 -150
- package/dist/es/index.js +2 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.css +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.flow +2 -0
- package/dist/index.js.map +1 -0
- package/dist/strings.js +71 -0
- package/index.html +20 -0
- package/less/echo.less +56 -0
- package/less/main.less +5 -0
- package/less/overrides.less +129 -0
- package/less/popover.less +22 -0
- package/less/tabbar.less +6 -0
- package/package.json +38 -70
- package/src/actions/index.js +57 -0
- package/src/components/__tests__/gesture-state-machine_test.js +437 -0
- package/src/components/__tests__/node-manager_test.js +89 -0
- package/src/components/__tests__/two-page-keypad_test.js +42 -0
- package/src/components/app.js +73 -0
- package/src/components/common-style.js +47 -0
- package/src/components/compute-layout-parameters.js +157 -0
- package/src/components/corner-decal.js +56 -0
- package/src/components/echo-manager.js +160 -0
- package/src/components/empty-keypad-button.js +49 -0
- package/src/components/expression-keypad.js +323 -0
- package/src/components/fraction-keypad.js +176 -0
- package/src/components/gesture-manager.js +226 -0
- package/src/components/gesture-state-machine.js +283 -0
- package/src/components/icon.js +74 -0
- package/src/components/iconography/arrow.js +22 -0
- package/src/components/iconography/backspace.js +29 -0
- package/src/components/iconography/cdot.js +29 -0
- package/src/components/iconography/cos.js +30 -0
- package/src/components/iconography/cube-root.js +36 -0
- package/src/components/iconography/dismiss.js +25 -0
- package/src/components/iconography/divide.js +34 -0
- package/src/components/iconography/down.js +16 -0
- package/src/components/iconography/equal.js +33 -0
- package/src/components/iconography/exp-2.js +29 -0
- package/src/components/iconography/exp-3.js +29 -0
- package/src/components/iconography/exp.js +29 -0
- package/src/components/iconography/frac.js +44 -0
- package/src/components/iconography/geq.js +33 -0
- package/src/components/iconography/gt.js +33 -0
- package/src/components/iconography/index.js +45 -0
- package/src/components/iconography/jump-into-numerator.js +41 -0
- package/src/components/iconography/jump-out-base.js +30 -0
- package/src/components/iconography/jump-out-denominator.js +41 -0
- package/src/components/iconography/jump-out-exponent.js +30 -0
- package/src/components/iconography/jump-out-numerator.js +41 -0
- package/src/components/iconography/jump-out-parentheses.js +33 -0
- package/src/components/iconography/left-paren.js +33 -0
- package/src/components/iconography/left.js +16 -0
- package/src/components/iconography/leq.js +33 -0
- package/src/components/iconography/ln.js +29 -0
- package/src/components/iconography/log-n.js +29 -0
- package/src/components/iconography/log.js +29 -0
- package/src/components/iconography/lt.js +33 -0
- package/src/components/iconography/minus.js +32 -0
- package/src/components/iconography/neq.js +33 -0
- package/src/components/iconography/parens.js +33 -0
- package/src/components/iconography/percent.js +49 -0
- package/src/components/iconography/period.js +26 -0
- package/src/components/iconography/plus.js +32 -0
- package/src/components/iconography/radical.js +36 -0
- package/src/components/iconography/right-paren.js +33 -0
- package/src/components/iconography/right.js +16 -0
- package/src/components/iconography/sin.js +30 -0
- package/src/components/iconography/sqrt.js +32 -0
- package/src/components/iconography/tan.js +30 -0
- package/src/components/iconography/times.js +33 -0
- package/src/components/iconography/up.js +16 -0
- package/src/components/input/__tests__/context-tracking_test.js +177 -0
- package/src/components/input/__tests__/math-wrapper.jsx +33 -0
- package/src/components/input/__tests__/mathquill_test.js +747 -0
- package/src/components/input/cursor-contexts.js +29 -0
- package/src/components/input/cursor-handle.js +137 -0
- package/src/components/input/drag-listener.js +75 -0
- package/src/components/input/math-input.js +924 -0
- package/src/components/input/math-wrapper.js +959 -0
- package/src/components/input/scroll-into-view.js +72 -0
- package/src/components/keypad/button-assets.js +492 -0
- package/src/components/keypad/button.js +106 -0
- package/src/components/keypad/button.stories.js +27 -0
- package/src/components/keypad/index.js +64 -0
- package/src/components/keypad/keypad-page-items.js +106 -0
- package/src/components/keypad/keypad-pages.stories.js +32 -0
- package/src/components/keypad/keypad.stories.js +35 -0
- package/src/components/keypad/numeric-input-page.js +100 -0
- package/src/components/keypad/pre-algebra-page.js +98 -0
- package/src/components/keypad/trigonometry-page.js +90 -0
- package/src/components/keypad-button.js +366 -0
- package/src/components/keypad-container.js +303 -0
- package/src/components/keypad.js +154 -0
- package/src/components/many-keypad-button.js +44 -0
- package/src/components/math-icon.js +65 -0
- package/src/components/multi-symbol-grid.js +182 -0
- package/src/components/multi-symbol-popover.js +59 -0
- package/src/components/navigation-pad.js +139 -0
- package/src/components/node-manager.js +129 -0
- package/src/components/popover-manager.js +76 -0
- package/src/components/popover-state-machine.js +173 -0
- package/src/components/prop-types.js +82 -0
- package/src/components/provided-keypad.js +99 -0
- package/src/components/styles.js +38 -0
- package/src/components/svg-icon.js +25 -0
- package/src/components/tabbar/__tests__/tabbar_test.js +65 -0
- package/src/components/tabbar/icons.js +69 -0
- package/src/components/tabbar/item.js +138 -0
- package/src/components/tabbar/tabbar.js +61 -0
- package/src/components/tabbar/tabbar.stories.js +60 -0
- package/src/components/tabbar/types.js +3 -0
- package/src/components/text-icon.js +52 -0
- package/src/components/touchable-keypad-button.js +146 -0
- package/src/components/two-page-keypad.js +99 -0
- package/src/components/velocity-tracker.js +76 -0
- package/src/components/z-indexes.js +9 -0
- package/src/consts.js +74 -0
- package/src/data/key-configs.js +349 -0
- package/src/data/keys.js +72 -0
- package/src/demo.js +8 -0
- package/src/fake-react-native-web/index.js +12 -0
- package/src/fake-react-native-web/text.js +56 -0
- package/src/fake-react-native-web/view.js +91 -0
- package/src/index.js +13 -0
- package/src/native-app.js +84 -0
- package/src/store/index.js +505 -0
- package/src/utils.js +18 -0
- package/tools/svg-to-react/convert.py +111 -0
- package/tools/svg-to-react/icons/math-keypad-icon-0.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-1.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-2.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-3.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-4.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-5.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-6.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-7.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-8.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-9.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-addition.svg +34 -0
- package/tools/svg-to-react/icons/math-keypad-icon-cos.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-delete.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-dismiss.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-division.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-equals-not.svg +50 -0
- package/tools/svg-to-react/icons/math-keypad-icon-equals.svg +48 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent-2.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent-3.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-fraction.svg +42 -0
- package/tools/svg-to-react/icons/math-keypad-icon-greater-than.svg +46 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-base.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-denominator.svg +48 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-exponent.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-parentheses.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-less-than.svg +46 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log-10.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log-e.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-multiplication-cross.svg +40 -0
- package/tools/svg-to-react/icons/math-keypad-icon-multiplication-dot.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-percent.svg +42 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical-2.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical-3.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radix-character.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-sin.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-subtraction.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-tan.svg +38 -0
- package/tools/svg-to-react/symbol_map.py +41 -0
- package/LICENSE.txt +0 -21
- package/build/math-input.js +0 -1
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains a wrapper around MathQuill so that we can provide a
|
|
3
|
+
* more regular interface for the functionality we need while insulating us
|
|
4
|
+
* from MathQuill changes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import $ from "jquery";
|
|
8
|
+
import MathQuill from "mathquill";
|
|
9
|
+
|
|
10
|
+
import {DecimalSeparators} from "../../consts.js";
|
|
11
|
+
import Keys from "../../data/keys.js";
|
|
12
|
+
import {decimalSeparator} from "../../utils.js";
|
|
13
|
+
|
|
14
|
+
import * as CursorContexts from "./cursor-contexts.js";
|
|
15
|
+
|
|
16
|
+
// Keeping `window` in place for test suite and GitHub Pages.
|
|
17
|
+
// If it does not exist, fall back to CommonJS require. - jsatk
|
|
18
|
+
|
|
19
|
+
const decimalSymbol = decimalSeparator === DecimalSeparators.COMMA ? "," : ".";
|
|
20
|
+
|
|
21
|
+
const WRITE = "write";
|
|
22
|
+
const CMD = "cmd";
|
|
23
|
+
const KEYSTROKE = "keystroke";
|
|
24
|
+
const MQ_END = 0;
|
|
25
|
+
|
|
26
|
+
// A mapping from keys that can be pressed on a keypad to the way in which
|
|
27
|
+
// MathQuill should modify its input in response to that key-press. Any keys
|
|
28
|
+
// that do not provide explicit actions (like the numeral keys) will merely
|
|
29
|
+
// write their contents to MathQuill.
|
|
30
|
+
const KeyActions = {
|
|
31
|
+
[Keys.PLUS]: {str: "+", fn: WRITE},
|
|
32
|
+
[Keys.MINUS]: {str: "-", fn: WRITE},
|
|
33
|
+
[Keys.NEGATIVE]: {str: "-", fn: WRITE},
|
|
34
|
+
[Keys.TIMES]: {str: "\\times", fn: WRITE},
|
|
35
|
+
[Keys.DIVIDE]: {str: "\\div", fn: WRITE},
|
|
36
|
+
[Keys.DECIMAL]: {
|
|
37
|
+
str: decimalSymbol,
|
|
38
|
+
fn: WRITE,
|
|
39
|
+
},
|
|
40
|
+
[Keys.EQUAL]: {str: "=", fn: WRITE},
|
|
41
|
+
[Keys.NEQ]: {str: "\\neq", fn: WRITE},
|
|
42
|
+
[Keys.CDOT]: {str: "\\cdot", fn: WRITE},
|
|
43
|
+
[Keys.PERCENT]: {str: "%", fn: WRITE},
|
|
44
|
+
[Keys.LEFT_PAREN]: {str: "(", fn: CMD},
|
|
45
|
+
[Keys.RIGHT_PAREN]: {str: ")", fn: CMD},
|
|
46
|
+
[Keys.SQRT]: {str: "sqrt", fn: CMD},
|
|
47
|
+
[Keys.PI]: {str: "pi", fn: CMD},
|
|
48
|
+
[Keys.THETA]: {str: "theta", fn: CMD},
|
|
49
|
+
[Keys.RADICAL]: {str: "nthroot", fn: CMD},
|
|
50
|
+
[Keys.LT]: {str: "<", fn: WRITE},
|
|
51
|
+
[Keys.LEQ]: {str: "\\leq", fn: WRITE},
|
|
52
|
+
[Keys.GT]: {str: ">", fn: WRITE},
|
|
53
|
+
[Keys.GEQ]: {str: "\\geq", fn: WRITE},
|
|
54
|
+
[Keys.UP]: {str: "Up", fn: KEYSTROKE},
|
|
55
|
+
[Keys.DOWN]: {str: "Down", fn: KEYSTROKE},
|
|
56
|
+
// The `FRAC_EXCLUSIVE` variant is handled manually, since we may need to do
|
|
57
|
+
// some additional navigation depending on the cursor position.
|
|
58
|
+
[Keys.FRAC_INCLUSIVE]: {str: "/", fn: CMD},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const NormalCommands = {
|
|
62
|
+
[Keys.LOG]: "log",
|
|
63
|
+
[Keys.LN]: "ln",
|
|
64
|
+
[Keys.SIN]: "sin",
|
|
65
|
+
[Keys.COS]: "cos",
|
|
66
|
+
[Keys.TAN]: "tan",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const ArithmeticOperators = ["+", "-", "\\cdot", "\\times", "\\div"];
|
|
70
|
+
const EqualityOperators = ["=", "\\neq", "<", "\\leq", ">", "\\geq"];
|
|
71
|
+
|
|
72
|
+
const Numerals = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
73
|
+
const GreekLetters = ["\\theta", "\\pi"];
|
|
74
|
+
const Letters = [
|
|
75
|
+
"A",
|
|
76
|
+
"B",
|
|
77
|
+
"C",
|
|
78
|
+
"D",
|
|
79
|
+
"E",
|
|
80
|
+
"F",
|
|
81
|
+
"G",
|
|
82
|
+
"H",
|
|
83
|
+
"I",
|
|
84
|
+
"J",
|
|
85
|
+
"K",
|
|
86
|
+
"L",
|
|
87
|
+
"M",
|
|
88
|
+
"N",
|
|
89
|
+
"O",
|
|
90
|
+
"P",
|
|
91
|
+
"Q",
|
|
92
|
+
"R",
|
|
93
|
+
"S",
|
|
94
|
+
"T",
|
|
95
|
+
"U",
|
|
96
|
+
"V",
|
|
97
|
+
"W",
|
|
98
|
+
"X",
|
|
99
|
+
"Y",
|
|
100
|
+
"Z",
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// We only consider numerals, variables, and Greek Letters to be proper
|
|
104
|
+
// leaf nodes.
|
|
105
|
+
const ValidLeaves = [
|
|
106
|
+
...Numerals,
|
|
107
|
+
...GreekLetters,
|
|
108
|
+
...Letters.map((letter) => letter.toLowerCase()),
|
|
109
|
+
...Letters.map((letter) => letter.toUpperCase()),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const KeysForJumpContext = {
|
|
113
|
+
[CursorContexts.IN_PARENS]: Keys.JUMP_OUT_PARENTHESES,
|
|
114
|
+
[CursorContexts.IN_SUPER_SCRIPT]: Keys.JUMP_OUT_EXPONENT,
|
|
115
|
+
[CursorContexts.IN_SUB_SCRIPT]: Keys.JUMP_OUT_BASE,
|
|
116
|
+
[CursorContexts.BEFORE_FRACTION]: Keys.JUMP_INTO_NUMERATOR,
|
|
117
|
+
[CursorContexts.IN_NUMERATOR]: Keys.JUMP_OUT_NUMERATOR,
|
|
118
|
+
[CursorContexts.IN_DENOMINATOR]: Keys.JUMP_OUT_DENOMINATOR,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
class MathWrapper {
|
|
122
|
+
constructor(element, options = {}, callbacks = {}) {
|
|
123
|
+
this.MQ = MathQuill.getInterface(2);
|
|
124
|
+
this.mathField = this.MQ.MathField(element, {
|
|
125
|
+
// use a span instead of a textarea so that we don't bring up the
|
|
126
|
+
// native keyboard on mobile when selecting the input
|
|
127
|
+
substituteTextarea: function () {
|
|
128
|
+
return document.createElement("span");
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
this.callbacks = callbacks;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
focus() {
|
|
135
|
+
// HACK(charlie): We shouldn't reaching into MathQuill internals like
|
|
136
|
+
// this, but it's the easiest way to allow us to manage the focus state
|
|
137
|
+
// ourselves.
|
|
138
|
+
const controller = this.mathField.__controller;
|
|
139
|
+
controller.cursor.show();
|
|
140
|
+
|
|
141
|
+
// Set MathQuill's internal state to reflect the focus, otherwise it
|
|
142
|
+
// will consistently try to hide the cursor on key-press and introduce
|
|
143
|
+
// layout jank.
|
|
144
|
+
controller.blurred = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
blur() {
|
|
148
|
+
const controller = this.mathField.__controller;
|
|
149
|
+
controller.cursor.hide();
|
|
150
|
+
controller.blurred = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_writeNormalFunction(name) {
|
|
154
|
+
this.mathField.write(`\\${name}\\left(\\right)`);
|
|
155
|
+
this.mathField.keystroke("Left");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Handle a key press and return the resulting cursor state.
|
|
160
|
+
*
|
|
161
|
+
* @param {Key} key - an enum representing the key that was pressed
|
|
162
|
+
* @returns {object} a cursor object, consisting of a cursor context
|
|
163
|
+
*/
|
|
164
|
+
pressKey(key) {
|
|
165
|
+
const cursor = this.mathField.__controller.cursor;
|
|
166
|
+
|
|
167
|
+
if (key in KeyActions) {
|
|
168
|
+
const {str, fn} = KeyActions[key];
|
|
169
|
+
|
|
170
|
+
if (str && fn) {
|
|
171
|
+
this.mathField[fn](str);
|
|
172
|
+
}
|
|
173
|
+
} else if (Object.keys(NormalCommands).includes(key)) {
|
|
174
|
+
this._writeNormalFunction(NormalCommands[key]);
|
|
175
|
+
} else if (key === Keys.FRAC_EXCLUSIVE) {
|
|
176
|
+
// If there's nothing to the left of the cursor, then we want to
|
|
177
|
+
// leave the cursor to the left of the fraction after creating it.
|
|
178
|
+
const shouldNavigateLeft = cursor[this.MQ.L] === MQ_END;
|
|
179
|
+
this.mathField.cmd("\\frac");
|
|
180
|
+
if (shouldNavigateLeft) {
|
|
181
|
+
this.mathField.keystroke("Left");
|
|
182
|
+
}
|
|
183
|
+
} else if (key === Keys.FRAC) {
|
|
184
|
+
// eslint-disable-next-line no-unused-vars
|
|
185
|
+
const shouldNavigateLeft = cursor[this.MQ.L] === MQ_END;
|
|
186
|
+
this.mathField.cmd("\\frac");
|
|
187
|
+
} else if (key === Keys.LOG_N) {
|
|
188
|
+
this.mathField.write("log_{ }\\left(\\right)");
|
|
189
|
+
this.mathField.keystroke("Left"); // into parentheses
|
|
190
|
+
this.mathField.keystroke("Left"); // out of parentheses
|
|
191
|
+
this.mathField.keystroke("Left"); // into index
|
|
192
|
+
} else if (key === Keys.CUBE_ROOT) {
|
|
193
|
+
this.mathField.write("\\sqrt[3]{}");
|
|
194
|
+
this.mathField.keystroke("Left"); // under the root
|
|
195
|
+
} else if (
|
|
196
|
+
key === Keys.EXP ||
|
|
197
|
+
key === Keys.EXP_2 ||
|
|
198
|
+
key === Keys.EXP_3
|
|
199
|
+
) {
|
|
200
|
+
this._handleExponent(cursor, key);
|
|
201
|
+
} else if (
|
|
202
|
+
key === Keys.JUMP_OUT_PARENTHESES ||
|
|
203
|
+
key === Keys.JUMP_OUT_EXPONENT ||
|
|
204
|
+
key === Keys.JUMP_OUT_BASE ||
|
|
205
|
+
key === Keys.JUMP_INTO_NUMERATOR ||
|
|
206
|
+
key === Keys.JUMP_OUT_NUMERATOR ||
|
|
207
|
+
key === Keys.JUMP_OUT_DENOMINATOR
|
|
208
|
+
) {
|
|
209
|
+
this._handleJumpOut(cursor, key);
|
|
210
|
+
} else if (key === Keys.BACKSPACE) {
|
|
211
|
+
this._handleBackspace(cursor);
|
|
212
|
+
} else if (key === Keys.LEFT) {
|
|
213
|
+
this._handleLeftArrow(cursor);
|
|
214
|
+
} else if (key === Keys.RIGHT || key === Keys.JUMP_OUT) {
|
|
215
|
+
this._handleRightArrow(cursor);
|
|
216
|
+
} else if (/^[a-zA-Z]$/.test(key)) {
|
|
217
|
+
this.mathField[WRITE](key);
|
|
218
|
+
} else if (/^NUM_\d/.test(key)) {
|
|
219
|
+
this.mathField[WRITE](key[4]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!cursor.selection) {
|
|
223
|
+
// don't show the cursor for selections
|
|
224
|
+
cursor.show();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (this.callbacks.onSelectionChanged) {
|
|
228
|
+
this.callbacks.onSelectionChanged(cursor.selection);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// NOTE(charlie): It's insufficient to do this as an `edited` handler
|
|
232
|
+
// on the MathField, as that handler isn't triggered on navigation
|
|
233
|
+
// events.
|
|
234
|
+
return {
|
|
235
|
+
context: this.contextForCursor(cursor),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Place the cursor beside the node located at the given coordinates.
|
|
241
|
+
*
|
|
242
|
+
* @param {number} x - the x coordinate in the viewport
|
|
243
|
+
* @param {number} y - the y coordinate in the viewport
|
|
244
|
+
* @param {Node} hitNode - the node next to which the cursor should be
|
|
245
|
+
* placed; if provided, the coordinates will be used
|
|
246
|
+
* to determine on which side of the node the cursor
|
|
247
|
+
* should be placed
|
|
248
|
+
*/
|
|
249
|
+
setCursorPosition(x, y, hitNode) {
|
|
250
|
+
const el = hitNode || document.elementFromPoint(x, y);
|
|
251
|
+
|
|
252
|
+
if (el) {
|
|
253
|
+
const cursor = this.getCursor();
|
|
254
|
+
|
|
255
|
+
if (el.hasAttribute("mq-root-block")) {
|
|
256
|
+
// If we're in the empty area place the cursor at the right
|
|
257
|
+
// end of the expression.
|
|
258
|
+
cursor.insAtRightEnd(this.mathField.__controller.root);
|
|
259
|
+
} else {
|
|
260
|
+
// Otherwise place beside the element at x, y.
|
|
261
|
+
const controller = this.mathField.__controller;
|
|
262
|
+
|
|
263
|
+
const pageX = x - document.body.scrollLeft;
|
|
264
|
+
const pageY = y - document.body.scrollTop;
|
|
265
|
+
controller.seek($(el), pageX, pageY).cursor.startSelection();
|
|
266
|
+
|
|
267
|
+
// Unless that would leave us mid-command, in which case, we
|
|
268
|
+
// need to adjust and place the cursor inside the parens
|
|
269
|
+
// following the command.
|
|
270
|
+
const command = this._maybeFindCommand(cursor[this.MQ.L]);
|
|
271
|
+
if (command && command.endNode) {
|
|
272
|
+
// NOTE(charlie): endNode should definitely be \left(.
|
|
273
|
+
cursor.insLeftOf(command.endNode);
|
|
274
|
+
this.mathField.keystroke("Right");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (this.callbacks.onCursorMove) {
|
|
279
|
+
this.callbacks.onCursorMove({
|
|
280
|
+
context: this.contextForCursor(cursor),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getCursor() {
|
|
287
|
+
return this.mathField.__controller.cursor;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
getSelection() {
|
|
291
|
+
return this.getCursor().selection;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getContent() {
|
|
295
|
+
return this.mathField.latex();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setContent(latex) {
|
|
299
|
+
this.mathField.latex(latex);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
isEmpty() {
|
|
303
|
+
const cursor = this.getCursor();
|
|
304
|
+
return cursor.parent.id === 1 && cursor[1] === 0 && cursor[-1] === 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Notes about MathQuill
|
|
308
|
+
//
|
|
309
|
+
// MathQuill's stores its layout as nested linked lists. Each node in the
|
|
310
|
+
// list has this.MQ.L '-1' and this.MQ.R '1' properties that define links to
|
|
311
|
+
// the left and right nodes respectively. They also have
|
|
312
|
+
//
|
|
313
|
+
// ctrlSeq: contains the latex code snippet that defines that node.
|
|
314
|
+
// jQ: jQuery object for the DOM node(s) for this MathQuill node.
|
|
315
|
+
// ends: pointers to the nodes at the ends of the container.
|
|
316
|
+
// parent: parent node.
|
|
317
|
+
// blocks: an array containing one or more nodes that make up the node.
|
|
318
|
+
// sub?: subscript node if there is one as is the case in log_n
|
|
319
|
+
//
|
|
320
|
+
// All of the code below is super fragile. Please be especially careful
|
|
321
|
+
// when upgrading MathQuill.
|
|
322
|
+
|
|
323
|
+
_handleBackspaceInNthRoot(cursor) {
|
|
324
|
+
const isAtLeftEnd = cursor[this.MQ.L] === MQ_END;
|
|
325
|
+
|
|
326
|
+
const isRootEmpty = this._isInsideEmptyNode(
|
|
327
|
+
cursor.parent.parent.blocks[0].ends,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (isAtLeftEnd) {
|
|
331
|
+
this._selectNode(cursor.parent.parent, cursor);
|
|
332
|
+
|
|
333
|
+
if (isRootEmpty) {
|
|
334
|
+
this.mathField.keystroke("Backspace");
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
this.mathField.keystroke("Backspace");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Advances the cursor to the next logical position.
|
|
343
|
+
*
|
|
344
|
+
* @param {cursor} cursor
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
_handleJumpOut(cursor, key) {
|
|
348
|
+
const context = this.contextForCursor(cursor);
|
|
349
|
+
|
|
350
|
+
// Validate that the current cursor context matches the key's intent.
|
|
351
|
+
if (KeysForJumpContext[context] !== key) {
|
|
352
|
+
// If we don't have a valid cursor context, yet the user was able
|
|
353
|
+
// to trigger a jump-out key, that's a broken invariant. Rather
|
|
354
|
+
// than throw an error (which would kick the user out of the
|
|
355
|
+
// exercise), we do nothing, as a fallback strategy. The user can
|
|
356
|
+
// still move the cursor manually.
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
switch (context) {
|
|
361
|
+
case CursorContexts.IN_PARENS:
|
|
362
|
+
// Insert at the end of the parentheses, and then navigate right
|
|
363
|
+
// once more to get 'beyond' the parentheses.
|
|
364
|
+
cursor.insRightOf(cursor.parent.parent);
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case CursorContexts.BEFORE_FRACTION:
|
|
368
|
+
// Find the nearest fraction to the right of the cursor.
|
|
369
|
+
let fractionNode;
|
|
370
|
+
let visitor = cursor;
|
|
371
|
+
while (visitor[this.MQ.R] !== MQ_END) {
|
|
372
|
+
if (this._isFraction(visitor[this.MQ.R])) {
|
|
373
|
+
fractionNode = visitor[this.MQ.R];
|
|
374
|
+
}
|
|
375
|
+
visitor = visitor[this.MQ.R];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Jump into it!
|
|
379
|
+
cursor.insLeftOf(fractionNode);
|
|
380
|
+
this.mathField.keystroke("Right");
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
case CursorContexts.IN_NUMERATOR:
|
|
384
|
+
// HACK(charlie): I can't find a better way to do this. The goal
|
|
385
|
+
// is to place the cursor at the start of the matching
|
|
386
|
+
// denominator. So, we identify the appropriate node, and
|
|
387
|
+
// continue rightwards until we find ourselves inside of it.
|
|
388
|
+
// It's possible that there are cases in which we don't reach
|
|
389
|
+
// the denominator, though I can't think of any.
|
|
390
|
+
const siblingDenominator = cursor.parent.parent.blocks[1];
|
|
391
|
+
while (cursor.parent !== siblingDenominator) {
|
|
392
|
+
this.mathField.keystroke("Right");
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
case CursorContexts.IN_DENOMINATOR:
|
|
397
|
+
cursor.insRightOf(cursor.parent.parent);
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case CursorContexts.IN_SUB_SCRIPT:
|
|
401
|
+
// Insert just beyond the superscript.
|
|
402
|
+
cursor.insRightOf(cursor.parent.parent);
|
|
403
|
+
|
|
404
|
+
// Navigate right once more, if we're right before parens. This
|
|
405
|
+
// is to handle the standard case in which the subscript is the
|
|
406
|
+
// base of a custom log.
|
|
407
|
+
if (this._isParens(cursor[this.MQ.R])) {
|
|
408
|
+
this.mathField.keystroke("Right");
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case CursorContexts.IN_SUPER_SCRIPT:
|
|
413
|
+
// Insert just beyond the superscript.
|
|
414
|
+
cursor.insRightOf(cursor.parent.parent);
|
|
415
|
+
break;
|
|
416
|
+
|
|
417
|
+
default:
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Attempted to 'Jump Out' from node, but found no ` +
|
|
420
|
+
`appropriate cursor context: ${context}`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Selects and deletes part of the expression based on the cursor location.
|
|
427
|
+
* See inline comments for precise behavior of different cases.
|
|
428
|
+
*
|
|
429
|
+
* @param {cursor} cursor
|
|
430
|
+
* @private
|
|
431
|
+
*/
|
|
432
|
+
_handleBackspace(cursor) {
|
|
433
|
+
if (!cursor.selection) {
|
|
434
|
+
const parent = cursor.parent;
|
|
435
|
+
const grandparent = parent.parent;
|
|
436
|
+
const leftNode = cursor[this.MQ.L];
|
|
437
|
+
|
|
438
|
+
if (this._isFraction(leftNode)) {
|
|
439
|
+
this._selectNode(leftNode, cursor);
|
|
440
|
+
} else if (this._isSquareRoot(leftNode)) {
|
|
441
|
+
this._selectNode(leftNode, cursor);
|
|
442
|
+
} else if (this._isNthRoot(leftNode)) {
|
|
443
|
+
this._selectNode(leftNode, cursor);
|
|
444
|
+
} else if (this._isNthRootIndex(parent)) {
|
|
445
|
+
this._handleBackspaceInRootIndex(cursor);
|
|
446
|
+
} else if (leftNode.ctrlSeq === "\\left(") {
|
|
447
|
+
this._handleBackspaceOutsideParens(cursor);
|
|
448
|
+
} else if (grandparent.ctrlSeq === "\\left(") {
|
|
449
|
+
this._handleBackspaceInsideParens(cursor);
|
|
450
|
+
} else if (this._isInsideLogIndex(cursor)) {
|
|
451
|
+
this._handleBackspaceInLogIndex(cursor);
|
|
452
|
+
} else if (
|
|
453
|
+
leftNode.ctrlSeq === "\\ge " ||
|
|
454
|
+
leftNode.ctrlSeq === "\\le "
|
|
455
|
+
) {
|
|
456
|
+
this._handleBackspaceAfterLigaturedSymbol(cursor);
|
|
457
|
+
} else if (this._isNthRoot(grandparent) && leftNode === MQ_END) {
|
|
458
|
+
this._handleBackspaceInNthRoot(cursor);
|
|
459
|
+
} else {
|
|
460
|
+
this.mathField.keystroke("Backspace");
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
this.mathField.keystroke("Backspace");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_handleLeftArrow(cursor) {
|
|
468
|
+
// If we're inside a function, and just after the left parentheses, we
|
|
469
|
+
// need to skip the entire function name, rather than move the cursor
|
|
470
|
+
// inside of it. For example, when hitting left from within the
|
|
471
|
+
// parentheses in `cos()`, we want to place the cursor to the left of
|
|
472
|
+
// the entire expression, rather than between the `s` and the left
|
|
473
|
+
// parenthesis.
|
|
474
|
+
// From the cursor's perspective, this requires that our left node is
|
|
475
|
+
// the MQ_END node, that our grandparent is the left parenthesis, and
|
|
476
|
+
// the nodes to the left of our grandparent comprise a valid function
|
|
477
|
+
// name.
|
|
478
|
+
if (cursor[this.MQ.L] === MQ_END) {
|
|
479
|
+
const parent = cursor.parent;
|
|
480
|
+
const grandparent = parent.parent;
|
|
481
|
+
if (grandparent.ctrlSeq === "\\left(") {
|
|
482
|
+
const command = this._maybeFindCommandBeforeParens(grandparent);
|
|
483
|
+
if (command) {
|
|
484
|
+
cursor.insLeftOf(command.startNode);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Otherwise, we default to the standard MathQull left behavior.
|
|
491
|
+
this.mathField.keystroke("Left");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
_handleRightArrow(cursor) {
|
|
495
|
+
const command = this._maybeFindCommand(cursor[this.MQ.R]);
|
|
496
|
+
if (command) {
|
|
497
|
+
// Similarly, if a function is to our right, then we need to place
|
|
498
|
+
// the cursor at the start of its parenthetical content, which is
|
|
499
|
+
// done by putting it to the left of ites parentheses and then
|
|
500
|
+
// moving right once.
|
|
501
|
+
cursor.insLeftOf(command.endNode);
|
|
502
|
+
this.mathField.keystroke("Right");
|
|
503
|
+
} else {
|
|
504
|
+
// Otherwise, we default to the standard MathQull right behavior.
|
|
505
|
+
this.mathField.keystroke("Right");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_handleExponent(cursor, key) {
|
|
510
|
+
// If there's an invalid operator preceding the cursor (anything that
|
|
511
|
+
// knowingly cannot be raised to a power), add an empty set of
|
|
512
|
+
// parentheses and apply the exponent to that.
|
|
513
|
+
const invalidPrefixes = [...ArithmeticOperators, ...EqualityOperators];
|
|
514
|
+
|
|
515
|
+
const precedingNode = cursor[this.MQ.L];
|
|
516
|
+
const shouldPrefixWithParens =
|
|
517
|
+
precedingNode === MQ_END ||
|
|
518
|
+
invalidPrefixes.includes(precedingNode.ctrlSeq.trim());
|
|
519
|
+
if (shouldPrefixWithParens) {
|
|
520
|
+
this.mathField.write("\\left(\\right)");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Insert the appropriate exponent operator.
|
|
524
|
+
switch (key) {
|
|
525
|
+
case Keys.EXP:
|
|
526
|
+
this.mathField.cmd("^");
|
|
527
|
+
break;
|
|
528
|
+
|
|
529
|
+
case Keys.EXP_2:
|
|
530
|
+
case Keys.EXP_3:
|
|
531
|
+
this.mathField.write(`^${key === Keys.EXP_2 ? 2 : 3}`);
|
|
532
|
+
|
|
533
|
+
// If we enter a square or a cube, we should leave the cursor
|
|
534
|
+
// within the newly inserted parens, if they exist. This takes
|
|
535
|
+
// exactly four left strokes, since the cursor by default would
|
|
536
|
+
// end up to the right of the exponent.
|
|
537
|
+
if (shouldPrefixWithParens) {
|
|
538
|
+
this.mathField.keystroke("Left");
|
|
539
|
+
this.mathField.keystroke("Left");
|
|
540
|
+
this.mathField.keystroke("Left");
|
|
541
|
+
this.mathField.keystroke("Left");
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
|
|
545
|
+
default:
|
|
546
|
+
throw new Error(`Invalid exponent key: ${key}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Return the start node, end node, and full name of the command of which
|
|
552
|
+
* the initial node is a part, or `null` if the node is not part of a
|
|
553
|
+
* command.
|
|
554
|
+
*
|
|
555
|
+
* @param {node} initialNode - the node to included as part of the command
|
|
556
|
+
* @returns {null|object} - `null` or an object containing the start node
|
|
557
|
+
* (`startNode`), end node (`endNode`), and full
|
|
558
|
+
* name (`name`) of the command
|
|
559
|
+
* @private
|
|
560
|
+
*/
|
|
561
|
+
_maybeFindCommand(initialNode) {
|
|
562
|
+
if (!initialNode) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// MathQuill stores commands as separate characters so that
|
|
567
|
+
// users can delete commands one character at a time. We iterate over
|
|
568
|
+
// the nodes from right to left until we hit a sequence starting with a
|
|
569
|
+
// '\\', which signifies the start of a command; then we iterate from
|
|
570
|
+
// left to right until we hit a '\\left(', which signifies the end of a
|
|
571
|
+
// command. If we encounter any character that doesn't belong in a
|
|
572
|
+
// command, we return null. We match a single character at a time.
|
|
573
|
+
// Ex) ['\\l', 'o', 'g ', '\\left(', ...]
|
|
574
|
+
const commandCharRegex = /^[a-z]$/;
|
|
575
|
+
const commandStartRegex = /^\\[a-z]$/;
|
|
576
|
+
const commandEndSeq = "\\left(";
|
|
577
|
+
|
|
578
|
+
// Note: We whitelist the set of valid commands, since relying solely on
|
|
579
|
+
// a command being prefixed with a backslash leads to undesired
|
|
580
|
+
// behavior. For example, Greek symbols, left parentheses, and square
|
|
581
|
+
// roots all get treated as commands.
|
|
582
|
+
const validCommands = ["\\log", "\\ln", "\\cos", "\\sin", "\\tan"];
|
|
583
|
+
|
|
584
|
+
let name = "";
|
|
585
|
+
let startNode;
|
|
586
|
+
let endNode;
|
|
587
|
+
|
|
588
|
+
// Collect the portion of the command from the current node, leftwards
|
|
589
|
+
// until the start of the command.
|
|
590
|
+
let node = initialNode;
|
|
591
|
+
while (node !== 0) {
|
|
592
|
+
const ctrlSeq = node.ctrlSeq.trim();
|
|
593
|
+
if (commandCharRegex.test(ctrlSeq)) {
|
|
594
|
+
name = ctrlSeq + name;
|
|
595
|
+
} else if (commandStartRegex.test(ctrlSeq)) {
|
|
596
|
+
name = ctrlSeq + name;
|
|
597
|
+
startNode = node;
|
|
598
|
+
break;
|
|
599
|
+
} else {
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
node = node[this.MQ.L];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// If we hit the start of a command, then grab the rest of it by
|
|
607
|
+
// iterating rightwards to compute the full name of the command, along
|
|
608
|
+
// with its terminal node.
|
|
609
|
+
if (startNode) {
|
|
610
|
+
// Next, iterate from the start to the right.
|
|
611
|
+
node = initialNode[this.MQ.R];
|
|
612
|
+
while (node !== 0) {
|
|
613
|
+
const ctrlSeq = node.ctrlSeq.trim();
|
|
614
|
+
if (commandCharRegex.test(ctrlSeq)) {
|
|
615
|
+
// If we have a single character, add it to the command
|
|
616
|
+
// name.
|
|
617
|
+
name = name + ctrlSeq;
|
|
618
|
+
} else if (ctrlSeq === commandEndSeq) {
|
|
619
|
+
// If we hit the command end delimiter (the left
|
|
620
|
+
// parentheses surrounding its arguments), stop.
|
|
621
|
+
endNode = node;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
node = node[this.MQ.R];
|
|
626
|
+
}
|
|
627
|
+
if (validCommands.includes(name)) {
|
|
628
|
+
return {name, startNode, endNode};
|
|
629
|
+
} else {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Return the start node, end node, and full name of the command to the left
|
|
639
|
+
* of `\\left(`, or `null` if there is no command.
|
|
640
|
+
*
|
|
641
|
+
* @param {node} leftParenNode - node where .ctrlSeq == `\\left(`
|
|
642
|
+
* @returns {null|object} - `null` or an object containing the start node
|
|
643
|
+
* (`startNode`), end node (`endNode`), and full
|
|
644
|
+
* name (`name`) of the command
|
|
645
|
+
* @private
|
|
646
|
+
*/
|
|
647
|
+
_maybeFindCommandBeforeParens(leftParenNode) {
|
|
648
|
+
return this._maybeFindCommand(leftParenNode[this.MQ.L]);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
_selectNode(node, cursor) {
|
|
652
|
+
cursor.insLeftOf(node);
|
|
653
|
+
cursor.startSelection();
|
|
654
|
+
cursor.insRightOf(node);
|
|
655
|
+
cursor.select();
|
|
656
|
+
cursor.endSelection();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_isFraction(node) {
|
|
660
|
+
return node.jQ && node.jQ.hasClass("mq-fraction");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
_isNumerator(node) {
|
|
664
|
+
return node.jQ && node.jQ.hasClass("mq-numerator");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
_isDenominator(node) {
|
|
668
|
+
return node.jQ && node.jQ.hasClass("mq-denominator");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
_isSubScript(node) {
|
|
672
|
+
// NOTE(charlie): MyScript has a structure whereby its superscripts seem
|
|
673
|
+
// to be represented as a parent node with 'mq-sup-only' containing a
|
|
674
|
+
// single child with 'mq-sup'.
|
|
675
|
+
return (
|
|
676
|
+
node.jQ &&
|
|
677
|
+
(node.jQ.hasClass("mq-sub-only") || node.jQ.hasClass("mq-sub"))
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
_isSuperScript(node) {
|
|
682
|
+
// NOTE(charlie): MyScript has a structure whereby its superscripts seem
|
|
683
|
+
// to be represented as a parent node with 'mq-sup-only' containing a
|
|
684
|
+
// single child with 'mq-sup'.
|
|
685
|
+
return (
|
|
686
|
+
node.jQ &&
|
|
687
|
+
(node.jQ.hasClass("mq-sup-only") || node.jQ.hasClass("mq-sup"))
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
_isParens(node) {
|
|
692
|
+
return node && node.ctrlSeq === "\\left(";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_isLeaf(node) {
|
|
696
|
+
return (
|
|
697
|
+
node && node.ctrlSeq && ValidLeaves.includes(node.ctrlSeq.trim())
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
_isSquareRoot(node) {
|
|
702
|
+
return (
|
|
703
|
+
node.blocks &&
|
|
704
|
+
node.blocks[0].jQ &&
|
|
705
|
+
node.blocks[0].jQ.hasClass("mq-sqrt-stem")
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
_isNthRoot(node) {
|
|
710
|
+
return (
|
|
711
|
+
node.blocks &&
|
|
712
|
+
node.blocks[0].jQ &&
|
|
713
|
+
node.blocks[0].jQ.hasClass("mq-nthroot")
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
_isNthRootIndex(node) {
|
|
718
|
+
return node.jQ && node.jQ.hasClass("mq-nthroot");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
_isInsideLogIndex(cursor) {
|
|
722
|
+
const grandparent = cursor.parent.parent;
|
|
723
|
+
|
|
724
|
+
if (grandparent && grandparent.jQ.hasClass("mq-supsub")) {
|
|
725
|
+
const command = this._maybeFindCommandBeforeParens(grandparent);
|
|
726
|
+
|
|
727
|
+
if (command && command.name === "\\log") {
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
_isInsideEmptyNode(cursor) {
|
|
736
|
+
return cursor[this.MQ.L] === MQ_END && cursor[this.MQ.R] === MQ_END;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
_handleBackspaceInRootIndex(cursor) {
|
|
740
|
+
if (this._isInsideEmptyNode(cursor)) {
|
|
741
|
+
// When deleting the index in a nthroot, we change from the nthroot
|
|
742
|
+
// to a sqrt, e.g. \sqrt[|]{35x-5} => |\sqrt{35x-5}. If there's no
|
|
743
|
+
// content under the root, then we delete the whole thing.
|
|
744
|
+
|
|
745
|
+
const grandparent = cursor.parent.parent;
|
|
746
|
+
const latex = grandparent.latex();
|
|
747
|
+
const reinsertionPoint = grandparent[this.MQ.L];
|
|
748
|
+
|
|
749
|
+
this._selectNode(grandparent, cursor);
|
|
750
|
+
|
|
751
|
+
const rootIsEmpty = grandparent.blocks[1].jQ.text() === "";
|
|
752
|
+
|
|
753
|
+
if (rootIsEmpty) {
|
|
754
|
+
// If there is not content under the root then simply delete
|
|
755
|
+
// the whole thing.
|
|
756
|
+
this.mathField.keystroke("Backspace");
|
|
757
|
+
} else {
|
|
758
|
+
// Replace the nthroot with a sqrt if there was content under
|
|
759
|
+
// the root.
|
|
760
|
+
|
|
761
|
+
// Start by deleting the selection.
|
|
762
|
+
this.mathField.keystroke("Backspace");
|
|
763
|
+
|
|
764
|
+
// Replace the nth-root with a sqrt.
|
|
765
|
+
this.mathField.write(latex.replace(/^\\sqrt\[\]/, "\\sqrt"));
|
|
766
|
+
|
|
767
|
+
// Adjust the cursor to be to the left the sqrt.
|
|
768
|
+
if (reinsertionPoint === MQ_END) {
|
|
769
|
+
this.mathField.moveToDirEnd(this.MQ.L);
|
|
770
|
+
} else {
|
|
771
|
+
cursor.insRightOf(reinsertionPoint);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
if (cursor[this.MQ.L] !== MQ_END) {
|
|
776
|
+
// If the cursor is not at the leftmost position inside the
|
|
777
|
+
// root's index, delete a character.
|
|
778
|
+
this.mathField.keystroke("Backspace");
|
|
779
|
+
} else {
|
|
780
|
+
// TODO(kevinb) verify that we want this behavior after testing
|
|
781
|
+
// Do nothing because we haven't completely deleted the
|
|
782
|
+
// index of the radical.
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
_handleBackspaceInLogIndex(cursor) {
|
|
788
|
+
if (this._isInsideEmptyNode(cursor)) {
|
|
789
|
+
const grandparent = cursor.parent.parent;
|
|
790
|
+
const command = this._maybeFindCommandBeforeParens(grandparent);
|
|
791
|
+
|
|
792
|
+
cursor.insLeftOf(command.startNode);
|
|
793
|
+
cursor.startSelection();
|
|
794
|
+
|
|
795
|
+
if (grandparent[this.MQ.R] !== MQ_END) {
|
|
796
|
+
cursor.insRightOf(grandparent[this.MQ.R]);
|
|
797
|
+
} else {
|
|
798
|
+
cursor.insRightOf(grandparent);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
cursor.select();
|
|
802
|
+
cursor.endSelection();
|
|
803
|
+
|
|
804
|
+
const isLogBodyEmpty =
|
|
805
|
+
grandparent[this.MQ.R].contentjQ.text() === "";
|
|
806
|
+
|
|
807
|
+
if (isLogBodyEmpty) {
|
|
808
|
+
// If there's no content inside the log's parens then delete the
|
|
809
|
+
// whole thing.
|
|
810
|
+
this.mathField.keystroke("Backspace");
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
this.mathField.keystroke("Backspace");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
_handleBackspaceOutsideParens(cursor) {
|
|
818
|
+
// In this case the node with '\\left(' for its ctrlSeq
|
|
819
|
+
// is the parent of the expression contained within the
|
|
820
|
+
// parentheses.
|
|
821
|
+
//
|
|
822
|
+
// Handle selecting an expression before deleting:
|
|
823
|
+
// (x+1)| => |(x+1)|
|
|
824
|
+
// \log(x+1)| => |\log(x+1)|
|
|
825
|
+
|
|
826
|
+
const leftNode = cursor[this.MQ.L];
|
|
827
|
+
const rightNode = cursor[this.MQ.R];
|
|
828
|
+
const command = this._maybeFindCommandBeforeParens(leftNode);
|
|
829
|
+
|
|
830
|
+
if (command && command.startNode) {
|
|
831
|
+
// There's a command before the parens so we select it as well as
|
|
832
|
+
// the parens.
|
|
833
|
+
cursor.insLeftOf(command.startNode);
|
|
834
|
+
cursor.startSelection();
|
|
835
|
+
if (rightNode === MQ_END) {
|
|
836
|
+
cursor.insAtRightEnd(cursor.parent);
|
|
837
|
+
} else {
|
|
838
|
+
cursor.insLeftOf(rightNode);
|
|
839
|
+
}
|
|
840
|
+
cursor.select();
|
|
841
|
+
cursor.endSelection();
|
|
842
|
+
} else {
|
|
843
|
+
cursor.startSelection();
|
|
844
|
+
cursor.insLeftOf(leftNode); // left of \\left(
|
|
845
|
+
cursor.select();
|
|
846
|
+
cursor.endSelection();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
_handleBackspaceInsideParens(cursor) {
|
|
851
|
+
// Handle situations when the cursor is inside parens or a
|
|
852
|
+
// command that uses parens, e.g. \log() or \tan()
|
|
853
|
+
//
|
|
854
|
+
// MathQuill represents log(x+1) in roughly the following way
|
|
855
|
+
// [l, o, g, \\left[parent:[x, +, 1]]]
|
|
856
|
+
//
|
|
857
|
+
// If the cursor is inside the parentheses it's next to one of:
|
|
858
|
+
// x, +, or 1. This makes sub_sub_expr its parent and sub_expr
|
|
859
|
+
// it's parent.
|
|
860
|
+
//
|
|
861
|
+
// Interestingly parent doesn't have any nodes to the left or
|
|
862
|
+
// right of it (even though the corresponding DOM node has
|
|
863
|
+
// ( and ) characters on either side.
|
|
864
|
+
//
|
|
865
|
+
// The grandparent's ctrlSeq is `\\left(`. The `\\right)` isn't
|
|
866
|
+
// stored anywhere. NOTE(kevinb): I believe this is because
|
|
867
|
+
// MathQuill knows what the close paren should be and does the
|
|
868
|
+
// right thing at render time.
|
|
869
|
+
//
|
|
870
|
+
// This conditional branch handles the following cases:
|
|
871
|
+
// - \log(x+1|) => \log(x+|)
|
|
872
|
+
// - \log(|x+1) => |\log(x+1)|
|
|
873
|
+
// - \log(|) => |
|
|
874
|
+
|
|
875
|
+
if (cursor[this.MQ.L] !== MQ_END) {
|
|
876
|
+
// This command contains math and there's some math to
|
|
877
|
+
// the left of the cursor that we should delete normally
|
|
878
|
+
// before doing anything special.
|
|
879
|
+
this.mathField.keystroke("Backspace");
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const grandparent = cursor.parent.parent;
|
|
884
|
+
|
|
885
|
+
// If the cursors is inside the parens at the start but the command
|
|
886
|
+
// has a subscript as is the case in log_n then move the cursor into
|
|
887
|
+
// the subscript, e.g. \log_{5}(|x+1) => \log_{5|}(x+1)
|
|
888
|
+
|
|
889
|
+
if (grandparent[this.MQ.L].sub) {
|
|
890
|
+
// if there is a subscript
|
|
891
|
+
if (grandparent[this.MQ.L].sub.jQ.text()) {
|
|
892
|
+
// and it contains text
|
|
893
|
+
// move the cursor to the right end of the subscript
|
|
894
|
+
cursor.insAtRightEnd(grandparent[this.MQ.L].sub);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Determine if the parens are empty before we modify the
|
|
900
|
+
// cursor's position.
|
|
901
|
+
const isEmpty = this._isInsideEmptyNode(cursor);
|
|
902
|
+
|
|
903
|
+
// Insert the cursor to the left of the command if there is one
|
|
904
|
+
// or before the '\\left(` if there isn't
|
|
905
|
+
const command = this._maybeFindCommandBeforeParens(grandparent);
|
|
906
|
+
|
|
907
|
+
cursor.insLeftOf((command && command.startNode) || grandparent);
|
|
908
|
+
cursor.startSelection();
|
|
909
|
+
cursor.insRightOf(grandparent);
|
|
910
|
+
cursor.select();
|
|
911
|
+
cursor.endSelection();
|
|
912
|
+
|
|
913
|
+
// Delete the selection, but only if the parens were empty to
|
|
914
|
+
// begin with.
|
|
915
|
+
if (isEmpty) {
|
|
916
|
+
this.mathField.keystroke("Backspace");
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
_handleBackspaceAfterLigaturedSymbol(cursor) {
|
|
921
|
+
this.mathField.keystroke("Backspace");
|
|
922
|
+
this.mathField.keystroke("Backspace");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
contextForCursor(cursor) {
|
|
926
|
+
// First, try to find any fraction to the right, unimpeded.
|
|
927
|
+
let visitor = cursor;
|
|
928
|
+
while (visitor[this.MQ.R] !== MQ_END) {
|
|
929
|
+
if (this._isFraction(visitor[this.MQ.R])) {
|
|
930
|
+
return CursorContexts.BEFORE_FRACTION;
|
|
931
|
+
} else if (!this._isLeaf(visitor[this.MQ.R])) {
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
visitor = visitor[this.MQ.R];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// If that didn't work, check if the parent or grandparent is a special
|
|
938
|
+
// context, so that we can jump outwards.
|
|
939
|
+
if (this._isParens(cursor.parent && cursor.parent.parent)) {
|
|
940
|
+
return CursorContexts.IN_PARENS;
|
|
941
|
+
} else if (this._isNumerator(cursor.parent)) {
|
|
942
|
+
return CursorContexts.IN_NUMERATOR;
|
|
943
|
+
} else if (this._isDenominator(cursor.parent)) {
|
|
944
|
+
return CursorContexts.IN_DENOMINATOR;
|
|
945
|
+
} else if (this._isSubScript(cursor.parent)) {
|
|
946
|
+
return CursorContexts.IN_SUB_SCRIPT;
|
|
947
|
+
} else if (this._isSuperScript(cursor.parent)) {
|
|
948
|
+
return CursorContexts.IN_SUPER_SCRIPT;
|
|
949
|
+
} else {
|
|
950
|
+
return CursorContexts.NONE;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
_isAtTopLevel(cursor) {
|
|
955
|
+
return !cursor.parent.parent;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export default MathWrapper;
|