@khanacademy/math-input 0.6.6 → 0.6.8

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/index.js CHANGED
@@ -4,12 +4,13 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var Color = require('@khanacademy/wonder-blocks-color');
6
6
  var i18n = require('@khanacademy/wonder-blocks-i18n');
7
+ var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
7
8
  var aphrodite = require('aphrodite');
8
- var PropTypes = require('prop-types');
9
9
  var React = require('react');
10
10
  var ReactDOM = require('react-dom');
11
11
  var $ = require('jquery');
12
12
  var MathQuill = require('mathquill');
13
+ var PropTypes = require('prop-types');
13
14
  var reactRedux = require('react-redux');
14
15
  var Redux = require('redux');
15
16
  var katex = require('katex');
@@ -40,11 +41,11 @@ function _interopNamespace(e) {
40
41
 
41
42
  var Color__default = /*#__PURE__*/_interopDefaultLegacy(Color);
42
43
  var i18n__namespace = /*#__PURE__*/_interopNamespace(i18n);
43
- var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
44
44
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
45
45
  var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM);
46
46
  var $__default = /*#__PURE__*/_interopDefaultLegacy($);
47
47
  var MathQuill__default = /*#__PURE__*/_interopDefaultLegacy(MathQuill);
48
+ var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
48
49
  var Redux__namespace = /*#__PURE__*/_interopNamespace(Redux);
49
50
  var katex__default = /*#__PURE__*/_interopDefaultLegacy(katex);
50
51
  var Clickable__default = /*#__PURE__*/_interopDefaultLegacy(Clickable);
@@ -270,6 +271,176 @@ const navigationPadWidthPx = 192; // HACK(charlie): This should be injected by w
270
271
 
271
272
  const toolbarHeightPx = 60;
272
273
 
274
+ const touchTargetRadiusPx = 2 * cursorHandleRadiusPx;
275
+ const touchTargetHeightPx = 2 * touchTargetRadiusPx;
276
+ const touchTargetWidthPx = 2 * touchTargetRadiusPx;
277
+ const cursorRadiusPx = cursorHandleRadiusPx;
278
+ const cursorHeightPx = cursorHandleDistanceMultiplier * (cursorRadiusPx * 4);
279
+ const cursorWidthPx = 4 * cursorRadiusPx;
280
+
281
+ class CursorHandle extends React__namespace.Component {
282
+ render() {
283
+ const {
284
+ x,
285
+ y,
286
+ animateIntoPosition
287
+ } = this.props;
288
+ const animationStyle = animateIntoPosition ? {
289
+ transitionDuration: "100ms",
290
+ transitionProperty: "transform"
291
+ } : {};
292
+ const transformString = "translate(".concat(x, "px, ").concat(y, "px)");
293
+ const outerStyle = {
294
+ position: "absolute",
295
+ // This is essentially webapp's interactiveComponent + 1.
296
+ // TODO(charlie): Pull in those styles somehow to avoid breakages.
297
+ zIndex: 4,
298
+ left: -touchTargetWidthPx / 2,
299
+ top: 0,
300
+ transform: transformString,
301
+ width: touchTargetWidthPx,
302
+ height: touchTargetHeightPx,
303
+ // Touch events that start on the cursor shouldn't be allowed to
304
+ // produce page scrolls.
305
+ touchAction: "none",
306
+ ...animationStyle
307
+ };
308
+ return /*#__PURE__*/React__namespace.createElement("span", {
309
+ style: outerStyle,
310
+ onTouchStart: this.props.onTouchStart,
311
+ onTouchMove: this.props.onTouchMove,
312
+ onTouchEnd: this.props.onTouchEnd,
313
+ onTouchCancel: this.props.onTouchCancel
314
+ }, /*#__PURE__*/React__namespace.createElement("svg", {
315
+ fill: "none",
316
+ width: cursorWidthPx,
317
+ height: cursorHeightPx,
318
+ viewBox: "0 0 ".concat(cursorWidthPx, " ").concat(cursorHeightPx)
319
+ }, /*#__PURE__*/React__namespace.createElement("filter", {
320
+ id: "math-input_cursor",
321
+ colorInterpolationFilters: "sRGB",
322
+ filterUnits: "userSpaceOnUse",
323
+ height: cursorHeightPx * 0.87 // ~40
324
+ ,
325
+ width: cursorWidthPx * 0.82 // ~36
326
+ ,
327
+ x: "4",
328
+ y: "0"
329
+ }, /*#__PURE__*/React__namespace.createElement("feFlood", {
330
+ floodOpacity: "0",
331
+ result: "BackgroundImageFix"
332
+ }), /*#__PURE__*/React__namespace.createElement("feColorMatrix", {
333
+ in: "SourceAlpha",
334
+ type: "matrix",
335
+ values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
336
+ }), /*#__PURE__*/React__namespace.createElement("feOffset", {
337
+ dy: "4"
338
+ }), /*#__PURE__*/React__namespace.createElement("feGaussianBlur", {
339
+ stdDeviation: "4"
340
+ }), /*#__PURE__*/React__namespace.createElement("feColorMatrix", {
341
+ type: "matrix",
342
+ values: "0 0 0 0 0.129412 0 0 0 0 0.141176 0 0 0 0 0.172549 0 0 0 0.08 0"
343
+ }), /*#__PURE__*/React__namespace.createElement("feBlend", {
344
+ in2: "BackgroundImageFix",
345
+ mode: "normal",
346
+ result: "effect1_dropShadow"
347
+ }), /*#__PURE__*/React__namespace.createElement("feBlend", {
348
+ in: "SourceGraphic",
349
+ in2: "effect1_dropShadow",
350
+ mode: "normal",
351
+ result: "shape"
352
+ })), /*#__PURE__*/React__namespace.createElement("g", {
353
+ filter: "url(#math-input_cursor)"
354
+ }, /*#__PURE__*/React__namespace.createElement("path", {
355
+ d: "m22 4-7.07 7.0284c-1.3988 1.3901-2.3515 3.1615-2.7376 5.09-.3861 1.9284-.1883 3.9274.5685 5.7441s2.0385 3.3694 3.6831 4.4619c1.6445 1.0925 3.5781 1.6756 5.556 1.6756s3.9115-.5831 5.556-1.6756c1.6446-1.0925 2.9263-2.6452 3.6831-4.4619s.9546-3.8157.5685-5.7441c-.3861-1.9285-1.3388-3.6999-2.7376-5.09z",
356
+ fill: "#1865f2"
357
+ })), /*#__PURE__*/React__namespace.createElement("path", {
358
+ d: "m14.9301 10.4841 7.0699-7.06989 7.0699 7.06989.0001.0001c1.3988 1.3984 2.3515 3.1802 2.7376 5.1201s.1883 3.9507-.5685 5.7782c-.7568 1.8274-2.0385 3.3894-3.6831 4.4883-1.6445 1.099-3.5781 1.6855-5.556 1.6855s-3.9115-.5865-5.556-1.6855c-1.6446-1.0989-2.9263-2.6609-3.6831-4.4883-.7568-1.8275-.9546-3.8383-.5685-5.7782s1.3388-3.7217 2.7376-5.1201z",
359
+ stroke: "#fff",
360
+ strokeWidth: "2"
361
+ })));
362
+ }
363
+
364
+ }
365
+
366
+ _defineProperty(CursorHandle, "defaultProps", {
367
+ animateIntoPosition: false,
368
+ visible: false,
369
+ x: 0,
370
+ y: 0
371
+ });
372
+
373
+ /**
374
+ * A gesture recognizer that detects 'drags', crudely defined as either scrolls
375
+ * or touches that move a sufficient distance.
376
+ */
377
+ // The 'slop' factor, after which we consider the use to be dragging. The value
378
+ // is taken from the Android SDK. It won't be robust to page zoom and the like,
379
+ // but it should be good enough for our purposes.
380
+ const touchSlopPx = 8;
381
+
382
+ class DragListener {
383
+ constructor(onDrag, initialEvent) {
384
+ // We detect drags in two ways. First, by listening for the window
385
+ // scroll event (we consider any legitimate scroll to be a drag).
386
+ this._scrollListener = () => {
387
+ onDrag();
388
+ }; // And second, by listening for touch moves and tracking the each
389
+ // finger's displacement. This allows us to track, e.g., when the user
390
+ // scrolls within an individual view.
391
+
392
+
393
+ const touchLocationsById = {};
394
+
395
+ for (let i = 0; i < initialEvent.changedTouches.length; i++) {
396
+ const touch = initialEvent.changedTouches[i];
397
+ touchLocationsById[touch.identifier] = [touch.clientX, touch.clientY];
398
+ }
399
+
400
+ this._moveListener = evt => {
401
+ for (let i = 0; i < evt.changedTouches.length; i++) {
402
+ const touch = evt.changedTouches[i];
403
+ const initialTouchLocation = touchLocationsById[touch.identifier];
404
+
405
+ if (initialTouchLocation) {
406
+ const touchLocation = [touch.clientX, touch.clientY];
407
+ const dx = touchLocation[0] - initialTouchLocation[0];
408
+ const dy = touchLocation[1] - initialTouchLocation[1];
409
+ const squaredDist = dx * dx + dy * dy;
410
+ const squaredTouchSlop = touchSlopPx * touchSlopPx;
411
+
412
+ if (squaredDist > squaredTouchSlop) {
413
+ onDrag();
414
+ }
415
+ }
416
+ }
417
+ }; // Clean-up any terminated gestures, since some browsers reuse
418
+ // identifiers.
419
+
420
+
421
+ this._endAndCancelListener = evt => {
422
+ for (let i = 0; i < evt.changedTouches.length; i++) {
423
+ delete touchLocationsById[evt.changedTouches[i].identifier];
424
+ }
425
+ };
426
+ }
427
+
428
+ attach() {
429
+ window.addEventListener("scroll", this._scrollListener);
430
+ window.addEventListener("touchmove", this._moveListener);
431
+ window.addEventListener("touchend", this._endAndCancelListener);
432
+ window.addEventListener("touchcancel", this._endAndCancelListener);
433
+ }
434
+
435
+ detach() {
436
+ window.removeEventListener("scroll", this._scrollListener);
437
+ window.removeEventListener("touchmove", this._moveListener);
438
+ window.removeEventListener("touchend", this._endAndCancelListener);
439
+ window.removeEventListener("touchcancel", this._endAndCancelListener);
440
+ }
441
+
442
+ }
443
+
273
444
  /**
274
445
  * Constants that are shared between multiple files.
275
446
  */
@@ -343,1193 +514,656 @@ const EchoAnimationTypes = {
343
514
  const decimalSeparator = i18n.getDecimalSeparator() === "," ? DecimalSeparators.COMMA : DecimalSeparators.PERIOD;
344
515
 
345
516
  /**
346
- * This file contains configuration settings for the buttons in the keypad.
517
+ * Constants that define the various contexts in which a cursor can exist. The
518
+ * active context is determined first by looking at the cursor's siblings (e.g.,
519
+ * for the `BEFORE_FRACTION` context), and then at its direct parent. Though a
520
+ * cursor could in theory be nested in multiple contexts, we only care about the
521
+ * immediate context.
522
+ *
523
+ * TODO(charlie): Add a context to represent being inside of a radical. Right
524
+ * now, we show the dismiss button rather than allowing the user to jump out of
525
+ * the radical.
347
526
  */
348
- const KeyConfigs = {
349
- // Basic math keys.
527
+ // TODO: Get rid of these constants in favour of CursorContext type.
528
+ // The cursor is not in any of the other viable contexts.
529
+ const NONE = "NONE"; // The cursor is within a set of parentheses.
530
+
531
+ const IN_PARENS = "IN_PARENS"; // The cursor is within a superscript (e.g., an exponent).
532
+
533
+ const IN_SUPER_SCRIPT = "IN_SUPER_SCRIPT"; // The cursor is within a subscript (e.g., the base of a custom logarithm).
534
+
535
+ const IN_SUB_SCRIPT = "IN_SUB_SCRIPT"; // The cursor is in the numerator of a fraction.
536
+
537
+ const IN_NUMERATOR = "IN_NUMERATOR"; // The cursor is in the denominator of a fraction.
538
+
539
+ const IN_DENOMINATOR = "IN_DENOMINATOR"; // The cursor is sitting before a fraction; that is, the cursor is within
540
+ // what looks to be a mixed number preceding a fraction. This will only be
541
+ // the case when the only math between the cursor and the fraction to its
542
+ // write is non-leaf math (numbers and variables).
543
+
544
+ const BEFORE_FRACTION = "BEFORE_FRACTION";
545
+
546
+ var CursorContexts = /*#__PURE__*/Object.freeze({
547
+ __proto__: null,
548
+ NONE: NONE,
549
+ IN_PARENS: IN_PARENS,
550
+ IN_SUPER_SCRIPT: IN_SUPER_SCRIPT,
551
+ IN_SUB_SCRIPT: IN_SUB_SCRIPT,
552
+ IN_NUMERATOR: IN_NUMERATOR,
553
+ IN_DENOMINATOR: IN_DENOMINATOR,
554
+ BEFORE_FRACTION: BEFORE_FRACTION
555
+ });
556
+
557
+ /**
558
+ * This file contains a wrapper around MathQuill so that we can provide a
559
+ * more regular interface for the functionality we need while insulating us
560
+ * from MathQuill changes.
561
+ */
562
+ // If it does not exist, fall back to CommonJS require. - jsatk
563
+
564
+ const decimalSymbol = decimalSeparator === DecimalSeparators.COMMA ? "," : ".";
565
+ const WRITE = "write";
566
+ const CMD = "cmd";
567
+ const KEYSTROKE = "keystroke";
568
+ const MQ_END = 0; // A mapping from keys that can be pressed on a keypad to the way in which
569
+ // MathQuill should modify its input in response to that key-press. Any keys
570
+ // that do not provide explicit actions (like the numeral keys) will merely
571
+ // write their contents to MathQuill.
572
+
573
+ const KeyActions = {
350
574
  [Keys.PLUS]: {
351
- type: KeyTypes.OPERATOR,
352
- // I18N: A label for a plus sign.
353
- ariaLabel: i18n__namespace._("Plus")
575
+ str: "+",
576
+ fn: WRITE
354
577
  },
355
578
  [Keys.MINUS]: {
356
- type: KeyTypes.OPERATOR,
357
- // I18N: A label for a minus sign.
358
- ariaLabel: i18n__namespace._("Minus")
579
+ str: "-",
580
+ fn: WRITE
359
581
  },
360
582
  [Keys.NEGATIVE]: {
361
- type: KeyTypes.VALUE,
362
- // I18N: A label for a minus sign.
363
- ariaLabel: i18n__namespace._("Negative")
583
+ str: "-",
584
+ fn: WRITE
364
585
  },
365
586
  [Keys.TIMES]: {
366
- type: KeyTypes.OPERATOR,
367
- // I18N: A label for a multiplication sign (represented with an 'x').
368
- ariaLabel: i18n__namespace._("Multiply")
587
+ str: "\\times",
588
+ fn: WRITE
369
589
  },
370
590
  [Keys.DIVIDE]: {
371
- type: KeyTypes.OPERATOR,
372
- // I18N: A label for a division sign.
373
- ariaLabel: i18n__namespace._("Divide")
591
+ str: "\\div",
592
+ fn: WRITE
374
593
  },
375
594
  [Keys.DECIMAL]: {
376
- type: KeyTypes.VALUE,
377
- // I18N: A label for a decimal symbol.
378
- ariaLabel: i18n__namespace._("Decimal"),
379
- icon: decimalSeparator === DecimalSeparators.COMMA ? {
380
- // TODO(charlie): Get an SVG icon for the comma, or verify with
381
- // design that the text-rendered version is acceptable.
382
- type: IconTypes.TEXT,
383
- data: ","
384
- } : {
385
- type: IconTypes.SVG,
386
- data: Keys.PERIOD
387
- }
388
- },
389
- [Keys.PERCENT]: {
390
- type: KeyTypes.OPERATOR,
391
- // I18N: A label for a percent sign.
392
- ariaLabel: i18n__namespace._("Percent")
393
- },
394
- [Keys.CDOT]: {
395
- type: KeyTypes.OPERATOR,
396
- // I18N: A label for a multiplication sign (represented as a dot).
397
- ariaLabel: i18n__namespace._("Multiply")
595
+ str: decimalSymbol,
596
+ fn: WRITE
398
597
  },
399
598
  [Keys.EQUAL]: {
400
- type: KeyTypes.OPERATOR,
401
- ariaLabel: i18n__namespace._("Equals sign")
599
+ str: "=",
600
+ fn: WRITE
402
601
  },
403
602
  [Keys.NEQ]: {
404
- type: KeyTypes.OPERATOR,
405
- ariaLabel: i18n__namespace._("Not-equals sign")
406
- },
407
- [Keys.GT]: {
408
- type: KeyTypes.OPERATOR,
409
- // I18N: A label for a 'greater than' sign (represented as '>').
410
- ariaLabel: i18n__namespace._("Greater than sign")
411
- },
412
- [Keys.LT]: {
413
- type: KeyTypes.OPERATOR,
414
- // I18N: A label for a 'less than' sign (represented as '<').
415
- ariaLabel: i18n__namespace._("Less than sign")
416
- },
417
- [Keys.GEQ]: {
418
- type: KeyTypes.OPERATOR,
419
- ariaLabel: i18n__namespace._("Greater than or equal to sign")
420
- },
421
- [Keys.LEQ]: {
422
- type: KeyTypes.OPERATOR,
423
- ariaLabel: i18n__namespace._("Less than or equal to sign")
424
- },
425
- // mobile native
426
- [Keys.FRAC_INCLUSIVE]: {
427
- type: KeyTypes.OPERATOR,
428
- // I18N: A label for a button that creates a new fraction and puts the
429
- // current expression in the numerator of that fraction.
430
- ariaLabel: i18n__namespace._("Fraction, with current expression in numerator")
431
- },
432
- // mobile native
433
- [Keys.FRAC_EXCLUSIVE]: {
434
- type: KeyTypes.OPERATOR,
435
- // I18N: A label for a button that creates a new fraction next to the
436
- // cursor.
437
- ariaLabel: i18n__namespace._("Fraction, excluding the current expression")
438
- },
439
- // mobile web
440
- [Keys.FRAC]: {
441
- type: KeyTypes.OPERATOR,
442
- // I18N: A label for a button that creates a new fraction next to the
443
- // cursor.
444
- ariaLabel: i18n__namespace._("Fraction, excluding the current expression")
445
- },
446
- [Keys.EXP]: {
447
- type: KeyTypes.OPERATOR,
448
- // I18N: A label for a button that will allow the user to input a custom
449
- // exponent.
450
- ariaLabel: i18n__namespace._("Custom exponent")
451
- },
452
- [Keys.EXP_2]: {
453
- type: KeyTypes.OPERATOR,
454
- // I18N: A label for a button that will square (take to the second
455
- // power) some math.
456
- ariaLabel: i18n__namespace._("Square")
457
- },
458
- [Keys.EXP_3]: {
459
- type: KeyTypes.OPERATOR,
460
- // I18N: A label for a button that will cube (take to the third power)
461
- // some math.
462
- ariaLabel: i18n__namespace._("Cube")
463
- },
464
- [Keys.SQRT]: {
465
- type: KeyTypes.OPERATOR,
466
- ariaLabel: i18n__namespace._("Square root")
603
+ str: "\\neq",
604
+ fn: WRITE
467
605
  },
468
- [Keys.CUBE_ROOT]: {
469
- type: KeyTypes.OPERATOR,
470
- ariaLabel: i18n__namespace._("Cube root")
606
+ [Keys.CDOT]: {
607
+ str: "\\cdot",
608
+ fn: WRITE
471
609
  },
472
- [Keys.RADICAL]: {
473
- type: KeyTypes.OPERATOR,
474
- ariaLabel: i18n__namespace._("Radical with custom root")
610
+ [Keys.PERCENT]: {
611
+ str: "%",
612
+ fn: WRITE
475
613
  },
476
614
  [Keys.LEFT_PAREN]: {
477
- type: KeyTypes.OPERATOR,
478
- ariaLabel: i18n__namespace._("Left parenthesis")
615
+ str: "(",
616
+ fn: CMD
479
617
  },
480
618
  [Keys.RIGHT_PAREN]: {
481
- type: KeyTypes.OPERATOR,
482
- ariaLabel: i18n__namespace._("Right parenthesis")
483
- },
484
- [Keys.LN]: {
485
- type: KeyTypes.OPERATOR,
486
- ariaLabel: i18n__namespace._("Natural logarithm")
487
- },
488
- [Keys.LOG]: {
489
- type: KeyTypes.OPERATOR,
490
- ariaLabel: i18n__namespace._("Logarithm with base 10")
491
- },
492
- [Keys.LOG_N]: {
493
- type: KeyTypes.OPERATOR,
494
- ariaLabel: i18n__namespace._("Logarithm with custom base")
495
- },
496
- [Keys.SIN]: {
497
- type: KeyTypes.OPERATOR,
498
- ariaLabel: i18n__namespace._("Sine")
499
- },
500
- [Keys.COS]: {
501
- type: KeyTypes.OPERATOR,
502
- ariaLabel: i18n__namespace._("Cosine")
619
+ str: ")",
620
+ fn: CMD
503
621
  },
504
- [Keys.TAN]: {
505
- type: KeyTypes.OPERATOR,
506
- ariaLabel: i18n__namespace._("Tangent")
622
+ [Keys.SQRT]: {
623
+ str: "sqrt",
624
+ fn: CMD
507
625
  },
508
626
  [Keys.PI]: {
509
- type: KeyTypes.VALUE,
510
- ariaLabel: i18n__namespace._("Pi"),
511
- icon: {
512
- type: IconTypes.MATH,
513
- data: "\\pi"
514
- }
627
+ str: "pi",
628
+ fn: CMD
515
629
  },
516
630
  [Keys.THETA]: {
517
- type: KeyTypes.VALUE,
518
- ariaLabel: i18n__namespace._("Theta"),
519
- icon: {
520
- type: IconTypes.MATH,
521
- data: "\\theta"
522
- }
523
- },
524
- [Keys.NOOP]: {
525
- type: KeyTypes.EMPTY
526
- },
527
- // Input navigation keys.
528
- [Keys.UP]: {
529
- type: KeyTypes.INPUT_NAVIGATION,
530
- ariaLabel: i18n__namespace._("Up arrow")
531
- },
532
- [Keys.RIGHT]: {
533
- type: KeyTypes.INPUT_NAVIGATION,
534
- ariaLabel: i18n__namespace._("Right arrow")
535
- },
536
- [Keys.DOWN]: {
537
- type: KeyTypes.INPUT_NAVIGATION,
538
- ariaLabel: i18n__namespace._("Down arrow")
539
- },
540
- [Keys.LEFT]: {
541
- type: KeyTypes.INPUT_NAVIGATION,
542
- ariaLabel: i18n__namespace._("Left arrow")
631
+ str: "theta",
632
+ fn: CMD
543
633
  },
544
- [Keys.JUMP_OUT_PARENTHESES]: {
545
- type: KeyTypes.INPUT_NAVIGATION,
546
- ariaLabel: i18n__namespace._("Navigate right out of a set of parentheses")
634
+ [Keys.RADICAL]: {
635
+ str: "nthroot",
636
+ fn: CMD
547
637
  },
548
- [Keys.JUMP_OUT_EXPONENT]: {
549
- type: KeyTypes.INPUT_NAVIGATION,
550
- ariaLabel: i18n__namespace._("Navigate right out of an exponent")
638
+ [Keys.LT]: {
639
+ str: "<",
640
+ fn: WRITE
551
641
  },
552
- [Keys.JUMP_OUT_BASE]: {
553
- type: KeyTypes.INPUT_NAVIGATION,
554
- ariaLabel: i18n__namespace._("Navigate right out of a base")
642
+ [Keys.LEQ]: {
643
+ str: "\\leq",
644
+ fn: WRITE
555
645
  },
556
- [Keys.JUMP_INTO_NUMERATOR]: {
557
- type: KeyTypes.INPUT_NAVIGATION,
558
- ariaLabel: i18n__namespace._("Navigate right into the numerator of a fraction")
646
+ [Keys.GT]: {
647
+ str: ">",
648
+ fn: WRITE
559
649
  },
560
- [Keys.JUMP_OUT_NUMERATOR]: {
561
- type: KeyTypes.INPUT_NAVIGATION,
562
- ariaLabel: i18n__namespace._("Navigate right out of the numerator and into the denominator")
650
+ [Keys.GEQ]: {
651
+ str: "\\geq",
652
+ fn: WRITE
563
653
  },
564
- [Keys.JUMP_OUT_DENOMINATOR]: {
565
- type: KeyTypes.INPUT_NAVIGATION,
566
- ariaLabel: i18n__namespace._("Navigate right out of the denominator of a fraction")
654
+ [Keys.UP]: {
655
+ str: "Up",
656
+ fn: KEYSTROKE
567
657
  },
568
- [Keys.BACKSPACE]: {
569
- type: KeyTypes.INPUT_NAVIGATION,
570
- // I18N: A label for a button that will delete some input.
571
- ariaLabel: i18n__namespace._("Delete")
658
+ [Keys.DOWN]: {
659
+ str: "Down",
660
+ fn: KEYSTROKE
572
661
  },
573
- // Keypad navigation keys.
574
- [Keys.DISMISS]: {
575
- type: KeyTypes.KEYPAD_NAVIGATION,
576
- // I18N: A label for a button that will dismiss/hide a keypad.
577
- ariaLabel: i18n__namespace._("Dismiss")
662
+ // The `FRAC_EXCLUSIVE` variant is handled manually, since we may need to do
663
+ // some additional navigation depending on the cursor position.
664
+ [Keys.FRAC_INCLUSIVE]: {
665
+ str: "/",
666
+ fn: CMD
578
667
  }
579
- }; // Add in any multi-function buttons. By default, these keys will mix in any
580
- // configuration settings from their default child key (i.e., the first key in
581
- // the `childKeyIds` array).
582
- // TODO(charlie): Make the multi-function button's long-press interaction
583
- // accessible.
584
- // NOTE(kevinb): This is only used in the mobile native app.
585
-
586
- KeyConfigs[Keys.FRAC_MULTI] = {
587
- childKeyIds: [Keys.FRAC_INCLUSIVE, Keys.FRAC_EXCLUSIVE]
588
- }; // TODO(charlie): Use the numeral color for the 'Many' key.
668
+ };
669
+ const NormalCommands = {
670
+ [Keys.LOG]: "log",
671
+ [Keys.LN]: "ln",
672
+ [Keys.SIN]: "sin",
673
+ [Keys.COS]: "cos",
674
+ [Keys.TAN]: "tan"
675
+ };
676
+ const ArithmeticOperators = ["+", "-", "\\cdot", "\\times", "\\div"];
677
+ const EqualityOperators = ["=", "\\neq", "<", "\\leq", ">", "\\geq"];
678
+ const Numerals = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
679
+ const GreekLetters = ["\\theta", "\\pi"];
680
+ const Letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; // We only consider numerals, variables, and Greek Letters to be proper
681
+ // leaf nodes.
589
682
 
590
- KeyConfigs[Keys.MANY] = {
591
- type: KeyTypes.MANY // childKeyIds will be configured by the client.
683
+ const ValidLeaves = [...Numerals, ...GreekLetters, ...Letters.map(letter => letter.toLowerCase()), ...Letters.map(letter => letter.toUpperCase())];
684
+ const KeysForJumpContext = {
685
+ [IN_PARENS]: Keys.JUMP_OUT_PARENTHESES,
686
+ [IN_SUPER_SCRIPT]: Keys.JUMP_OUT_EXPONENT,
687
+ [IN_SUB_SCRIPT]: Keys.JUMP_OUT_BASE,
688
+ [BEFORE_FRACTION]: Keys.JUMP_INTO_NUMERATOR,
689
+ [IN_NUMERATOR]: Keys.JUMP_OUT_NUMERATOR,
690
+ [IN_DENOMINATOR]: Keys.JUMP_OUT_DENOMINATOR
691
+ };
592
692
 
593
- }; // Add in every numeral.
693
+ class MathWrapper {
694
+ constructor(element) {
695
+ let callbacks = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
696
+ this.MQ = MathQuill__default["default"].getInterface(2);
697
+ this.mathField = this.MQ.MathField(element, {
698
+ // use a span instead of a textarea so that we don't bring up the
699
+ // native keyboard on mobile when selecting the input
700
+ substituteTextarea: function () {
701
+ return document.createElement("span");
702
+ }
703
+ });
704
+ this.callbacks = callbacks;
705
+ }
594
706
 
595
- const NUMBERS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
707
+ focus() {
708
+ // HACK(charlie): We shouldn't reaching into MathQuill internals like
709
+ // this, but it's the easiest way to allow us to manage the focus state
710
+ // ourselves.
711
+ const controller = this.mathField.__controller;
712
+ controller.cursor.show(); // Set MathQuill's internal state to reflect the focus, otherwise it
713
+ // will consistently try to hide the cursor on key-press and introduce
714
+ // layout jank.
596
715
 
597
- for (const num of NUMBERS) {
598
- // TODO(charlie): Consider removing the SVG icons that we have for the
599
- // numeral keys. They can be rendered just as easily with text (though that
600
- // would mean that we'd be using text beyond the variable key).
601
- const textRepresentation = "".concat(num);
602
- KeyConfigs["NUM_".concat(num)] = {
603
- type: KeyTypes.VALUE,
604
- ariaLabel: textRepresentation,
605
- icon: {
606
- type: IconTypes.TEXT,
607
- data: textRepresentation
608
- }
609
- };
610
- } // Add in every variable.
716
+ controller.blurred = false;
717
+ }
611
718
 
719
+ blur() {
720
+ const controller = this.mathField.__controller;
721
+ controller.cursor.hide();
722
+ controller.blurred = true;
723
+ }
612
724
 
613
- const LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
725
+ _writeNormalFunction(name) {
726
+ this.mathField.write("\\".concat(name, "\\left(\\right)"));
727
+ this.mathField.keystroke("Left");
728
+ }
729
+ /**
730
+ * Handle a key press and return the resulting cursor state.
731
+ *
732
+ * @param {Key} key - an enum representing the key that was pressed
733
+ * @returns {object} a cursor object, consisting of a cursor context
734
+ */
614
735
 
615
- for (const letter of LETTERS) {
616
- const lowerCaseVariable = letter.toLowerCase();
617
- const upperCaseVariable = letter.toUpperCase();
618
736
 
619
- for (const textRepresentation of [lowerCaseVariable, upperCaseVariable]) {
620
- KeyConfigs[textRepresentation] = {
621
- type: KeyTypes.VALUE,
622
- ariaLabel: textRepresentation,
623
- icon: {
624
- type: IconTypes.MATH,
625
- data: textRepresentation
737
+ pressKey(key) {
738
+ const cursor = this.mathField.__controller.cursor;
739
+
740
+ if (key in KeyActions) {
741
+ const {
742
+ str,
743
+ fn
744
+ } = KeyActions[key];
745
+
746
+ if (str && fn) {
747
+ this.mathField[fn](str);
748
+ }
749
+ } else if (Object.keys(NormalCommands).includes(key)) {
750
+ this._writeNormalFunction(NormalCommands[key]);
751
+ } else if (key === Keys.FRAC_EXCLUSIVE) {
752
+ // If there's nothing to the left of the cursor, then we want to
753
+ // leave the cursor to the left of the fraction after creating it.
754
+ const shouldNavigateLeft = cursor[this.MQ.L] === MQ_END;
755
+ this.mathField.cmd("\\frac");
756
+
757
+ if (shouldNavigateLeft) {
758
+ this.mathField.keystroke("Left");
626
759
  }
760
+ } else if (key === Keys.FRAC) {
761
+ // eslint-disable-next-line no-unused-vars
762
+ cursor[this.MQ.L] === MQ_END;
763
+ this.mathField.cmd("\\frac");
764
+ } else if (key === Keys.LOG_N) {
765
+ this.mathField.write("log_{ }\\left(\\right)");
766
+ this.mathField.keystroke("Left"); // into parentheses
767
+
768
+ this.mathField.keystroke("Left"); // out of parentheses
769
+
770
+ this.mathField.keystroke("Left"); // into index
771
+ } else if (key === Keys.CUBE_ROOT) {
772
+ this.mathField.write("\\sqrt[3]{}");
773
+ this.mathField.keystroke("Left"); // under the root
774
+ } else if (key === Keys.EXP || key === Keys.EXP_2 || key === Keys.EXP_3) {
775
+ this._handleExponent(cursor, key);
776
+ } else if (key === Keys.JUMP_OUT_PARENTHESES || key === Keys.JUMP_OUT_EXPONENT || key === Keys.JUMP_OUT_BASE || key === Keys.JUMP_INTO_NUMERATOR || key === Keys.JUMP_OUT_NUMERATOR || key === Keys.JUMP_OUT_DENOMINATOR) {
777
+ this._handleJumpOut(cursor, key);
778
+ } else if (key === Keys.BACKSPACE) {
779
+ this._handleBackspace(cursor);
780
+ } else if (key === Keys.LEFT) {
781
+ this._handleLeftArrow(cursor);
782
+ } else if (key === Keys.RIGHT || key === Keys.JUMP_OUT) {
783
+ this._handleRightArrow(cursor);
784
+ } else if (/^[a-zA-Z]$/.test(key)) {
785
+ this.mathField[WRITE](key);
786
+ } else if (/^NUM_\d/.test(key)) {
787
+ this.mathField[WRITE](key[4]);
788
+ }
789
+
790
+ if (!cursor.selection) {
791
+ // don't show the cursor for selections
792
+ cursor.show();
793
+ }
794
+
795
+ if (this.callbacks.onSelectionChanged) {
796
+ this.callbacks.onSelectionChanged(cursor.selection);
797
+ } // NOTE(charlie): It's insufficient to do this as an `edited` handler
798
+ // on the MathField, as that handler isn't triggered on navigation
799
+ // events.
800
+
801
+
802
+ return {
803
+ context: this.contextForCursor(cursor)
627
804
  };
628
805
  }
629
- }
806
+ /**
807
+ * Place the cursor beside the node located at the given coordinates.
808
+ *
809
+ * @param {number} x - the x coordinate in the viewport
810
+ * @param {number} y - the y coordinate in the viewport
811
+ * @param {Node} hitNode - the node next to which the cursor should be
812
+ * placed; if provided, the coordinates will be used
813
+ * to determine on which side of the node the cursor
814
+ * should be placed
815
+ */
630
816
 
631
- for (const key of Object.keys(KeyConfigs)) {
632
- KeyConfigs[key] = {
633
- id: key,
634
- // Default to an SVG icon indexed by the key name.
635
- icon: {
636
- type: IconTypes.SVG,
637
- data: key
638
- },
639
- ...KeyConfigs[key]
640
- };
641
- }
642
817
 
643
- /**
644
- * Constants that define the various contexts in which a cursor can exist. The
645
- * active context is determined first by looking at the cursor's siblings (e.g.,
646
- * for the `BEFORE_FRACTION` context), and then at its direct parent. Though a
647
- * cursor could in theory be nested in multiple contexts, we only care about the
648
- * immediate context.
649
- *
650
- * TODO(charlie): Add a context to represent being inside of a radical. Right
651
- * now, we show the dismiss button rather than allowing the user to jump out of
652
- * the radical.
653
- */
654
- // TODO: Get rid of these constants in favour of CursorContext type.
655
- // The cursor is not in any of the other viable contexts.
656
- const NONE = "NONE"; // The cursor is within a set of parentheses.
818
+ setCursorPosition(x, y, hitNode) {
819
+ const el = hitNode || document.elementFromPoint(x, y);
657
820
 
658
- const IN_PARENS = "IN_PARENS"; // The cursor is within a superscript (e.g., an exponent).
821
+ if (el) {
822
+ const cursor = this.getCursor();
659
823
 
660
- const IN_SUPER_SCRIPT = "IN_SUPER_SCRIPT"; // The cursor is within a subscript (e.g., the base of a custom logarithm).
824
+ if (el.hasAttribute("mq-root-block")) {
825
+ // If we're in the empty area place the cursor at the right
826
+ // end of the expression.
827
+ cursor.insAtRightEnd(this.mathField.__controller.root);
828
+ } else {
829
+ // Otherwise place beside the element at x, y.
830
+ const controller = this.mathField.__controller;
831
+ const pageX = x - document.body.scrollLeft;
832
+ const pageY = y - document.body.scrollTop;
833
+ controller.seek($__default["default"](el), pageX, pageY).cursor.startSelection(); // Unless that would leave us mid-command, in which case, we
834
+ // need to adjust and place the cursor inside the parens
835
+ // following the command.
661
836
 
662
- const IN_SUB_SCRIPT = "IN_SUB_SCRIPT"; // The cursor is in the numerator of a fraction.
837
+ const command = this._maybeFindCommand(cursor[this.MQ.L]);
663
838
 
664
- const IN_NUMERATOR = "IN_NUMERATOR"; // The cursor is in the denominator of a fraction.
839
+ if (command && command.endNode) {
840
+ // NOTE(charlie): endNode should definitely be \left(.
841
+ cursor.insLeftOf(command.endNode);
842
+ this.mathField.keystroke("Right");
843
+ }
844
+ }
665
845
 
666
- const IN_DENOMINATOR = "IN_DENOMINATOR"; // The cursor is sitting before a fraction; that is, the cursor is within
667
- // what looks to be a mixed number preceding a fraction. This will only be
668
- // the case when the only math between the cursor and the fraction to its
669
- // write is non-leaf math (numbers and variables).
846
+ if (this.callbacks.onCursorMove) {
847
+ this.callbacks.onCursorMove({
848
+ context: this.contextForCursor(cursor)
849
+ });
850
+ }
851
+ }
852
+ }
670
853
 
671
- const BEFORE_FRACTION = "BEFORE_FRACTION";
854
+ getCursor() {
855
+ return this.mathField.__controller.cursor;
856
+ }
672
857
 
673
- var CursorContexts = /*#__PURE__*/Object.freeze({
674
- __proto__: null,
675
- NONE: NONE,
676
- IN_PARENS: IN_PARENS,
677
- IN_SUPER_SCRIPT: IN_SUPER_SCRIPT,
678
- IN_SUB_SCRIPT: IN_SUB_SCRIPT,
679
- IN_NUMERATOR: IN_NUMERATOR,
680
- IN_DENOMINATOR: IN_DENOMINATOR,
681
- BEFORE_FRACTION: BEFORE_FRACTION
682
- });
858
+ getSelection() {
859
+ return this.getCursor().selection;
860
+ }
683
861
 
684
- /**
685
- * React PropTypes that may be shared between components.
686
- */
687
- const iconPropType = PropTypes__default["default"].shape({
688
- type: PropTypes__default["default"].oneOf(Object.keys(IconTypes)).isRequired,
689
- data: PropTypes__default["default"].string.isRequired
690
- });
691
- const keyIdPropType = PropTypes__default["default"].oneOf(Object.keys(KeyConfigs));
692
- const keyConfigPropType = PropTypes__default["default"].shape({
693
- ariaLabel: PropTypes__default["default"].string,
694
- id: keyIdPropType.isRequired,
695
- type: PropTypes__default["default"].oneOf(Object.keys(KeyTypes)).isRequired,
696
- childKeyIds: PropTypes__default["default"].arrayOf(keyIdPropType),
697
- icon: iconPropType.isRequired
698
- });
699
- const keypadConfigurationPropType = PropTypes__default["default"].shape({
700
- keypadType: PropTypes__default["default"].oneOf(Object.keys(KeypadTypes)).isRequired,
701
- extraKeys: PropTypes__default["default"].arrayOf(keyIdPropType)
702
- }); // NOTE(jared): This is no longer guaranteed to be React element
862
+ getContent() {
863
+ return this.mathField.latex();
864
+ }
703
865
 
704
- const keypadElementPropType = PropTypes__default["default"].shape({
705
- activate: PropTypes__default["default"].func.isRequired,
706
- dismiss: PropTypes__default["default"].func.isRequired,
707
- configure: PropTypes__default["default"].func.isRequired,
708
- setCursor: PropTypes__default["default"].func.isRequired,
709
- setKeyHandler: PropTypes__default["default"].func.isRequired,
710
- getDOMNode: PropTypes__default["default"].func.isRequired
711
- });
712
- const bordersPropType = PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOf(Object.keys(BorderDirections)));
713
- const boundingBoxPropType = PropTypes__default["default"].shape({
714
- height: PropTypes__default["default"].number,
715
- width: PropTypes__default["default"].number,
716
- top: PropTypes__default["default"].number,
717
- right: PropTypes__default["default"].number,
718
- bottom: PropTypes__default["default"].number,
719
- left: PropTypes__default["default"].number
720
- });
721
- const echoPropType = PropTypes__default["default"].shape({
722
- animationId: PropTypes__default["default"].string.isRequired,
723
- animationType: PropTypes__default["default"].oneOf(Object.keys(EchoAnimationTypes)).isRequired,
724
- borders: bordersPropType,
725
- id: keyIdPropType.isRequired,
726
- initialBounds: boundingBoxPropType.isRequired
727
- });
728
- const cursorContextPropType = PropTypes__default["default"].oneOf(Object.keys(CursorContexts));
729
- const popoverPropType = PropTypes__default["default"].shape({
730
- parentId: keyIdPropType.isRequired,
731
- bounds: boundingBoxPropType.isRequired,
732
- childKeyIds: PropTypes__default["default"].arrayOf(keyIdPropType).isRequired
733
- });
734
- PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].node), PropTypes__default["default"].node]);
735
-
736
- const touchTargetRadiusPx = 2 * cursorHandleRadiusPx;
737
- const touchTargetHeightPx = 2 * touchTargetRadiusPx;
738
- const touchTargetWidthPx = 2 * touchTargetRadiusPx;
739
- const cursorRadiusPx = cursorHandleRadiusPx;
740
- const cursorHeightPx = cursorHandleDistanceMultiplier * (cursorRadiusPx * 4);
741
- const cursorWidthPx = 4 * cursorRadiusPx;
742
-
743
- class CursorHandle extends React__namespace.Component {
744
- render() {
745
- const {
746
- x,
747
- y,
748
- animateIntoPosition
749
- } = this.props;
750
- const animationStyle = animateIntoPosition ? {
751
- msTransitionDuration: "100ms",
752
- WebkitTransitionDuration: "100ms",
753
- transitionDuration: "100ms",
754
- msTransitionProperty: "transform",
755
- WebkitTransitionProperty: "transform",
756
- transitionProperty: "transform"
757
- } : {};
758
- const transformString = "translate(".concat(x, "px, ").concat(y, "px)");
759
- const outerStyle = {
760
- position: "absolute",
761
- // This is essentially webapp's interactiveComponent + 1.
762
- // TODO(charlie): Pull in those styles somehow to avoid breakages.
763
- zIndex: 4,
764
- left: -touchTargetWidthPx / 2,
765
- top: 0,
766
- msTransform: transformString,
767
- WebkitTransform: transformString,
768
- transform: transformString,
769
- width: touchTargetWidthPx,
770
- height: touchTargetHeightPx,
771
- // Touch events that start on the cursor shouldn't be allowed to
772
- // produce page scrolls.
773
- touchAction: "none",
774
- ...animationStyle
775
- };
776
- return /*#__PURE__*/React__namespace.createElement("span", {
777
- style: outerStyle,
778
- onTouchStart: this.props.onTouchStart,
779
- onTouchMove: this.props.onTouchMove,
780
- onTouchEnd: this.props.onTouchEnd,
781
- onTouchCancel: this.props.onTouchCancel
782
- }, /*#__PURE__*/React__namespace.createElement("svg", {
783
- fill: "none",
784
- width: cursorWidthPx,
785
- height: cursorHeightPx,
786
- viewBox: "0 0 ".concat(cursorWidthPx, " ").concat(cursorHeightPx)
787
- }, /*#__PURE__*/React__namespace.createElement("filter", {
788
- id: "math-input_cursor",
789
- colorInterpolationFilters: "sRGB",
790
- filterUnits: "userSpaceOnUse",
791
- height: cursorHeightPx * 0.87 // ~40
792
- ,
793
- width: cursorWidthPx * 0.82 // ~36
794
- ,
795
- x: "4",
796
- y: "0"
797
- }, /*#__PURE__*/React__namespace.createElement("feFlood", {
798
- floodOpacity: "0",
799
- result: "BackgroundImageFix"
800
- }), /*#__PURE__*/React__namespace.createElement("feColorMatrix", {
801
- in: "SourceAlpha",
802
- type: "matrix",
803
- values: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
804
- }), /*#__PURE__*/React__namespace.createElement("feOffset", {
805
- dy: "4"
806
- }), /*#__PURE__*/React__namespace.createElement("feGaussianBlur", {
807
- stdDeviation: "4"
808
- }), /*#__PURE__*/React__namespace.createElement("feColorMatrix", {
809
- type: "matrix",
810
- values: "0 0 0 0 0.129412 0 0 0 0 0.141176 0 0 0 0 0.172549 0 0 0 0.08 0"
811
- }), /*#__PURE__*/React__namespace.createElement("feBlend", {
812
- in2: "BackgroundImageFix",
813
- mode: "normal",
814
- result: "effect1_dropShadow"
815
- }), /*#__PURE__*/React__namespace.createElement("feBlend", {
816
- in: "SourceGraphic",
817
- in2: "effect1_dropShadow",
818
- mode: "normal",
819
- result: "shape"
820
- })), /*#__PURE__*/React__namespace.createElement("g", {
821
- filter: "url(#math-input_cursor)"
822
- }, /*#__PURE__*/React__namespace.createElement("path", {
823
- d: "m22 4-7.07 7.0284c-1.3988 1.3901-2.3515 3.1615-2.7376 5.09-.3861 1.9284-.1883 3.9274.5685 5.7441s2.0385 3.3694 3.6831 4.4619c1.6445 1.0925 3.5781 1.6756 5.556 1.6756s3.9115-.5831 5.556-1.6756c1.6446-1.0925 2.9263-2.6452 3.6831-4.4619s.9546-3.8157.5685-5.7441c-.3861-1.9285-1.3388-3.6999-2.7376-5.09z",
824
- fill: "#1865f2"
825
- })), /*#__PURE__*/React__namespace.createElement("path", {
826
- d: "m14.9301 10.4841 7.0699-7.06989 7.0699 7.06989.0001.0001c1.3988 1.3984 2.3515 3.1802 2.7376 5.1201s.1883 3.9507-.5685 5.7782c-.7568 1.8274-2.0385 3.3894-3.6831 4.4883-1.6445 1.099-3.5781 1.6855-5.556 1.6855s-3.9115-.5865-5.556-1.6855c-1.6446-1.0989-2.9263-2.6609-3.6831-4.4883-.7568-1.8275-.9546-3.8383-.5685-5.7782s1.3388-3.7217 2.7376-5.1201z",
827
- stroke: "#fff",
828
- strokeWidth: "2"
829
- })));
866
+ setContent(latex) {
867
+ this.mathField.latex(latex);
830
868
  }
831
869
 
832
- }
870
+ isEmpty() {
871
+ const cursor = this.getCursor();
872
+ return cursor.parent.id === 1 && cursor[1] === 0 && cursor[-1] === 0;
873
+ } // Notes about MathQuill
874
+ //
875
+ // MathQuill's stores its layout as nested linked lists. Each node in the
876
+ // list has this.MQ.L '-1' and this.MQ.R '1' properties that define links to
877
+ // the left and right nodes respectively. They also have
878
+ //
879
+ // ctrlSeq: contains the latex code snippet that defines that node.
880
+ // jQ: jQuery object for the DOM node(s) for this MathQuill node.
881
+ // ends: pointers to the nodes at the ends of the container.
882
+ // parent: parent node.
883
+ // blocks: an array containing one or more nodes that make up the node.
884
+ // sub?: subscript node if there is one as is the case in log_n
885
+ //
886
+ // All of the code below is super fragile. Please be especially careful
887
+ // when upgrading MathQuill.
833
888
 
834
- _defineProperty(CursorHandle, "propTypes", {
835
- animateIntoPosition: PropTypes__default["default"].bool,
836
- onTouchCancel: PropTypes__default["default"].func.isRequired,
837
- onTouchEnd: PropTypes__default["default"].func.isRequired,
838
- onTouchMove: PropTypes__default["default"].func.isRequired,
839
- onTouchStart: PropTypes__default["default"].func.isRequired,
840
- visible: PropTypes__default["default"].bool.isRequired,
841
- x: PropTypes__default["default"].number.isRequired,
842
- y: PropTypes__default["default"].number.isRequired
843
- });
844
889
 
845
- _defineProperty(CursorHandle, "defaultProps", {
846
- animateIntoPosition: false,
847
- visible: false,
848
- x: 0,
849
- y: 0
850
- });
890
+ _handleBackspaceInNthRoot(cursor) {
891
+ const isAtLeftEnd = cursor[this.MQ.L] === MQ_END;
851
892
 
852
- /**
853
- * A gesture recognizer that detects 'drags', crudely defined as either scrolls
854
- * or touches that move a sufficient distance.
855
- */
856
- // The 'slop' factor, after which we consider the use to be dragging. The value
857
- // is taken from the Android SDK. It won't be robust to page zoom and the like,
858
- // but it should be good enough for our purposes.
859
- const touchSlopPx = 8;
893
+ const isRootEmpty = this._isInsideEmptyNode(cursor.parent.parent.blocks[0].ends);
860
894
 
861
- class DragListener {
862
- constructor(onDrag, initialEvent) {
863
- // We detect drags in two ways. First, by listening for the window
864
- // scroll event (we consider any legitimate scroll to be a drag).
865
- this._scrollListener = () => {
866
- onDrag();
867
- }; // And second, by listening for touch moves and tracking the each
868
- // finger's displacement. This allows us to track, e.g., when the user
869
- // scrolls within an individual view.
895
+ if (isAtLeftEnd) {
896
+ this._selectNode(cursor.parent.parent, cursor);
897
+
898
+ if (isRootEmpty) {
899
+ this.mathField.keystroke("Backspace");
900
+ }
901
+ } else {
902
+ this.mathField.keystroke("Backspace");
903
+ }
904
+ }
905
+ /**
906
+ * Advances the cursor to the next logical position.
907
+ *
908
+ * @param {cursor} cursor
909
+ * @private
910
+ */
870
911
 
871
912
 
872
- const touchLocationsById = {};
913
+ _handleJumpOut(cursor, key) {
914
+ const context = this.contextForCursor(cursor); // Validate that the current cursor context matches the key's intent.
873
915
 
874
- for (let i = 0; i < initialEvent.changedTouches.length; i++) {
875
- const touch = initialEvent.changedTouches[i];
876
- touchLocationsById[touch.identifier] = [touch.clientX, touch.clientY];
916
+ if (KeysForJumpContext[context] !== key) {
917
+ // If we don't have a valid cursor context, yet the user was able
918
+ // to trigger a jump-out key, that's a broken invariant. Rather
919
+ // than throw an error (which would kick the user out of the
920
+ // exercise), we do nothing, as a fallback strategy. The user can
921
+ // still move the cursor manually.
922
+ return;
877
923
  }
878
924
 
879
- this._moveListener = evt => {
880
- for (let i = 0; i < evt.changedTouches.length; i++) {
881
- const touch = evt.changedTouches[i];
882
- const initialTouchLocation = touchLocationsById[touch.identifier];
925
+ switch (context) {
926
+ case IN_PARENS:
927
+ // Insert at the end of the parentheses, and then navigate right
928
+ // once more to get 'beyond' the parentheses.
929
+ cursor.insRightOf(cursor.parent.parent);
930
+ break;
883
931
 
884
- if (initialTouchLocation) {
885
- const touchLocation = [touch.clientX, touch.clientY];
886
- const dx = touchLocation[0] - initialTouchLocation[0];
887
- const dy = touchLocation[1] - initialTouchLocation[1];
888
- const squaredDist = dx * dx + dy * dy;
889
- const squaredTouchSlop = touchSlopPx * touchSlopPx;
932
+ case BEFORE_FRACTION:
933
+ // Find the nearest fraction to the right of the cursor.
934
+ let fractionNode;
935
+ let visitor = cursor;
890
936
 
891
- if (squaredDist > squaredTouchSlop) {
892
- onDrag();
937
+ while (visitor[this.MQ.R] !== MQ_END) {
938
+ if (this._isFraction(visitor[this.MQ.R])) {
939
+ fractionNode = visitor[this.MQ.R];
893
940
  }
894
- }
895
- }
896
- }; // Clean-up any terminated gestures, since some browsers reuse
897
- // identifiers.
898
-
899
941
 
900
- this._endAndCancelListener = evt => {
901
- for (let i = 0; i < evt.changedTouches.length; i++) {
902
- delete touchLocationsById[evt.changedTouches[i].identifier];
903
- }
904
- };
905
- }
942
+ visitor = visitor[this.MQ.R];
943
+ } // Jump into it!
906
944
 
907
- attach() {
908
- window.addEventListener("scroll", this._scrollListener);
909
- window.addEventListener("touchmove", this._moveListener);
910
- window.addEventListener("touchend", this._endAndCancelListener);
911
- window.addEventListener("touchcancel", this._endAndCancelListener);
912
- }
913
945
 
914
- detach() {
915
- window.removeEventListener("scroll", this._scrollListener);
916
- window.removeEventListener("touchmove", this._moveListener);
917
- window.removeEventListener("touchend", this._endAndCancelListener);
918
- window.removeEventListener("touchcancel", this._endAndCancelListener);
919
- }
946
+ cursor.insLeftOf(fractionNode);
947
+ this.mathField.keystroke("Right");
948
+ break;
920
949
 
921
- }
950
+ case IN_NUMERATOR:
951
+ // HACK(charlie): I can't find a better way to do this. The goal
952
+ // is to place the cursor at the start of the matching
953
+ // denominator. So, we identify the appropriate node, and
954
+ // continue rightwards until we find ourselves inside of it.
955
+ // It's possible that there are cases in which we don't reach
956
+ // the denominator, though I can't think of any.
957
+ const siblingDenominator = cursor.parent.parent.blocks[1];
922
958
 
923
- /**
924
- * This file contains a wrapper around MathQuill so that we can provide a
925
- * more regular interface for the functionality we need while insulating us
926
- * from MathQuill changes.
927
- */
928
- // If it does not exist, fall back to CommonJS require. - jsatk
929
-
930
- const decimalSymbol = decimalSeparator === DecimalSeparators.COMMA ? "," : ".";
931
- const WRITE = "write";
932
- const CMD = "cmd";
933
- const KEYSTROKE = "keystroke";
934
- const MQ_END = 0; // A mapping from keys that can be pressed on a keypad to the way in which
935
- // MathQuill should modify its input in response to that key-press. Any keys
936
- // that do not provide explicit actions (like the numeral keys) will merely
937
- // write their contents to MathQuill.
959
+ while (cursor.parent !== siblingDenominator) {
960
+ this.mathField.keystroke("Right");
961
+ }
938
962
 
939
- const KeyActions = {
940
- [Keys.PLUS]: {
941
- str: "+",
942
- fn: WRITE
943
- },
944
- [Keys.MINUS]: {
945
- str: "-",
946
- fn: WRITE
947
- },
948
- [Keys.NEGATIVE]: {
949
- str: "-",
950
- fn: WRITE
951
- },
952
- [Keys.TIMES]: {
953
- str: "\\times",
954
- fn: WRITE
955
- },
956
- [Keys.DIVIDE]: {
957
- str: "\\div",
958
- fn: WRITE
959
- },
960
- [Keys.DECIMAL]: {
961
- str: decimalSymbol,
962
- fn: WRITE
963
- },
964
- [Keys.EQUAL]: {
965
- str: "=",
966
- fn: WRITE
967
- },
968
- [Keys.NEQ]: {
969
- str: "\\neq",
970
- fn: WRITE
971
- },
972
- [Keys.CDOT]: {
973
- str: "\\cdot",
974
- fn: WRITE
975
- },
976
- [Keys.PERCENT]: {
977
- str: "%",
978
- fn: WRITE
979
- },
980
- [Keys.LEFT_PAREN]: {
981
- str: "(",
982
- fn: CMD
983
- },
984
- [Keys.RIGHT_PAREN]: {
985
- str: ")",
986
- fn: CMD
987
- },
988
- [Keys.SQRT]: {
989
- str: "sqrt",
990
- fn: CMD
991
- },
992
- [Keys.PI]: {
993
- str: "pi",
994
- fn: CMD
995
- },
996
- [Keys.THETA]: {
997
- str: "theta",
998
- fn: CMD
999
- },
1000
- [Keys.RADICAL]: {
1001
- str: "nthroot",
1002
- fn: CMD
1003
- },
1004
- [Keys.LT]: {
1005
- str: "<",
1006
- fn: WRITE
1007
- },
1008
- [Keys.LEQ]: {
1009
- str: "\\leq",
1010
- fn: WRITE
1011
- },
1012
- [Keys.GT]: {
1013
- str: ">",
1014
- fn: WRITE
1015
- },
1016
- [Keys.GEQ]: {
1017
- str: "\\geq",
1018
- fn: WRITE
1019
- },
1020
- [Keys.UP]: {
1021
- str: "Up",
1022
- fn: KEYSTROKE
1023
- },
1024
- [Keys.DOWN]: {
1025
- str: "Down",
1026
- fn: KEYSTROKE
1027
- },
1028
- // The `FRAC_EXCLUSIVE` variant is handled manually, since we may need to do
1029
- // some additional navigation depending on the cursor position.
1030
- [Keys.FRAC_INCLUSIVE]: {
1031
- str: "/",
1032
- fn: CMD
1033
- }
1034
- };
1035
- const NormalCommands = {
1036
- [Keys.LOG]: "log",
1037
- [Keys.LN]: "ln",
1038
- [Keys.SIN]: "sin",
1039
- [Keys.COS]: "cos",
1040
- [Keys.TAN]: "tan"
1041
- };
1042
- const ArithmeticOperators = ["+", "-", "\\cdot", "\\times", "\\div"];
1043
- const EqualityOperators = ["=", "\\neq", "<", "\\leq", ">", "\\geq"];
1044
- const Numerals = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
1045
- const GreekLetters = ["\\theta", "\\pi"];
1046
- const Letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]; // We only consider numerals, variables, and Greek Letters to be proper
1047
- // leaf nodes.
963
+ break;
1048
964
 
1049
- const ValidLeaves = [...Numerals, ...GreekLetters, ...Letters.map(letter => letter.toLowerCase()), ...Letters.map(letter => letter.toUpperCase())];
1050
- const KeysForJumpContext = {
1051
- [IN_PARENS]: Keys.JUMP_OUT_PARENTHESES,
1052
- [IN_SUPER_SCRIPT]: Keys.JUMP_OUT_EXPONENT,
1053
- [IN_SUB_SCRIPT]: Keys.JUMP_OUT_BASE,
1054
- [BEFORE_FRACTION]: Keys.JUMP_INTO_NUMERATOR,
1055
- [IN_NUMERATOR]: Keys.JUMP_OUT_NUMERATOR,
1056
- [IN_DENOMINATOR]: Keys.JUMP_OUT_DENOMINATOR
1057
- };
965
+ case IN_DENOMINATOR:
966
+ cursor.insRightOf(cursor.parent.parent);
967
+ break;
1058
968
 
1059
- class MathWrapper {
1060
- constructor(element) {
1061
- let callbacks = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1062
- this.MQ = MathQuill__default["default"].getInterface(2);
1063
- this.mathField = this.MQ.MathField(element, {
1064
- // use a span instead of a textarea so that we don't bring up the
1065
- // native keyboard on mobile when selecting the input
1066
- substituteTextarea: function () {
1067
- return document.createElement("span");
1068
- }
1069
- });
1070
- this.callbacks = callbacks;
1071
- }
969
+ case IN_SUB_SCRIPT:
970
+ // Insert just beyond the superscript.
971
+ cursor.insRightOf(cursor.parent.parent); // Navigate right once more, if we're right before parens. This
972
+ // is to handle the standard case in which the subscript is the
973
+ // base of a custom log.
1072
974
 
1073
- focus() {
1074
- // HACK(charlie): We shouldn't reaching into MathQuill internals like
1075
- // this, but it's the easiest way to allow us to manage the focus state
1076
- // ourselves.
1077
- const controller = this.mathField.__controller;
1078
- controller.cursor.show(); // Set MathQuill's internal state to reflect the focus, otherwise it
1079
- // will consistently try to hide the cursor on key-press and introduce
1080
- // layout jank.
975
+ if (this._isParens(cursor[this.MQ.R])) {
976
+ this.mathField.keystroke("Right");
977
+ }
1081
978
 
1082
- controller.blurred = false;
1083
- }
979
+ break;
1084
980
 
1085
- blur() {
1086
- const controller = this.mathField.__controller;
1087
- controller.cursor.hide();
1088
- controller.blurred = true;
1089
- }
981
+ case IN_SUPER_SCRIPT:
982
+ // Insert just beyond the superscript.
983
+ cursor.insRightOf(cursor.parent.parent);
984
+ break;
1090
985
 
1091
- _writeNormalFunction(name) {
1092
- this.mathField.write("\\".concat(name, "\\left(\\right)"));
1093
- this.mathField.keystroke("Left");
986
+ default:
987
+ throw new Error("Attempted to 'Jump Out' from node, but found no " + "appropriate cursor context: ".concat(context));
988
+ }
1094
989
  }
1095
990
  /**
1096
- * Handle a key press and return the resulting cursor state.
991
+ * Selects and deletes part of the expression based on the cursor location.
992
+ * See inline comments for precise behavior of different cases.
1097
993
  *
1098
- * @param {Key} key - an enum representing the key that was pressed
1099
- * @returns {object} a cursor object, consisting of a cursor context
994
+ * @param {cursor} cursor
995
+ * @private
1100
996
  */
1101
997
 
1102
998
 
1103
- pressKey(key) {
1104
- const cursor = this.mathField.__controller.cursor;
1105
-
1106
- if (key in KeyActions) {
1107
- const {
1108
- str,
1109
- fn
1110
- } = KeyActions[key];
1111
-
1112
- if (str && fn) {
1113
- this.mathField[fn](str);
1114
- }
1115
- } else if (Object.keys(NormalCommands).includes(key)) {
1116
- this._writeNormalFunction(NormalCommands[key]);
1117
- } else if (key === Keys.FRAC_EXCLUSIVE) {
1118
- // If there's nothing to the left of the cursor, then we want to
1119
- // leave the cursor to the left of the fraction after creating it.
1120
- const shouldNavigateLeft = cursor[this.MQ.L] === MQ_END;
1121
- this.mathField.cmd("\\frac");
999
+ _handleBackspace(cursor) {
1000
+ if (!cursor.selection) {
1001
+ const parent = cursor.parent;
1002
+ const grandparent = parent.parent;
1003
+ const leftNode = cursor[this.MQ.L];
1122
1004
 
1123
- if (shouldNavigateLeft) {
1124
- this.mathField.keystroke("Left");
1005
+ if (this._isFraction(leftNode)) {
1006
+ this._selectNode(leftNode, cursor);
1007
+ } else if (this._isSquareRoot(leftNode)) {
1008
+ this._selectNode(leftNode, cursor);
1009
+ } else if (this._isNthRoot(leftNode)) {
1010
+ this._selectNode(leftNode, cursor);
1011
+ } else if (this._isNthRootIndex(parent)) {
1012
+ this._handleBackspaceInRootIndex(cursor);
1013
+ } else if (leftNode.ctrlSeq === "\\left(") {
1014
+ this._handleBackspaceOutsideParens(cursor);
1015
+ } else if (grandparent.ctrlSeq === "\\left(") {
1016
+ this._handleBackspaceInsideParens(cursor);
1017
+ } else if (this._isInsideLogIndex(cursor)) {
1018
+ this._handleBackspaceInLogIndex(cursor);
1019
+ } else if (leftNode.ctrlSeq === "\\ge " || leftNode.ctrlSeq === "\\le ") {
1020
+ this._handleBackspaceAfterLigaturedSymbol(cursor);
1021
+ } else if (this._isNthRoot(grandparent) && leftNode === MQ_END) {
1022
+ this._handleBackspaceInNthRoot(cursor);
1023
+ } else {
1024
+ this.mathField.keystroke("Backspace");
1125
1025
  }
1126
- } else if (key === Keys.FRAC) {
1127
- // eslint-disable-next-line no-unused-vars
1128
- cursor[this.MQ.L] === MQ_END;
1129
- this.mathField.cmd("\\frac");
1130
- } else if (key === Keys.LOG_N) {
1131
- this.mathField.write("log_{ }\\left(\\right)");
1132
- this.mathField.keystroke("Left"); // into parentheses
1133
-
1134
- this.mathField.keystroke("Left"); // out of parentheses
1026
+ } else {
1027
+ this.mathField.keystroke("Backspace");
1028
+ }
1029
+ }
1135
1030
 
1136
- this.mathField.keystroke("Left"); // into index
1137
- } else if (key === Keys.CUBE_ROOT) {
1138
- this.mathField.write("\\sqrt[3]{}");
1139
- this.mathField.keystroke("Left"); // under the root
1140
- } else if (key === Keys.EXP || key === Keys.EXP_2 || key === Keys.EXP_3) {
1141
- this._handleExponent(cursor, key);
1142
- } else if (key === Keys.JUMP_OUT_PARENTHESES || key === Keys.JUMP_OUT_EXPONENT || key === Keys.JUMP_OUT_BASE || key === Keys.JUMP_INTO_NUMERATOR || key === Keys.JUMP_OUT_NUMERATOR || key === Keys.JUMP_OUT_DENOMINATOR) {
1143
- this._handleJumpOut(cursor, key);
1144
- } else if (key === Keys.BACKSPACE) {
1145
- this._handleBackspace(cursor);
1146
- } else if (key === Keys.LEFT) {
1147
- this._handleLeftArrow(cursor);
1148
- } else if (key === Keys.RIGHT || key === Keys.JUMP_OUT) {
1149
- this._handleRightArrow(cursor);
1150
- } else if (/^[a-zA-Z]$/.test(key)) {
1151
- this.mathField[WRITE](key);
1152
- } else if (/^NUM_\d/.test(key)) {
1153
- this.mathField[WRITE](key[4]);
1154
- }
1155
-
1156
- if (!cursor.selection) {
1157
- // don't show the cursor for selections
1158
- cursor.show();
1159
- }
1160
-
1161
- if (this.callbacks.onSelectionChanged) {
1162
- this.callbacks.onSelectionChanged(cursor.selection);
1163
- } // NOTE(charlie): It's insufficient to do this as an `edited` handler
1164
- // on the MathField, as that handler isn't triggered on navigation
1165
- // events.
1166
-
1167
-
1168
- return {
1169
- context: this.contextForCursor(cursor)
1170
- };
1171
- }
1172
- /**
1173
- * Place the cursor beside the node located at the given coordinates.
1174
- *
1175
- * @param {number} x - the x coordinate in the viewport
1176
- * @param {number} y - the y coordinate in the viewport
1177
- * @param {Node} hitNode - the node next to which the cursor should be
1178
- * placed; if provided, the coordinates will be used
1179
- * to determine on which side of the node the cursor
1180
- * should be placed
1181
- */
1182
-
1183
-
1184
- setCursorPosition(x, y, hitNode) {
1185
- const el = hitNode || document.elementFromPoint(x, y);
1186
-
1187
- if (el) {
1188
- const cursor = this.getCursor();
1189
-
1190
- if (el.hasAttribute("mq-root-block")) {
1191
- // If we're in the empty area place the cursor at the right
1192
- // end of the expression.
1193
- cursor.insAtRightEnd(this.mathField.__controller.root);
1194
- } else {
1195
- // Otherwise place beside the element at x, y.
1196
- const controller = this.mathField.__controller;
1197
- const pageX = x - document.body.scrollLeft;
1198
- const pageY = y - document.body.scrollTop;
1199
- controller.seek($__default["default"](el), pageX, pageY).cursor.startSelection(); // Unless that would leave us mid-command, in which case, we
1200
- // need to adjust and place the cursor inside the parens
1201
- // following the command.
1031
+ _handleLeftArrow(cursor) {
1032
+ // If we're inside a function, and just after the left parentheses, we
1033
+ // need to skip the entire function name, rather than move the cursor
1034
+ // inside of it. For example, when hitting left from within the
1035
+ // parentheses in `cos()`, we want to place the cursor to the left of
1036
+ // the entire expression, rather than between the `s` and the left
1037
+ // parenthesis.
1038
+ // From the cursor's perspective, this requires that our left node is
1039
+ // the MQ_END node, that our grandparent is the left parenthesis, and
1040
+ // the nodes to the left of our grandparent comprise a valid function
1041
+ // name.
1042
+ if (cursor[this.MQ.L] === MQ_END) {
1043
+ const parent = cursor.parent;
1044
+ const grandparent = parent.parent;
1202
1045
 
1203
- const command = this._maybeFindCommand(cursor[this.MQ.L]);
1046
+ if (grandparent.ctrlSeq === "\\left(") {
1047
+ const command = this._maybeFindCommandBeforeParens(grandparent);
1204
1048
 
1205
- if (command && command.endNode) {
1206
- // NOTE(charlie): endNode should definitely be \left(.
1207
- cursor.insLeftOf(command.endNode);
1208
- this.mathField.keystroke("Right");
1049
+ if (command) {
1050
+ cursor.insLeftOf(command.startNode);
1051
+ return;
1209
1052
  }
1210
1053
  }
1054
+ } // Otherwise, we default to the standard MathQull left behavior.
1211
1055
 
1212
- if (this.callbacks.onCursorMove) {
1213
- this.callbacks.onCursorMove({
1214
- context: this.contextForCursor(cursor)
1215
- });
1216
- }
1217
- }
1218
- }
1219
1056
 
1220
- getCursor() {
1221
- return this.mathField.__controller.cursor;
1057
+ this.mathField.keystroke("Left");
1222
1058
  }
1223
1059
 
1224
- getSelection() {
1225
- return this.getCursor().selection;
1226
- }
1060
+ _handleRightArrow(cursor) {
1061
+ const command = this._maybeFindCommand(cursor[this.MQ.R]);
1227
1062
 
1228
- getContent() {
1229
- return this.mathField.latex();
1063
+ if (command) {
1064
+ // Similarly, if a function is to our right, then we need to place
1065
+ // the cursor at the start of its parenthetical content, which is
1066
+ // done by putting it to the left of ites parentheses and then
1067
+ // moving right once.
1068
+ cursor.insLeftOf(command.endNode);
1069
+ this.mathField.keystroke("Right");
1070
+ } else {
1071
+ // Otherwise, we default to the standard MathQull right behavior.
1072
+ this.mathField.keystroke("Right");
1073
+ }
1230
1074
  }
1231
1075
 
1232
- setContent(latex) {
1233
- this.mathField.latex(latex);
1234
- }
1076
+ _handleExponent(cursor, key) {
1077
+ // If there's an invalid operator preceding the cursor (anything that
1078
+ // knowingly cannot be raised to a power), add an empty set of
1079
+ // parentheses and apply the exponent to that.
1080
+ const invalidPrefixes = [...ArithmeticOperators, ...EqualityOperators];
1081
+ const precedingNode = cursor[this.MQ.L];
1082
+ const shouldPrefixWithParens = precedingNode === MQ_END || invalidPrefixes.includes(precedingNode.ctrlSeq.trim());
1235
1083
 
1236
- isEmpty() {
1237
- const cursor = this.getCursor();
1238
- return cursor.parent.id === 1 && cursor[1] === 0 && cursor[-1] === 0;
1239
- } // Notes about MathQuill
1240
- //
1241
- // MathQuill's stores its layout as nested linked lists. Each node in the
1242
- // list has this.MQ.L '-1' and this.MQ.R '1' properties that define links to
1243
- // the left and right nodes respectively. They also have
1244
- //
1245
- // ctrlSeq: contains the latex code snippet that defines that node.
1246
- // jQ: jQuery object for the DOM node(s) for this MathQuill node.
1247
- // ends: pointers to the nodes at the ends of the container.
1248
- // parent: parent node.
1249
- // blocks: an array containing one or more nodes that make up the node.
1250
- // sub?: subscript node if there is one as is the case in log_n
1251
- //
1252
- // All of the code below is super fragile. Please be especially careful
1253
- // when upgrading MathQuill.
1084
+ if (shouldPrefixWithParens) {
1085
+ this.mathField.write("\\left(\\right)");
1086
+ } // Insert the appropriate exponent operator.
1254
1087
 
1255
1088
 
1256
- _handleBackspaceInNthRoot(cursor) {
1257
- const isAtLeftEnd = cursor[this.MQ.L] === MQ_END;
1089
+ switch (key) {
1090
+ case Keys.EXP:
1091
+ this.mathField.cmd("^");
1092
+ break;
1258
1093
 
1259
- const isRootEmpty = this._isInsideEmptyNode(cursor.parent.parent.blocks[0].ends);
1094
+ case Keys.EXP_2:
1095
+ case Keys.EXP_3:
1096
+ this.mathField.write("^".concat(key === Keys.EXP_2 ? 2 : 3)); // If we enter a square or a cube, we should leave the cursor
1097
+ // within the newly inserted parens, if they exist. This takes
1098
+ // exactly four left strokes, since the cursor by default would
1099
+ // end up to the right of the exponent.
1260
1100
 
1261
- if (isAtLeftEnd) {
1262
- this._selectNode(cursor.parent.parent, cursor);
1101
+ if (shouldPrefixWithParens) {
1102
+ this.mathField.keystroke("Left");
1103
+ this.mathField.keystroke("Left");
1104
+ this.mathField.keystroke("Left");
1105
+ this.mathField.keystroke("Left");
1106
+ }
1263
1107
 
1264
- if (isRootEmpty) {
1265
- this.mathField.keystroke("Backspace");
1266
- }
1267
- } else {
1268
- this.mathField.keystroke("Backspace");
1108
+ break;
1109
+
1110
+ default:
1111
+ throw new Error("Invalid exponent key: ".concat(key));
1269
1112
  }
1270
1113
  }
1271
1114
  /**
1272
- * Advances the cursor to the next logical position.
1115
+ * Return the start node, end node, and full name of the command of which
1116
+ * the initial node is a part, or `null` if the node is not part of a
1117
+ * command.
1273
1118
  *
1274
- * @param {cursor} cursor
1119
+ * @param {node} initialNode - the node to included as part of the command
1120
+ * @returns {null|object} - `null` or an object containing the start node
1121
+ * (`startNode`), end node (`endNode`), and full
1122
+ * name (`name`) of the command
1275
1123
  * @private
1276
1124
  */
1277
1125
 
1278
1126
 
1279
- _handleJumpOut(cursor, key) {
1280
- const context = this.contextForCursor(cursor); // Validate that the current cursor context matches the key's intent.
1281
-
1282
- if (KeysForJumpContext[context] !== key) {
1283
- // If we don't have a valid cursor context, yet the user was able
1284
- // to trigger a jump-out key, that's a broken invariant. Rather
1285
- // than throw an error (which would kick the user out of the
1286
- // exercise), we do nothing, as a fallback strategy. The user can
1287
- // still move the cursor manually.
1288
- return;
1289
- }
1127
+ _maybeFindCommand(initialNode) {
1128
+ if (!initialNode) {
1129
+ return null;
1130
+ } // MathQuill stores commands as separate characters so that
1131
+ // users can delete commands one character at a time. We iterate over
1132
+ // the nodes from right to left until we hit a sequence starting with a
1133
+ // '\\', which signifies the start of a command; then we iterate from
1134
+ // left to right until we hit a '\\left(', which signifies the end of a
1135
+ // command. If we encounter any character that doesn't belong in a
1136
+ // command, we return null. We match a single character at a time.
1137
+ // Ex) ['\\l', 'o', 'g ', '\\left(', ...]
1290
1138
 
1291
- switch (context) {
1292
- case IN_PARENS:
1293
- // Insert at the end of the parentheses, and then navigate right
1294
- // once more to get 'beyond' the parentheses.
1295
- cursor.insRightOf(cursor.parent.parent);
1296
- break;
1297
1139
 
1298
- case BEFORE_FRACTION:
1299
- // Find the nearest fraction to the right of the cursor.
1300
- let fractionNode;
1301
- let visitor = cursor;
1140
+ const commandCharRegex = /^[a-z]$/;
1141
+ const commandStartRegex = /^\\[a-z]$/;
1142
+ const commandEndSeq = "\\left("; // Note: We allowlist the set of valid commands, since relying solely on
1143
+ // a command being prefixed with a backslash leads to undesired
1144
+ // behavior. For example, Greek symbols, left parentheses, and square
1145
+ // roots all get treated as commands.
1302
1146
 
1303
- while (visitor[this.MQ.R] !== MQ_END) {
1304
- if (this._isFraction(visitor[this.MQ.R])) {
1305
- fractionNode = visitor[this.MQ.R];
1306
- }
1147
+ const validCommands = ["\\log", "\\ln", "\\cos", "\\sin", "\\tan"];
1148
+ let name = "";
1149
+ let startNode;
1150
+ let endNode; // Collect the portion of the command from the current node, leftwards
1151
+ // until the start of the command.
1307
1152
 
1308
- visitor = visitor[this.MQ.R];
1309
- } // Jump into it!
1153
+ let node = initialNode;
1310
1154
 
1155
+ while (node !== 0) {
1156
+ const ctrlSeq = node.ctrlSeq.trim();
1311
1157
 
1312
- cursor.insLeftOf(fractionNode);
1313
- this.mathField.keystroke("Right");
1158
+ if (commandCharRegex.test(ctrlSeq)) {
1159
+ name = ctrlSeq + name;
1160
+ } else if (commandStartRegex.test(ctrlSeq)) {
1161
+ name = ctrlSeq + name;
1162
+ startNode = node;
1163
+ break;
1164
+ } else {
1314
1165
  break;
1315
-
1316
- case IN_NUMERATOR:
1317
- // HACK(charlie): I can't find a better way to do this. The goal
1318
- // is to place the cursor at the start of the matching
1319
- // denominator. So, we identify the appropriate node, and
1320
- // continue rightwards until we find ourselves inside of it.
1321
- // It's possible that there are cases in which we don't reach
1322
- // the denominator, though I can't think of any.
1323
- const siblingDenominator = cursor.parent.parent.blocks[1];
1324
-
1325
- while (cursor.parent !== siblingDenominator) {
1326
- this.mathField.keystroke("Right");
1327
- }
1328
-
1329
- break;
1330
-
1331
- case IN_DENOMINATOR:
1332
- cursor.insRightOf(cursor.parent.parent);
1333
- break;
1334
-
1335
- case IN_SUB_SCRIPT:
1336
- // Insert just beyond the superscript.
1337
- cursor.insRightOf(cursor.parent.parent); // Navigate right once more, if we're right before parens. This
1338
- // is to handle the standard case in which the subscript is the
1339
- // base of a custom log.
1340
-
1341
- if (this._isParens(cursor[this.MQ.R])) {
1342
- this.mathField.keystroke("Right");
1343
- }
1344
-
1345
- break;
1346
-
1347
- case IN_SUPER_SCRIPT:
1348
- // Insert just beyond the superscript.
1349
- cursor.insRightOf(cursor.parent.parent);
1350
- break;
1351
-
1352
- default:
1353
- throw new Error("Attempted to 'Jump Out' from node, but found no " + "appropriate cursor context: ".concat(context));
1354
- }
1355
- }
1356
- /**
1357
- * Selects and deletes part of the expression based on the cursor location.
1358
- * See inline comments for precise behavior of different cases.
1359
- *
1360
- * @param {cursor} cursor
1361
- * @private
1362
- */
1363
-
1364
-
1365
- _handleBackspace(cursor) {
1366
- if (!cursor.selection) {
1367
- const parent = cursor.parent;
1368
- const grandparent = parent.parent;
1369
- const leftNode = cursor[this.MQ.L];
1370
-
1371
- if (this._isFraction(leftNode)) {
1372
- this._selectNode(leftNode, cursor);
1373
- } else if (this._isSquareRoot(leftNode)) {
1374
- this._selectNode(leftNode, cursor);
1375
- } else if (this._isNthRoot(leftNode)) {
1376
- this._selectNode(leftNode, cursor);
1377
- } else if (this._isNthRootIndex(parent)) {
1378
- this._handleBackspaceInRootIndex(cursor);
1379
- } else if (leftNode.ctrlSeq === "\\left(") {
1380
- this._handleBackspaceOutsideParens(cursor);
1381
- } else if (grandparent.ctrlSeq === "\\left(") {
1382
- this._handleBackspaceInsideParens(cursor);
1383
- } else if (this._isInsideLogIndex(cursor)) {
1384
- this._handleBackspaceInLogIndex(cursor);
1385
- } else if (leftNode.ctrlSeq === "\\ge " || leftNode.ctrlSeq === "\\le ") {
1386
- this._handleBackspaceAfterLigaturedSymbol(cursor);
1387
- } else if (this._isNthRoot(grandparent) && leftNode === MQ_END) {
1388
- this._handleBackspaceInNthRoot(cursor);
1389
- } else {
1390
- this.mathField.keystroke("Backspace");
1391
- }
1392
- } else {
1393
- this.mathField.keystroke("Backspace");
1394
- }
1395
- }
1396
-
1397
- _handleLeftArrow(cursor) {
1398
- // If we're inside a function, and just after the left parentheses, we
1399
- // need to skip the entire function name, rather than move the cursor
1400
- // inside of it. For example, when hitting left from within the
1401
- // parentheses in `cos()`, we want to place the cursor to the left of
1402
- // the entire expression, rather than between the `s` and the left
1403
- // parenthesis.
1404
- // From the cursor's perspective, this requires that our left node is
1405
- // the MQ_END node, that our grandparent is the left parenthesis, and
1406
- // the nodes to the left of our grandparent comprise a valid function
1407
- // name.
1408
- if (cursor[this.MQ.L] === MQ_END) {
1409
- const parent = cursor.parent;
1410
- const grandparent = parent.parent;
1411
-
1412
- if (grandparent.ctrlSeq === "\\left(") {
1413
- const command = this._maybeFindCommandBeforeParens(grandparent);
1414
-
1415
- if (command) {
1416
- cursor.insLeftOf(command.startNode);
1417
- return;
1418
- }
1419
- }
1420
- } // Otherwise, we default to the standard MathQull left behavior.
1421
-
1422
-
1423
- this.mathField.keystroke("Left");
1424
- }
1425
-
1426
- _handleRightArrow(cursor) {
1427
- const command = this._maybeFindCommand(cursor[this.MQ.R]);
1428
-
1429
- if (command) {
1430
- // Similarly, if a function is to our right, then we need to place
1431
- // the cursor at the start of its parenthetical content, which is
1432
- // done by putting it to the left of ites parentheses and then
1433
- // moving right once.
1434
- cursor.insLeftOf(command.endNode);
1435
- this.mathField.keystroke("Right");
1436
- } else {
1437
- // Otherwise, we default to the standard MathQull right behavior.
1438
- this.mathField.keystroke("Right");
1439
- }
1440
- }
1441
-
1442
- _handleExponent(cursor, key) {
1443
- // If there's an invalid operator preceding the cursor (anything that
1444
- // knowingly cannot be raised to a power), add an empty set of
1445
- // parentheses and apply the exponent to that.
1446
- const invalidPrefixes = [...ArithmeticOperators, ...EqualityOperators];
1447
- const precedingNode = cursor[this.MQ.L];
1448
- const shouldPrefixWithParens = precedingNode === MQ_END || invalidPrefixes.includes(precedingNode.ctrlSeq.trim());
1449
-
1450
- if (shouldPrefixWithParens) {
1451
- this.mathField.write("\\left(\\right)");
1452
- } // Insert the appropriate exponent operator.
1453
-
1454
-
1455
- switch (key) {
1456
- case Keys.EXP:
1457
- this.mathField.cmd("^");
1458
- break;
1459
-
1460
- case Keys.EXP_2:
1461
- case Keys.EXP_3:
1462
- this.mathField.write("^".concat(key === Keys.EXP_2 ? 2 : 3)); // If we enter a square or a cube, we should leave the cursor
1463
- // within the newly inserted parens, if they exist. This takes
1464
- // exactly four left strokes, since the cursor by default would
1465
- // end up to the right of the exponent.
1466
-
1467
- if (shouldPrefixWithParens) {
1468
- this.mathField.keystroke("Left");
1469
- this.mathField.keystroke("Left");
1470
- this.mathField.keystroke("Left");
1471
- this.mathField.keystroke("Left");
1472
- }
1473
-
1474
- break;
1475
-
1476
- default:
1477
- throw new Error("Invalid exponent key: ".concat(key));
1478
- }
1479
- }
1480
- /**
1481
- * Return the start node, end node, and full name of the command of which
1482
- * the initial node is a part, or `null` if the node is not part of a
1483
- * command.
1484
- *
1485
- * @param {node} initialNode - the node to included as part of the command
1486
- * @returns {null|object} - `null` or an object containing the start node
1487
- * (`startNode`), end node (`endNode`), and full
1488
- * name (`name`) of the command
1489
- * @private
1490
- */
1491
-
1492
-
1493
- _maybeFindCommand(initialNode) {
1494
- if (!initialNode) {
1495
- return null;
1496
- } // MathQuill stores commands as separate characters so that
1497
- // users can delete commands one character at a time. We iterate over
1498
- // the nodes from right to left until we hit a sequence starting with a
1499
- // '\\', which signifies the start of a command; then we iterate from
1500
- // left to right until we hit a '\\left(', which signifies the end of a
1501
- // command. If we encounter any character that doesn't belong in a
1502
- // command, we return null. We match a single character at a time.
1503
- // Ex) ['\\l', 'o', 'g ', '\\left(', ...]
1504
-
1505
-
1506
- const commandCharRegex = /^[a-z]$/;
1507
- const commandStartRegex = /^\\[a-z]$/;
1508
- const commandEndSeq = "\\left("; // Note: We allowlist the set of valid commands, since relying solely on
1509
- // a command being prefixed with a backslash leads to undesired
1510
- // behavior. For example, Greek symbols, left parentheses, and square
1511
- // roots all get treated as commands.
1512
-
1513
- const validCommands = ["\\log", "\\ln", "\\cos", "\\sin", "\\tan"];
1514
- let name = "";
1515
- let startNode;
1516
- let endNode; // Collect the portion of the command from the current node, leftwards
1517
- // until the start of the command.
1518
-
1519
- let node = initialNode;
1520
-
1521
- while (node !== 0) {
1522
- const ctrlSeq = node.ctrlSeq.trim();
1523
-
1524
- if (commandCharRegex.test(ctrlSeq)) {
1525
- name = ctrlSeq + name;
1526
- } else if (commandStartRegex.test(ctrlSeq)) {
1527
- name = ctrlSeq + name;
1528
- startNode = node;
1529
- break;
1530
- } else {
1531
- break;
1532
- }
1166
+ }
1533
1167
 
1534
1168
  node = node[this.MQ.L];
1535
1169
  } // If we hit the start of a command, then grab the rest of it by
@@ -1938,12 +1572,39 @@ const scrollIntoView = (containerNode, keypadNode) => {
1938
1572
  }
1939
1573
  };
1940
1574
 
1941
- const constrainingFrictionFactor = 0.8; // eslint-disable-next-line react/no-unsafe
1575
+ const constrainingFrictionFactor = 0.8;
1942
1576
 
1577
+ // eslint-disable-next-line react/no-unsafe
1943
1578
  class MathInput extends React__namespace.Component {
1944
1579
  constructor() {
1945
1580
  super(...arguments);
1946
1581
 
1582
+ _defineProperty(this, "didTouchOutside", void 0);
1583
+
1584
+ _defineProperty(this, "didScroll", void 0);
1585
+
1586
+ _defineProperty(this, "mathField", void 0);
1587
+
1588
+ _defineProperty(this, "recordTouchStartOutside", void 0);
1589
+
1590
+ _defineProperty(this, "blurOnTouchEndOutside", void 0);
1591
+
1592
+ _defineProperty(this, "dragListener", void 0);
1593
+
1594
+ _defineProperty(this, "inputRef", void 0);
1595
+
1596
+ _defineProperty(this, "_isMounted", void 0);
1597
+
1598
+ _defineProperty(this, "_mathContainer", void 0);
1599
+
1600
+ _defineProperty(this, "_container", void 0);
1601
+
1602
+ _defineProperty(this, "_root", void 0);
1603
+
1604
+ _defineProperty(this, "_containerBounds", void 0);
1605
+
1606
+ _defineProperty(this, "_keypadBounds", void 0);
1607
+
1947
1608
  _defineProperty(this, "state", {
1948
1609
  focused: false,
1949
1610
  handle: {
@@ -1954,7 +1615,7 @@ class MathInput extends React__namespace.Component {
1954
1615
  }
1955
1616
  });
1956
1617
 
1957
- _defineProperty(this, "_clearKeypadBoundsCache", keypadNode => {
1618
+ _defineProperty(this, "_clearKeypadBoundsCache", () => {
1958
1619
  this._keypadBounds = null;
1959
1620
  });
1960
1621
 
@@ -2120,6 +1781,8 @@ class MathInput extends React__namespace.Component {
2120
1781
  const elementsById = {};
2121
1782
 
2122
1783
  for (const element of elements) {
1784
+ // $FlowFixMe[incompatible-use]
1785
+ // $FlowFixMe[prop-missing]
2123
1786
  const id = element.getAttribute("mathquill-command-id");
2124
1787
 
2125
1788
  if (id != null) {
@@ -2141,7 +1804,7 @@ class MathInput extends React__namespace.Component {
2141
1804
  // TODO(kevinb) consider preferring nodes hit by [x, y].
2142
1805
 
2143
1806
 
2144
- for (const [id, count] of Object.entries(counts)) {
1807
+ for (const [id, count] of wonderStuffCore.entries(counts)) {
2145
1808
  if (count > max) {
2146
1809
  max = count;
2147
1810
  hitNode = elementsById[id];
@@ -2456,7 +2119,8 @@ class MathInput extends React__namespace.Component {
2456
2119
  this._updateInputPadding();
2457
2120
 
2458
2121
  this._container = ReactDOM__default["default"].findDOMNode(this);
2459
- this._root = this._container.querySelector(".mq-root-block");
2122
+ this._root = this._container.querySelector(".mq-root-block"); // $FlowFixMe[incompatible-use]
2123
+ // $FlowFixMe[prop-missing]
2460
2124
 
2461
2125
  this._root.addEventListener("scroll", this._handleScroll); // Record the initial scroll displacement on touch start. This allows
2462
2126
  // us to detect whether a touch event was a scroll and only blur the
@@ -2464,8 +2128,6 @@ class MathInput extends React__namespace.Component {
2464
2128
  // frustrating user experience.
2465
2129
 
2466
2130
 
2467
- this.touchStartInitialScroll = null;
2468
-
2469
2131
  this.recordTouchStartOutside = evt => {
2470
2132
  if (this.state.focused) {
2471
2133
  // Only blur if the touch is both outside of the input, and
@@ -2524,165 +2186,499 @@ class MathInput extends React__namespace.Component {
2524
2186
 
2525
2187
  if (this.dragListener) {
2526
2188
  this.dragListener.detach();
2527
- this.removeListeners = null;
2528
2189
  }
2529
2190
  };
2530
2191
 
2531
- window.addEventListener("touchstart", this.recordTouchStartOutside);
2532
- window.addEventListener("touchend", this.blurOnTouchEndOutside);
2533
- window.addEventListener("touchcancel", this.blurOnTouchEndOutside); // HACK(benkomalo): if the window resizes, the keypad bounds can
2534
- // change. That's a bit peeking into the internals of the keypad
2535
- // itself, since we know bounds can change only when the viewport
2536
- // changes, but seems like a rare enough thing to get wrong that it's
2537
- // not worth wiring up extra things for the technical "purity" of
2538
- // having the keypad notify of changes to us.
2192
+ window.addEventListener("touchstart", this.recordTouchStartOutside);
2193
+ window.addEventListener("touchend", this.blurOnTouchEndOutside);
2194
+ window.addEventListener("touchcancel", this.blurOnTouchEndOutside); // HACK(benkomalo): if the window resizes, the keypad bounds can
2195
+ // change. That's a bit peeking into the internals of the keypad
2196
+ // itself, since we know bounds can change only when the viewport
2197
+ // changes, but seems like a rare enough thing to get wrong that it's
2198
+ // not worth wiring up extra things for the technical "purity" of
2199
+ // having the keypad notify of changes to us.
2200
+
2201
+ window.addEventListener("resize", this._clearKeypadBoundsCache);
2202
+ window.addEventListener("orientationchange", this._clearKeypadBoundsCache);
2203
+ }
2204
+
2205
+ UNSAFE_componentWillReceiveProps(props) {
2206
+ if (this.props.keypadElement !== props.keypadElement) {
2207
+ this._clearKeypadBoundsCache();
2208
+ }
2209
+ }
2210
+
2211
+ componentDidUpdate(prevProps, prevState) {
2212
+ if (this.mathField.getContent() !== this.props.value) {
2213
+ this.mathField.setContent(this.props.value);
2214
+ }
2215
+
2216
+ if (prevState.focused !== this.state.focused) {
2217
+ this._updateInputPadding();
2218
+ }
2219
+ }
2220
+
2221
+ componentWillUnmount() {
2222
+ this._isMounted = false;
2223
+ window.removeEventListener("touchstart", this.recordTouchStartOutside);
2224
+ window.removeEventListener("touchend", this.blurOnTouchEndOutside);
2225
+ window.removeEventListener("touchcancel", this.blurOnTouchEndOutside);
2226
+ window.removeEventListener("resize", this._clearKeypadBoundsCache());
2227
+ window.removeEventListener("orientationchange", this._clearKeypadBoundsCache());
2228
+ }
2229
+
2230
+ render() {
2231
+ const {
2232
+ focused,
2233
+ handle
2234
+ } = this.state;
2235
+ const {
2236
+ style
2237
+ } = this.props;
2238
+ const innerStyle = { ...inlineStyles$1.innerContainer,
2239
+ borderWidth: this.getBorderWidthPx(),
2240
+ ...(focused ? {
2241
+ borderColor: wonderBlocksBlue
2242
+ } : {}),
2243
+ ...style
2244
+ }; // NOTE(diedra): This label explicitly refers to tapping because this field
2245
+ // is currently only seen if the user is using a mobile device.
2246
+ // We added the tapping instructions because there is currently a bug where
2247
+ // Android users need to use two fingers to tap the input field to make the
2248
+ // keyboard appear. It should only require one finger, which is how iOS works.
2249
+ // TODO(diedra): Fix the bug that is causing Android to require a two finger tap
2250
+ // to the open the keyboard, and then remove the second half of this label.
2251
+
2252
+ const ariaLabel = i18n__namespace._("Math input box") + " " + i18n__namespace._("Tap with one or two fingers to open keyboard");
2253
+
2254
+ return /*#__PURE__*/React__namespace.createElement(View, {
2255
+ style: styles$e.input,
2256
+ onTouchStart: this.handleTouchStart,
2257
+ onTouchMove: this.handleTouchMove,
2258
+ onTouchEnd: this.handleTouchEnd,
2259
+ onClick: e => e.stopPropagation(),
2260
+ role: "textbox",
2261
+ ariaLabel: ariaLabel
2262
+ }, /*#__PURE__*/React__namespace.createElement("div", {
2263
+ className: "keypad-input",
2264
+ tabIndex: "0",
2265
+ ref: node => {
2266
+ this.inputRef = node;
2267
+ },
2268
+ onKeyUp: this.handleKeyUp
2269
+ }, /*#__PURE__*/React__namespace.createElement("div", {
2270
+ ref: node => {
2271
+ this._mathContainer = ReactDOM__default["default"].findDOMNode(node);
2272
+ },
2273
+ style: innerStyle
2274
+ })), focused && handle.visible && /*#__PURE__*/React__namespace.createElement(CursorHandle, _extends({}, handle, {
2275
+ onTouchStart: this.onCursorHandleTouchStart,
2276
+ onTouchMove: this.onCursorHandleTouchMove,
2277
+ onTouchEnd: this.onCursorHandleTouchEnd,
2278
+ onTouchCancel: this.onCursorHandleTouchCancel
2279
+ })));
2280
+ }
2281
+
2282
+ }
2283
+
2284
+ _defineProperty(MathInput, "defaultProps", {
2285
+ style: {},
2286
+ value: ""
2287
+ });
2288
+
2289
+ const fontSizePt = 18;
2290
+ const inputMaxWidth = 128; // The height of numerals in Symbola (rendered at 18pt) is about 20px (though
2291
+ // they render at 24px due to padding for ascenders and descenders). We want our
2292
+ // box to be laid out such that there's 12px of padding between a numeral and the
2293
+ // edge of the input, so we use this 20px number as our 'base height' and
2294
+ // account for the ascender and descender padding when computing the additional
2295
+ // padding in our `render` method.
2296
+
2297
+ const numeralHeightPx = 20;
2298
+ const totalDesiredPadding = 12;
2299
+ const minHeightPx = numeralHeightPx + totalDesiredPadding * 2;
2300
+ const minWidthPx = 64;
2301
+ const styles$e = aphrodite.StyleSheet.create({
2302
+ input: {
2303
+ position: "relative",
2304
+ display: "inline-block",
2305
+ verticalAlign: "middle",
2306
+ maxWidth: inputMaxWidth
2307
+ }
2308
+ });
2309
+ const inlineStyles$1 = {
2310
+ // Styles for the inner, MathQuill-ified input element. It's important that
2311
+ // these are done with regular inline styles rather than Aphrodite classes
2312
+ // as MathQuill adds CSS class names to the element outside of the typical
2313
+ // React flow; assigning a class to the element can thus disrupt MathQuill
2314
+ // behavior. For example, if the client provided new styles to be applied
2315
+ // on focus and the styles here were applied with Aphrodite, then Aphrodite
2316
+ // would merge the provided styles with the base styles here, producing a
2317
+ // new CSS class name that we would apply to the element, clobbering any CSS
2318
+ // class names that MathQuill had applied itself.
2319
+ innerContainer: {
2320
+ backgroundColor: "white",
2321
+ minHeight: minHeightPx,
2322
+ minWidth: minWidthPx,
2323
+ maxWidth: inputMaxWidth,
2324
+ boxSizing: "border-box",
2325
+ position: "relative",
2326
+ borderStyle: "solid",
2327
+ borderColor: Color__default["default"].offBlack50,
2328
+ borderRadius: 4,
2329
+ color: offBlack
2330
+ }
2331
+ };
2332
+
2333
+ /**
2334
+ * This file contains configuration settings for the buttons in the keypad.
2335
+ */
2336
+ const KeyConfigs = {
2337
+ // Basic math keys.
2338
+ [Keys.PLUS]: {
2339
+ type: KeyTypes.OPERATOR,
2340
+ // I18N: A label for a plus sign.
2341
+ ariaLabel: i18n__namespace._("Plus")
2342
+ },
2343
+ [Keys.MINUS]: {
2344
+ type: KeyTypes.OPERATOR,
2345
+ // I18N: A label for a minus sign.
2346
+ ariaLabel: i18n__namespace._("Minus")
2347
+ },
2348
+ [Keys.NEGATIVE]: {
2349
+ type: KeyTypes.VALUE,
2350
+ // I18N: A label for a minus sign.
2351
+ ariaLabel: i18n__namespace._("Negative")
2352
+ },
2353
+ [Keys.TIMES]: {
2354
+ type: KeyTypes.OPERATOR,
2355
+ // I18N: A label for a multiplication sign (represented with an 'x').
2356
+ ariaLabel: i18n__namespace._("Multiply")
2357
+ },
2358
+ [Keys.DIVIDE]: {
2359
+ type: KeyTypes.OPERATOR,
2360
+ // I18N: A label for a division sign.
2361
+ ariaLabel: i18n__namespace._("Divide")
2362
+ },
2363
+ [Keys.DECIMAL]: {
2364
+ type: KeyTypes.VALUE,
2365
+ // I18N: A label for a decimal symbol.
2366
+ ariaLabel: i18n__namespace._("Decimal"),
2367
+ icon: decimalSeparator === DecimalSeparators.COMMA ? {
2368
+ // TODO(charlie): Get an SVG icon for the comma, or verify with
2369
+ // design that the text-rendered version is acceptable.
2370
+ type: IconTypes.TEXT,
2371
+ data: ","
2372
+ } : {
2373
+ type: IconTypes.SVG,
2374
+ data: Keys.PERIOD
2375
+ }
2376
+ },
2377
+ [Keys.PERCENT]: {
2378
+ type: KeyTypes.OPERATOR,
2379
+ // I18N: A label for a percent sign.
2380
+ ariaLabel: i18n__namespace._("Percent")
2381
+ },
2382
+ [Keys.CDOT]: {
2383
+ type: KeyTypes.OPERATOR,
2384
+ // I18N: A label for a multiplication sign (represented as a dot).
2385
+ ariaLabel: i18n__namespace._("Multiply")
2386
+ },
2387
+ [Keys.EQUAL]: {
2388
+ type: KeyTypes.OPERATOR,
2389
+ ariaLabel: i18n__namespace._("Equals sign")
2390
+ },
2391
+ [Keys.NEQ]: {
2392
+ type: KeyTypes.OPERATOR,
2393
+ ariaLabel: i18n__namespace._("Not-equals sign")
2394
+ },
2395
+ [Keys.GT]: {
2396
+ type: KeyTypes.OPERATOR,
2397
+ // I18N: A label for a 'greater than' sign (represented as '>').
2398
+ ariaLabel: i18n__namespace._("Greater than sign")
2399
+ },
2400
+ [Keys.LT]: {
2401
+ type: KeyTypes.OPERATOR,
2402
+ // I18N: A label for a 'less than' sign (represented as '<').
2403
+ ariaLabel: i18n__namespace._("Less than sign")
2404
+ },
2405
+ [Keys.GEQ]: {
2406
+ type: KeyTypes.OPERATOR,
2407
+ ariaLabel: i18n__namespace._("Greater than or equal to sign")
2408
+ },
2409
+ [Keys.LEQ]: {
2410
+ type: KeyTypes.OPERATOR,
2411
+ ariaLabel: i18n__namespace._("Less than or equal to sign")
2412
+ },
2413
+ // mobile native
2414
+ [Keys.FRAC_INCLUSIVE]: {
2415
+ type: KeyTypes.OPERATOR,
2416
+ // I18N: A label for a button that creates a new fraction and puts the
2417
+ // current expression in the numerator of that fraction.
2418
+ ariaLabel: i18n__namespace._("Fraction, with current expression in numerator")
2419
+ },
2420
+ // mobile native
2421
+ [Keys.FRAC_EXCLUSIVE]: {
2422
+ type: KeyTypes.OPERATOR,
2423
+ // I18N: A label for a button that creates a new fraction next to the
2424
+ // cursor.
2425
+ ariaLabel: i18n__namespace._("Fraction, excluding the current expression")
2426
+ },
2427
+ // mobile web
2428
+ [Keys.FRAC]: {
2429
+ type: KeyTypes.OPERATOR,
2430
+ // I18N: A label for a button that creates a new fraction next to the
2431
+ // cursor.
2432
+ ariaLabel: i18n__namespace._("Fraction, excluding the current expression")
2433
+ },
2434
+ [Keys.EXP]: {
2435
+ type: KeyTypes.OPERATOR,
2436
+ // I18N: A label for a button that will allow the user to input a custom
2437
+ // exponent.
2438
+ ariaLabel: i18n__namespace._("Custom exponent")
2439
+ },
2440
+ [Keys.EXP_2]: {
2441
+ type: KeyTypes.OPERATOR,
2442
+ // I18N: A label for a button that will square (take to the second
2443
+ // power) some math.
2444
+ ariaLabel: i18n__namespace._("Square")
2445
+ },
2446
+ [Keys.EXP_3]: {
2447
+ type: KeyTypes.OPERATOR,
2448
+ // I18N: A label for a button that will cube (take to the third power)
2449
+ // some math.
2450
+ ariaLabel: i18n__namespace._("Cube")
2451
+ },
2452
+ [Keys.SQRT]: {
2453
+ type: KeyTypes.OPERATOR,
2454
+ ariaLabel: i18n__namespace._("Square root")
2455
+ },
2456
+ [Keys.CUBE_ROOT]: {
2457
+ type: KeyTypes.OPERATOR,
2458
+ ariaLabel: i18n__namespace._("Cube root")
2459
+ },
2460
+ [Keys.RADICAL]: {
2461
+ type: KeyTypes.OPERATOR,
2462
+ ariaLabel: i18n__namespace._("Radical with custom root")
2463
+ },
2464
+ [Keys.LEFT_PAREN]: {
2465
+ type: KeyTypes.OPERATOR,
2466
+ ariaLabel: i18n__namespace._("Left parenthesis")
2467
+ },
2468
+ [Keys.RIGHT_PAREN]: {
2469
+ type: KeyTypes.OPERATOR,
2470
+ ariaLabel: i18n__namespace._("Right parenthesis")
2471
+ },
2472
+ [Keys.LN]: {
2473
+ type: KeyTypes.OPERATOR,
2474
+ ariaLabel: i18n__namespace._("Natural logarithm")
2475
+ },
2476
+ [Keys.LOG]: {
2477
+ type: KeyTypes.OPERATOR,
2478
+ ariaLabel: i18n__namespace._("Logarithm with base 10")
2479
+ },
2480
+ [Keys.LOG_N]: {
2481
+ type: KeyTypes.OPERATOR,
2482
+ ariaLabel: i18n__namespace._("Logarithm with custom base")
2483
+ },
2484
+ [Keys.SIN]: {
2485
+ type: KeyTypes.OPERATOR,
2486
+ ariaLabel: i18n__namespace._("Sine")
2487
+ },
2488
+ [Keys.COS]: {
2489
+ type: KeyTypes.OPERATOR,
2490
+ ariaLabel: i18n__namespace._("Cosine")
2491
+ },
2492
+ [Keys.TAN]: {
2493
+ type: KeyTypes.OPERATOR,
2494
+ ariaLabel: i18n__namespace._("Tangent")
2495
+ },
2496
+ [Keys.PI]: {
2497
+ type: KeyTypes.VALUE,
2498
+ ariaLabel: i18n__namespace._("Pi"),
2499
+ icon: {
2500
+ type: IconTypes.MATH,
2501
+ data: "\\pi"
2502
+ }
2503
+ },
2504
+ [Keys.THETA]: {
2505
+ type: KeyTypes.VALUE,
2506
+ ariaLabel: i18n__namespace._("Theta"),
2507
+ icon: {
2508
+ type: IconTypes.MATH,
2509
+ data: "\\theta"
2510
+ }
2511
+ },
2512
+ [Keys.NOOP]: {
2513
+ type: KeyTypes.EMPTY
2514
+ },
2515
+ // Input navigation keys.
2516
+ [Keys.UP]: {
2517
+ type: KeyTypes.INPUT_NAVIGATION,
2518
+ ariaLabel: i18n__namespace._("Up arrow")
2519
+ },
2520
+ [Keys.RIGHT]: {
2521
+ type: KeyTypes.INPUT_NAVIGATION,
2522
+ ariaLabel: i18n__namespace._("Right arrow")
2523
+ },
2524
+ [Keys.DOWN]: {
2525
+ type: KeyTypes.INPUT_NAVIGATION,
2526
+ ariaLabel: i18n__namespace._("Down arrow")
2527
+ },
2528
+ [Keys.LEFT]: {
2529
+ type: KeyTypes.INPUT_NAVIGATION,
2530
+ ariaLabel: i18n__namespace._("Left arrow")
2531
+ },
2532
+ [Keys.JUMP_OUT_PARENTHESES]: {
2533
+ type: KeyTypes.INPUT_NAVIGATION,
2534
+ ariaLabel: i18n__namespace._("Navigate right out of a set of parentheses")
2535
+ },
2536
+ [Keys.JUMP_OUT_EXPONENT]: {
2537
+ type: KeyTypes.INPUT_NAVIGATION,
2538
+ ariaLabel: i18n__namespace._("Navigate right out of an exponent")
2539
+ },
2540
+ [Keys.JUMP_OUT_BASE]: {
2541
+ type: KeyTypes.INPUT_NAVIGATION,
2542
+ ariaLabel: i18n__namespace._("Navigate right out of a base")
2543
+ },
2544
+ [Keys.JUMP_INTO_NUMERATOR]: {
2545
+ type: KeyTypes.INPUT_NAVIGATION,
2546
+ ariaLabel: i18n__namespace._("Navigate right into the numerator of a fraction")
2547
+ },
2548
+ [Keys.JUMP_OUT_NUMERATOR]: {
2549
+ type: KeyTypes.INPUT_NAVIGATION,
2550
+ ariaLabel: i18n__namespace._("Navigate right out of the numerator and into the denominator")
2551
+ },
2552
+ [Keys.JUMP_OUT_DENOMINATOR]: {
2553
+ type: KeyTypes.INPUT_NAVIGATION,
2554
+ ariaLabel: i18n__namespace._("Navigate right out of the denominator of a fraction")
2555
+ },
2556
+ [Keys.BACKSPACE]: {
2557
+ type: KeyTypes.INPUT_NAVIGATION,
2558
+ // I18N: A label for a button that will delete some input.
2559
+ ariaLabel: i18n__namespace._("Delete")
2560
+ },
2561
+ // Keypad navigation keys.
2562
+ [Keys.DISMISS]: {
2563
+ type: KeyTypes.KEYPAD_NAVIGATION,
2564
+ // I18N: A label for a button that will dismiss/hide a keypad.
2565
+ ariaLabel: i18n__namespace._("Dismiss")
2566
+ }
2567
+ }; // Add in any multi-function buttons. By default, these keys will mix in any
2568
+ // configuration settings from their default child key (i.e., the first key in
2569
+ // the `childKeyIds` array).
2570
+ // TODO(charlie): Make the multi-function button's long-press interaction
2571
+ // accessible.
2572
+ // NOTE(kevinb): This is only used in the mobile native app.
2573
+
2574
+ KeyConfigs[Keys.FRAC_MULTI] = {
2575
+ childKeyIds: [Keys.FRAC_INCLUSIVE, Keys.FRAC_EXCLUSIVE]
2576
+ }; // TODO(charlie): Use the numeral color for the 'Many' key.
2539
2577
 
2540
- window.addEventListener("resize", this._clearKeypadBoundsCache);
2541
- window.addEventListener("orientationchange", this._clearKeypadBoundsCache);
2542
- }
2578
+ KeyConfigs[Keys.MANY] = {
2579
+ type: KeyTypes.MANY // childKeyIds will be configured by the client.
2543
2580
 
2544
- UNSAFE_componentWillReceiveProps(props) {
2545
- if (this.props.keypadElement !== props.keypadElement) {
2546
- this._clearKeypadBoundsCache();
2547
- }
2548
- }
2581
+ }; // Add in every numeral.
2549
2582
 
2550
- componentDidUpdate(prevProps, prevState) {
2551
- if (this.mathField.getContent() !== this.props.value) {
2552
- this.mathField.setContent(this.props.value);
2553
- }
2583
+ const NUMBERS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
2554
2584
 
2555
- if (prevState.focused !== this.state.focused) {
2556
- this._updateInputPadding();
2585
+ for (const num of NUMBERS) {
2586
+ // TODO(charlie): Consider removing the SVG icons that we have for the
2587
+ // numeral keys. They can be rendered just as easily with text (though that
2588
+ // would mean that we'd be using text beyond the variable key).
2589
+ const textRepresentation = "".concat(num);
2590
+ KeyConfigs["NUM_".concat(num)] = {
2591
+ type: KeyTypes.VALUE,
2592
+ ariaLabel: textRepresentation,
2593
+ icon: {
2594
+ type: IconTypes.TEXT,
2595
+ data: textRepresentation
2557
2596
  }
2558
- }
2597
+ };
2598
+ } // Add in every variable.
2559
2599
 
2560
- componentWillUnmount() {
2561
- this._isMounted = false;
2562
- window.removeEventListener("touchstart", this.recordTouchStartOutside);
2563
- window.removeEventListener("touchend", this.blurOnTouchEndOutside);
2564
- window.removeEventListener("touchcancel", this.blurOnTouchEndOutside);
2565
- window.removeEventListener("resize", this._clearKeypadBoundsCache());
2566
- window.removeEventListener("orientationchange", this._clearKeypadBoundsCache());
2567
- }
2568
2600
 
2569
- render() {
2570
- const {
2571
- focused,
2572
- handle
2573
- } = this.state;
2574
- const {
2575
- style
2576
- } = this.props;
2577
- const innerStyle = { ...inlineStyles$1.innerContainer,
2578
- borderWidth: this.getBorderWidthPx(),
2579
- ...(focused ? {
2580
- borderColor: wonderBlocksBlue
2581
- } : {}),
2582
- ...style
2583
- }; // NOTE(diedra): This label explicitly refers to tapping because this field
2584
- // is currently only seen if the user is using a mobile device.
2585
- // We added the tapping instructions because there is currently a bug where
2586
- // Android users need to use two fingers to tap the input field to make the
2587
- // keyboard appear. It should only require one finger, which is how iOS works.
2588
- // TODO(diedra): Fix the bug that is causing Android to require a two finger tap
2589
- // to the open the keyboard, and then remove the second half of this label.
2601
+ const LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
2590
2602
 
2591
- const ariaLabel = i18n__namespace._("Math input box") + " " + i18n__namespace._("Tap with one or two fingers to open keyboard");
2603
+ for (const letter of LETTERS) {
2604
+ const lowerCaseVariable = letter.toLowerCase();
2605
+ const upperCaseVariable = letter.toUpperCase();
2592
2606
 
2593
- return /*#__PURE__*/React__namespace.createElement(View, {
2594
- style: styles$e.input,
2595
- onTouchStart: this.handleTouchStart,
2596
- onTouchMove: this.handleTouchMove,
2597
- onTouchEnd: this.handleTouchEnd,
2598
- onClick: e => e.stopPropagation(),
2599
- role: "textbox",
2600
- ariaLabel: ariaLabel
2601
- }, /*#__PURE__*/React__namespace.createElement("div", {
2602
- className: "keypad-input",
2603
- tabIndex: "0",
2604
- ref: node => {
2605
- this.inputRef = node;
2606
- },
2607
- onKeyUp: this.handleKeyUp
2608
- }, /*#__PURE__*/React__namespace.createElement("div", {
2609
- ref: node => {
2610
- this._mathContainer = ReactDOM__default["default"].findDOMNode(node);
2611
- },
2612
- style: innerStyle
2613
- })), focused && handle.visible && /*#__PURE__*/React__namespace.createElement(CursorHandle, _extends({}, handle, {
2614
- onTouchStart: this.onCursorHandleTouchStart,
2615
- onTouchMove: this.onCursorHandleTouchMove,
2616
- onTouchEnd: this.onCursorHandleTouchEnd,
2617
- onTouchCancel: this.onCursorHandleTouchCancel
2618
- })));
2607
+ for (const textRepresentation of [lowerCaseVariable, upperCaseVariable]) {
2608
+ KeyConfigs[textRepresentation] = {
2609
+ type: KeyTypes.VALUE,
2610
+ ariaLabel: textRepresentation,
2611
+ icon: {
2612
+ type: IconTypes.MATH,
2613
+ data: textRepresentation
2614
+ }
2615
+ };
2619
2616
  }
2617
+ }
2620
2618
 
2619
+ for (const key of Object.keys(KeyConfigs)) {
2620
+ KeyConfigs[key] = {
2621
+ id: key,
2622
+ // Default to an SVG icon indexed by the key name.
2623
+ icon: {
2624
+ type: IconTypes.SVG,
2625
+ data: key
2626
+ },
2627
+ ...KeyConfigs[key]
2628
+ };
2621
2629
  }
2622
2630
 
2623
- _defineProperty(MathInput, "propTypes", {
2624
- // The React element node associated with the keypad that will send
2625
- // key-press events to this input. If provided, this can be used to:
2626
- // (1) Avoid blurring the input, on user interaction with the keypad.
2627
- // (2) Scroll the input into view, if it would otherwise be obscured
2628
- // by the keypad on focus.
2629
- keypadElement: keypadElementPropType,
2630
- onBlur: PropTypes__default["default"].func,
2631
- onChange: PropTypes__default["default"].func.isRequired,
2632
- onFocus: PropTypes__default["default"].func,
2633
- // An extra, vanilla style object, to be applied to the math input.
2634
- style: PropTypes__default["default"].any,
2635
- value: PropTypes__default["default"].string
2631
+ /**
2632
+ * React PropTypes that may be shared between components.
2633
+ */
2634
+ const iconPropType = PropTypes__default["default"].shape({
2635
+ type: PropTypes__default["default"].oneOf(Object.keys(IconTypes)).isRequired,
2636
+ data: PropTypes__default["default"].string.isRequired
2636
2637
  });
2637
-
2638
- _defineProperty(MathInput, "defaultProps", {
2639
- style: {},
2640
- value: ""
2638
+ const keyIdPropType = PropTypes__default["default"].oneOf(Object.keys(KeyConfigs));
2639
+ const keyConfigPropType = PropTypes__default["default"].shape({
2640
+ ariaLabel: PropTypes__default["default"].string,
2641
+ id: keyIdPropType.isRequired,
2642
+ type: PropTypes__default["default"].oneOf(Object.keys(KeyTypes)).isRequired,
2643
+ childKeyIds: PropTypes__default["default"].arrayOf(keyIdPropType),
2644
+ icon: iconPropType.isRequired
2641
2645
  });
2646
+ const keypadConfigurationPropType = PropTypes__default["default"].shape({
2647
+ keypadType: PropTypes__default["default"].oneOf(Object.keys(KeypadTypes)).isRequired,
2648
+ extraKeys: PropTypes__default["default"].arrayOf(keyIdPropType)
2649
+ }); // NOTE(jared): This is no longer guaranteed to be React element
2642
2650
 
2643
- const fontSizePt = 18;
2644
- const inputMaxWidth = 128; // The height of numerals in Symbola (rendered at 18pt) is about 20px (though
2645
- // they render at 24px due to padding for ascenders and descenders). We want our
2646
- // box to be laid out such that there's 12px of padding between a numeral and the
2647
- // edge of the input, so we use this 20px number as our 'base height' and
2648
- // account for the ascender and descender padding when computing the additional
2649
- // padding in our `render` method.
2650
-
2651
- const numeralHeightPx = 20;
2652
- const totalDesiredPadding = 12;
2653
- const minHeightPx = numeralHeightPx + totalDesiredPadding * 2;
2654
- const minWidthPx = 64;
2655
- const styles$e = aphrodite.StyleSheet.create({
2656
- input: {
2657
- position: "relative",
2658
- display: "inline-block",
2659
- verticalAlign: "middle",
2660
- maxWidth: inputMaxWidth
2661
- }
2651
+ const keypadElementPropType = PropTypes__default["default"].shape({
2652
+ activate: PropTypes__default["default"].func.isRequired,
2653
+ dismiss: PropTypes__default["default"].func.isRequired,
2654
+ configure: PropTypes__default["default"].func.isRequired,
2655
+ setCursor: PropTypes__default["default"].func.isRequired,
2656
+ setKeyHandler: PropTypes__default["default"].func.isRequired,
2657
+ getDOMNode: PropTypes__default["default"].func.isRequired
2662
2658
  });
2663
- const inlineStyles$1 = {
2664
- // Styles for the inner, MathQuill-ified input element. It's important that
2665
- // these are done with regular inline styles rather than Aphrodite classes
2666
- // as MathQuill adds CSS class names to the element outside of the typical
2667
- // React flow; assigning a class to the element can thus disrupt MathQuill
2668
- // behavior. For example, if the client provided new styles to be applied
2669
- // on focus and the styles here were applied with Aphrodite, then Aphrodite
2670
- // would merge the provided styles with the base styles here, producing a
2671
- // new CSS class name that we would apply to the element, clobbering any CSS
2672
- // class names that MathQuill had applied itself.
2673
- innerContainer: {
2674
- backgroundColor: "white",
2675
- minHeight: minHeightPx,
2676
- minWidth: minWidthPx,
2677
- maxWidth: inputMaxWidth,
2678
- boxSizing: "border-box",
2679
- position: "relative",
2680
- borderStyle: "solid",
2681
- borderColor: Color__default["default"].offBlack50,
2682
- borderRadius: 4,
2683
- color: offBlack
2684
- }
2685
- };
2659
+ const bordersPropType = PropTypes__default["default"].arrayOf(PropTypes__default["default"].oneOf(Object.keys(BorderDirections)));
2660
+ const boundingBoxPropType = PropTypes__default["default"].shape({
2661
+ height: PropTypes__default["default"].number,
2662
+ width: PropTypes__default["default"].number,
2663
+ top: PropTypes__default["default"].number,
2664
+ right: PropTypes__default["default"].number,
2665
+ bottom: PropTypes__default["default"].number,
2666
+ left: PropTypes__default["default"].number
2667
+ });
2668
+ const echoPropType = PropTypes__default["default"].shape({
2669
+ animationId: PropTypes__default["default"].string.isRequired,
2670
+ animationType: PropTypes__default["default"].oneOf(Object.keys(EchoAnimationTypes)).isRequired,
2671
+ borders: bordersPropType,
2672
+ id: keyIdPropType.isRequired,
2673
+ initialBounds: boundingBoxPropType.isRequired
2674
+ });
2675
+ const cursorContextPropType = PropTypes__default["default"].oneOf(Object.keys(CursorContexts));
2676
+ const popoverPropType = PropTypes__default["default"].shape({
2677
+ parentId: keyIdPropType.isRequired,
2678
+ bounds: boundingBoxPropType.isRequired,
2679
+ childKeyIds: PropTypes__default["default"].arrayOf(keyIdPropType).isRequired
2680
+ });
2681
+ PropTypes__default["default"].oneOfType([PropTypes__default["default"].arrayOf(PropTypes__default["default"].node), PropTypes__default["default"].node]);
2686
2682
 
2687
2683
  // naming convention: verb + noun
2688
2684
  // the noun should be one of the other properties in the object that's
@@ -7649,13 +7645,9 @@ const inlineStyles = {
7649
7645
  visibility: "hidden"
7650
7646
  },
7651
7647
  hidden: {
7652
- msTransform: "translate3d(0, 100%, 0)",
7653
- WebkitTransform: "translate3d(0, 100%, 0)",
7654
7648
  transform: "translate3d(0, 100%, 0)"
7655
7649
  },
7656
7650
  active: {
7657
- msTransform: "translate3d(0, 0, 0)",
7658
- WebkitTransform: "translate3d(0, 0, 0)",
7659
7651
  transform: "translate3d(0, 0, 0)"
7660
7652
  }
7661
7653
  };