@zzish/math-rich-input 0.1.51 → 0.1.53

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
@@ -20283,9 +20283,6 @@ var Toolbar = /*#__PURE__*/function (_React$Component) {
20283
20283
  var _this;
20284
20284
  _classCallCheck(this, Toolbar);
20285
20285
  _this = _callSuper(this, Toolbar, [props]);
20286
- _defineProperty(_this, "handleButtonClick", function (event, buttonName) {
20287
- _this.suppliedClickHandler(event, buttonName);
20288
- });
20289
20286
  _defineProperty(_this, "showAccentBar", function (event) {
20290
20287
  if (!_this.state.showAccents) _this.accentLetter = _this.suppliedAccentLetterFetcher();
20291
20288
  _this.setState({
@@ -20305,6 +20302,8 @@ var Toolbar = /*#__PURE__*/function (_React$Component) {
20305
20302
  });
20306
20303
  });
20307
20304
  _defineProperty(_this, "handleButtonClick", function (event, buttonName) {
20305
+ event.preventDefault();
20306
+ event.stopPropagation();
20308
20307
  _this.suppliedClickHandler(event, buttonName);
20309
20308
  });
20310
20309
  _defineProperty(_this, "handleAccentButtonClick", function (event, oldCharacter, newCharacter) {
@@ -20479,6 +20478,9 @@ var Toolbar = /*#__PURE__*/function (_React$Component) {
20479
20478
  }, "\xE1"), enableMath && /*#__PURE__*/React__default["default"].createElement("button", {
20480
20479
  name: "Equation",
20481
20480
  className: "Toolbar-button TB-wide",
20481
+ onClick: function onClick(e) {
20482
+ return e.preventDefault();
20483
+ },
20482
20484
  onMouseDown: function onMouseDown(e) {
20483
20485
  return _this2.handleButtonClick(e, "Equation");
20484
20486
  }
@@ -20668,6 +20670,73 @@ function determineMimeType(text) {
20668
20670
  {this.test("$a bcd> e$", "application/x-tex")}
20669
20671
  */
20670
20672
 
20673
+ var DEBUG_MATH_RICH_INPUT = false;
20674
+ function getNodePath(node) {
20675
+ var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
20676
+ if (!node) return null;
20677
+ var parts = [];
20678
+ var current = node;
20679
+ while (current && current !== root && parts.length < 8) {
20680
+ if (current.nodeType === 3) {
20681
+ parts.unshift("#text(\"".concat(current.nodeValue, "\")"));
20682
+ } else {
20683
+ var name = current.nodeName ? current.nodeName.toLowerCase() : "unknown";
20684
+ var className = current.className && typeof current.className === "string" ? ".".concat(current.className.split(" ").filter(Boolean).join(".")) : "";
20685
+ parts.unshift("".concat(name).concat(className));
20686
+ }
20687
+ current = current.parentNode;
20688
+ }
20689
+ if (root) parts.unshift("root");
20690
+ return parts.join(" > ");
20691
+ }
20692
+ function describeNode(node) {
20693
+ var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
20694
+ if (!node) return null;
20695
+ return {
20696
+ nodeName: node.nodeName,
20697
+ nodeType: node.nodeType,
20698
+ text: node.nodeType === 3 && typeof node.nodeValue === "string" ? node.nodeValue : undefined,
20699
+ textLength: node.nodeType === 3 && typeof node.nodeValue === "string" ? node.nodeValue.length : undefined,
20700
+ childCount: node.childNodes ? node.childNodes.length : undefined,
20701
+ path: getNodePath(node, root)
20702
+ };
20703
+ }
20704
+ function getSelectionSnapshot() {
20705
+ var editableDiv = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
20706
+ if (typeof window === "undefined" || !window.getSelection) return null;
20707
+ var selection = window.getSelection();
20708
+ if (!selection) return null;
20709
+ var snapshot = {
20710
+ activeElement: typeof document !== "undefined" && document.activeElement ? {
20711
+ nodeName: document.activeElement.nodeName,
20712
+ className: document.activeElement.className
20713
+ } : null,
20714
+ rangeCount: selection.rangeCount,
20715
+ isCollapsed: selection.isCollapsed,
20716
+ anchorOffset: selection.anchorOffset,
20717
+ focusOffset: selection.focusOffset,
20718
+ anchorNode: describeNode(selection.anchorNode, editableDiv),
20719
+ focusNode: describeNode(selection.focusNode, editableDiv)
20720
+ };
20721
+ if (selection.rangeCount > 0) {
20722
+ var range = selection.getRangeAt(0);
20723
+ snapshot.range = {
20724
+ collapsed: range.collapsed,
20725
+ startOffset: range.startOffset,
20726
+ endOffset: range.endOffset,
20727
+ startContainer: describeNode(range.startContainer, editableDiv),
20728
+ endContainer: describeNode(range.endContainer, editableDiv)
20729
+ };
20730
+ }
20731
+ return snapshot;
20732
+ }
20733
+ function debugMathRichInput(label) {
20734
+ return;
20735
+ }
20736
+ if (typeof window !== "undefined") {
20737
+ window.__MRI_DEBUG__ = DEBUG_MATH_RICH_INPUT;
20738
+ }
20739
+
20671
20740
  var RAW_TEXT_MATH_START_TAG_TEX = "\\[";
20672
20741
  var RAW_TEXT_MATH_END_TAG_TEX = "\\]";
20673
20742
  var RAW_TEXT_MATH_START_TAG_HTML_MATH = "<math>";
@@ -21309,6 +21378,55 @@ function findFirstTextNode(node) {
21309
21378
  }
21310
21379
  return null;
21311
21380
  }
21381
+ function findLastTextNode(node) {
21382
+ if (isTextNode(node)) return node;
21383
+ var nodes = node.childNodes;
21384
+ for (var i = nodes.length - 1; i >= 0; i--) {
21385
+ var found_node = findLastTextNode(nodes[i]);
21386
+ if (found_node !== null) return found_node;
21387
+ }
21388
+ return null;
21389
+ }
21390
+ function getTextPositionFromElementRangeEndpoint(container, offset) {
21391
+ if (isTextNode(container)) return {
21392
+ node: container,
21393
+ offset: offset
21394
+ };
21395
+ var nodes = container.childNodes;
21396
+ if (nodes === null || nodes === undefined || nodes.length === 0) return null;
21397
+
21398
+ // A collapsed selection inside contentEditable may report the editable SPAN
21399
+ // itself as the container and the child index as the offset. The previous
21400
+ // implementation always picked the first descendant text node, which moved
21401
+ // the caret to the beginning after React re-rendered the controlled input.
21402
+ if (offset > 0) {
21403
+ var previousChild = nodes[Math.min(offset - 1, nodes.length - 1)];
21404
+ var previousTextNode = findLastTextNode(previousChild);
21405
+ if (previousTextNode !== null) {
21406
+ return {
21407
+ node: previousTextNode,
21408
+ offset: previousTextNode.nodeValue.length
21409
+ };
21410
+ }
21411
+ }
21412
+ if (offset < nodes.length) {
21413
+ var nextTextNode = findFirstTextNode(nodes[offset]);
21414
+ if (nextTextNode !== null) {
21415
+ return {
21416
+ node: nextTextNode,
21417
+ offset: 0
21418
+ };
21419
+ }
21420
+ }
21421
+ var fallbackTextNode = findFirstTextNode(container);
21422
+ if (fallbackTextNode !== null) {
21423
+ return {
21424
+ node: fallbackTextNode,
21425
+ offset: 0
21426
+ };
21427
+ }
21428
+ return null;
21429
+ }
21312
21430
  function findNodeAndOffsetForGlobalOffsetInEditableDiv(editableDiv, globalOffset) {
21313
21431
  var result = {
21314
21432
  offset: 0
@@ -21350,6 +21468,10 @@ function findNodeAndOffsetForGlobalOffsetInEditableDiv(editableDiv, globalOffset
21350
21468
  function getRangeParams(editableDiv) {
21351
21469
  // console.log ("getRangeParams")
21352
21470
  // console.log("Inner html: "+editableDiv.innerHTML)
21471
+ debugMathRichInput("getRangeParams:start", {
21472
+ innerHTML: editableDiv ? editableDiv.innerHTML : null,
21473
+ selection: getSelectionSnapshot(editableDiv)
21474
+ });
21353
21475
  var isSupported = typeof window.getSelection !== "undefined";
21354
21476
  if (!isSupported) {
21355
21477
  console.error("Can't get range params, browser does not support window.getSelection");
@@ -21361,15 +21483,27 @@ function getRangeParams(editableDiv) {
21361
21483
  return null;
21362
21484
  }
21363
21485
  var range = selection.getRangeAt(0);
21486
+ debugMathRichInput("getRangeParams:range-before-normalize", {
21487
+ startContainer: describeNode(range.startContainer, editableDiv),
21488
+ startOffset: range.startOffset,
21489
+ endContainer: describeNode(range.endContainer, editableDiv),
21490
+ endOffset: range.endOffset
21491
+ });
21364
21492
 
21365
21493
  // logAllDescendants(editableDiv)
21366
- // If the range is not within a text node, move it to a text node
21367
- // This typically happens when the end of a range is at the start of a new line
21494
+ // If the range is not within a text node, move it to the text node implied
21495
+ // by the element-container offset. This typically happens when the browser
21496
+ // reports a collapsed contentEditable selection as (SPAN, childIndex).
21368
21497
  if (range.startContainer.nodeType !== 3) {
21369
- var textNode = findFirstTextNode(range.startContainer);
21370
- if (textNode !== null) {
21371
- console.warn("Adjusting start pos in getRangeParams as container was of type: " + range.startContainer.nodeName);
21372
- range.setStart(textNode, 0);
21498
+ var textPosition = getTextPositionFromElementRangeEndpoint(range.startContainer, range.startOffset);
21499
+ if (textPosition !== null) {
21500
+ debugMathRichInput("getRangeParams:normalize-start-element", {
21501
+ from: describeNode(range.startContainer, editableDiv),
21502
+ fromOffset: range.startOffset,
21503
+ to: describeNode(textPosition.node, editableDiv),
21504
+ toOffset: textPosition.offset
21505
+ });
21506
+ range.setStart(textPosition.node, textPosition.offset);
21373
21507
  } else {
21374
21508
  // Handle empty content case - return default range params
21375
21509
  // This is normal behavior after selecting all and deleting content
@@ -21385,10 +21519,15 @@ function getRangeParams(editableDiv) {
21385
21519
  }
21386
21520
  }
21387
21521
  if (range.endContainer.nodeType !== 3) {
21388
- var _textNode = findFirstTextNode(range.endContainer);
21389
- if (_textNode !== null) {
21390
- console.warn("Adjusting end pos in getRangeParams as container was of type: " + range.endContainer.nodeName);
21391
- range.setEnd(_textNode, 0);
21522
+ var _textPosition = getTextPositionFromElementRangeEndpoint(range.endContainer, range.endOffset);
21523
+ if (_textPosition !== null) {
21524
+ debugMathRichInput("getRangeParams:normalize-end-element", {
21525
+ from: describeNode(range.endContainer, editableDiv),
21526
+ fromOffset: range.endOffset,
21527
+ to: describeNode(_textPosition.node, editableDiv),
21528
+ toOffset: _textPosition.offset
21529
+ });
21530
+ range.setEnd(_textPosition.node, _textPosition.offset);
21392
21531
  } else {
21393
21532
  // Handle empty content case - return default range params
21394
21533
  // This is normal behavior after selecting all and deleting content
@@ -21434,11 +21573,20 @@ function getRangeParams(editableDiv) {
21434
21573
  startGlobalOffset: startResult.offset,
21435
21574
  endGlobalOffset: endResult.offset
21436
21575
  };
21576
+ debugMathRichInput("getRangeParams:result", {
21577
+ rangeParams: rangeParams,
21578
+ selection: getSelectionSnapshot(editableDiv)
21579
+ });
21437
21580
  return rangeParams;
21438
21581
  }
21439
21582
  function setRangeParamsFromGlobalOffsets(editableDiv, params) {
21440
21583
  var new_range = null;
21441
21584
  try {
21585
+ debugMathRichInput("setRangeParamsFromGlobalOffsets:start", {
21586
+ params: params,
21587
+ innerHTML: editableDiv ? editableDiv.innerHTML : null,
21588
+ selectionBefore: getSelectionSnapshot(editableDiv)
21589
+ });
21442
21590
  if (params === null || params === undefined) throw new Error("Can't set range params: params === " + params);
21443
21591
  var startResult = findNodeAndOffsetForGlobalOffsetInEditableDiv(editableDiv, params.startGlobalOffset);
21444
21592
  var endResult = findNodeAndOffsetForGlobalOffsetInEditableDiv(editableDiv, params.endGlobalOffset);
@@ -21458,6 +21606,18 @@ function setRangeParamsFromGlobalOffsets(editableDiv, params) {
21458
21606
  var selection = window.getSelection();
21459
21607
  selection.removeAllRanges();
21460
21608
  selection.addRange(new_range);
21609
+ debugMathRichInput("setRangeParamsFromGlobalOffsets:after", {
21610
+ params: params,
21611
+ startResult: {
21612
+ node: describeNode(startResult.node, editableDiv),
21613
+ offset: startResult.offset
21614
+ },
21615
+ endResult: {
21616
+ node: describeNode(endResult.node, editableDiv),
21617
+ offset: endResult.offset
21618
+ },
21619
+ selectionAfter: getSelectionSnapshot(editableDiv)
21620
+ });
21461
21621
  } catch (error) {
21462
21622
  console.error(error);
21463
21623
  console.log("Trying to set range:");
@@ -21471,6 +21631,11 @@ function setRangeParams(editableDiv, params) {
21471
21631
 
21472
21632
  var new_range = null;
21473
21633
  try {
21634
+ debugMathRichInput("setRangeParams:start", {
21635
+ params: params,
21636
+ innerHTML: editableDiv ? editableDiv.innerHTML : null,
21637
+ selectionBefore: getSelectionSnapshot(editableDiv)
21638
+ });
21474
21639
  if (params === null || params === undefined) {
21475
21640
  console.error("Can't set range params: params === " + params);
21476
21641
  return;
@@ -21530,6 +21695,7 @@ function setRangeParams(editableDiv, params) {
21530
21695
  startNode = findFirstTextNode(editableDiv);
21531
21696
  startOffset = 0;
21532
21697
  }
21698
+ if (startNode === null) return;
21533
21699
  if (!isTextNode(startNode)) {
21534
21700
  console.warn("Start node found for range start is not a text a node");
21535
21701
  console.log(editableDiv.childNodes);
@@ -21545,6 +21711,7 @@ function setRangeParams(editableDiv, params) {
21545
21711
  endNode = findFirstTextNode(editableDiv);
21546
21712
  endOffset = 0;
21547
21713
  }
21714
+ if (endNode === null) return;
21548
21715
  if (!isTextNode(endNode)) {
21549
21716
  console.warn("End node found for range start is not a text a node");
21550
21717
  console.log(editableDiv.childNodes);
@@ -21557,7 +21724,7 @@ function setRangeParams(editableDiv, params) {
21557
21724
  }
21558
21725
  new_range = document.createRange();
21559
21726
  if (startOffset > startNode.nodeValue.length) {
21560
- console.warn("endOffset > endNode.nodeValue.length when setting range: " + startOffset + " > " + startNode.nodeValue.length + " - " + stateNode.nodeType + " - " + startNode.nodeValue);
21727
+ console.warn("endOffset > endNode.nodeValue.length when setting range: " + startOffset + " > " + startNode.nodeValue.length + " - " + startNode.nodeType + " - " + startNode.nodeValue);
21561
21728
  startOffset = startNode.nodeValue.length;
21562
21729
  }
21563
21730
  new_range.setStart(startNode, startOffset);
@@ -21572,6 +21739,14 @@ function setRangeParams(editableDiv, params) {
21572
21739
  var selection = window.getSelection();
21573
21740
  selection.removeAllRanges();
21574
21741
  selection.addRange(new_range);
21742
+ debugMathRichInput("setRangeParams:after", {
21743
+ params: params,
21744
+ startNode: describeNode(startNode, editableDiv),
21745
+ startOffset: startOffset,
21746
+ endNode: describeNode(endNode, editableDiv),
21747
+ endOffset: endOffset,
21748
+ selectionAfter: getSelectionSnapshot(editableDiv)
21749
+ });
21575
21750
  } catch (error) {
21576
21751
  console.error(error);
21577
21752
  console.log("Trying to set range:");
@@ -21798,6 +21973,29 @@ var SMALL_SPACE_LENGTH = SMALL_SPACE.length;
21798
21973
  // const SMALL_SPACE_REG_EXP = new RegExp(SMALL_SPACE, "g")
21799
21974
 
21800
21975
  var MARK = getMark();
21976
+ var INLINE_STYLE_COMMANDS = {
21977
+ bold: "bold",
21978
+ italic: "italic",
21979
+ underline: "underline",
21980
+ subscript: "subscript",
21981
+ superscript: "superscript"
21982
+ };
21983
+ var INLINE_STYLE_TAGS = {
21984
+ bold: ["b", "strong"],
21985
+ italic: ["i", "em"],
21986
+ underline: ["u"],
21987
+ subscript: ["sub"],
21988
+ superscript: ["sup"]
21989
+ };
21990
+ var INLINE_STYLE_TAG_NAMES = ["b", "strong", "i", "em", "u", "sub", "sup"];
21991
+ var INLINE_STYLE_WRAPPER_TAGS = {
21992
+ bold: "strong",
21993
+ italic: "em",
21994
+ underline: "u",
21995
+ subscript: "sub",
21996
+ superscript: "sup"
21997
+ };
21998
+ var EMPTY_INLINE_FORMATTING_REG_EXP = /<(B|STRONG|I|EM|U|SUB|SUP)>(?:\s|&nbsp;|\u00a0|\u200b|\ufeff)*<\/\1>/gi;
21801
21999
 
21802
22000
  // const LATEX_MARKER_START = "<annotation encoding=\"application/x-tex\">"
21803
22001
  // const LATEX_MARKER_END = "</annotation>"
@@ -21939,12 +22137,34 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
21939
22137
  return countNodeTypes(_this2.editableDiv);
21940
22138
  });
21941
22139
  _defineProperty(_this2, "applyChangesToComponent", function (value, mimeType, rangeParams, isExpertMode, selectedTab) {
22140
+ var applyOptions = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};
21942
22141
  // rangeParams, isExpertMode and selectedTab can be null, in which case we will set them
21943
22142
  if (rangeParams === null) rangeParams = _this2.lastSetRangeParams;
21944
- _this2.afterComponentUpdateData = {
22143
+ debugMathRichInput("applyChangesToComponent:start", {
22144
+ value: value,
22145
+ mimeType: mimeType,
21945
22146
  rangeParams: rangeParams,
21946
- value: value
21947
- };
22147
+ propsValue: _this2.props.value,
22148
+ propsMimeType: _this2.props.mimeType,
22149
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
22150
+ selection: getSelectionSnapshot(_this2.editableDiv),
22151
+ skipControlledRender: applyOptions.skipControlledRender === true
22152
+ });
22153
+ if (applyOptions.skipControlledRender === true) {
22154
+ // Native contenteditable input has already mutated the DOM and kept the
22155
+ // browser selection in the right place. Re-rendering the same value via
22156
+ // dangerouslySetInnerHTML can replace text nodes under React 19 and move
22157
+ // the caret to the beginning. Programmatic edits still use the normal
22158
+ // render-and-restore path.
22159
+ _this2.skipNextControlledValueRender = true;
22160
+ _this2.skipNextControlledValue = value;
22161
+ _this2.afterComponentUpdateData = null;
22162
+ } else {
22163
+ _this2.afterComponentUpdateData = {
22164
+ rangeParams: rangeParams,
22165
+ value: value
22166
+ };
22167
+ }
21948
22168
  var options = _this2.props.options || DEFAULT_OPTIONS;
21949
22169
  if (isExpertMode === null) isExpertMode = options.isExpertMode;
21950
22170
  if (selectedTab === null) selectedTab = options.selectedTab;
@@ -21968,6 +22188,10 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
21968
22188
  isExpertMode: isExpertMode,
21969
22189
  selectedTab: selectedTab
21970
22190
  });
22191
+ debugMathRichInput("applyChangesToComponent:after-onChange", {
22192
+ afterComponentUpdateData: _this2.afterComponentUpdateData,
22193
+ selection: getSelectionSnapshot(_this2.editableDiv)
22194
+ });
21971
22195
  });
21972
22196
  _defineProperty(_this2, "initialiseHistory", function () {
21973
22197
  if (_this2.initialHistoryState == null) _this2.initialHistoryState = {
@@ -22011,14 +22235,43 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22011
22235
  });
22012
22236
  _defineProperty(_this2, "_setRangeParams", function (params) {
22013
22237
  try {
22238
+ debugMathRichInput("MathRichInput:_setRangeParams:start", {
22239
+ params: params,
22240
+ selectionBefore: getSelectionSnapshot(_this2.editableDiv)
22241
+ });
22014
22242
  setRangeParams(_this2.editableDiv, params);
22015
22243
  _this2.setOldRangeParams(params);
22016
- _this2.lastSetRangeParmas = params;
22244
+ _this2.lastSetRangeParams = params;
22245
+ debugMathRichInput("MathRichInput:_setRangeParams:after", {
22246
+ params: params,
22247
+ oldRangeParams: _this2.oldRangeParams,
22248
+ selectionAfter: getSelectionSnapshot(_this2.editableDiv)
22249
+ });
22017
22250
  } catch (error) {
22018
22251
  console.error(error);
22019
22252
  // console.log(this.editableDiv.childNodes);
22020
22253
  }
22021
22254
  });
22255
+ _defineProperty(_this2, "restoreRangeAfterBrowserWork", function (params, reason) {
22256
+ if (params === null || params === undefined) return;
22257
+ var restore = function restore(delay) {
22258
+ window.setTimeout(function () {
22259
+ if (_this2.state.showEquationEditor) return;
22260
+ if (!_this2.state.hasFocus && document.activeElement !== _this2.editableDiv) {
22261
+ return;
22262
+ }
22263
+ debugMathRichInput("restoreRangeAfterBrowserWork", {
22264
+ reason: reason,
22265
+ delay: delay,
22266
+ params: params,
22267
+ selectionBefore: getSelectionSnapshot(_this2.editableDiv)
22268
+ });
22269
+ _this2._setRangeParams(params);
22270
+ }, delay);
22271
+ };
22272
+ restore(0);
22273
+ restore(50);
22274
+ });
22022
22275
  _defineProperty(_this2, "handleKeyDownBackspace", function (e) {
22023
22276
  var rangeParams = _this2._getRangeParams();
22024
22277
  var start_node_index = rangeParams.startNodeIndex;
@@ -22146,7 +22399,11 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22146
22399
  _defineProperty(_this2, "isWordModifier", function (e) {
22147
22400
  var _this2$getPlatformMod = _this2.getPlatformModifiers(),
22148
22401
  isMac = _this2$getPlatformMod.isMac;
22149
- return isMac ? e.altKey : e.ctrlKey;
22402
+ // Borough production is pinned to math-rich-input 0.1.48, where Option +
22403
+ // Arrow did not get custom word navigation. Keep Mac Option + Arrow as
22404
+ // normal character movement to preserve the editor's existing UX; Command +
22405
+ // Arrow is handled by isLineModifier below.
22406
+ return isMac ? false : e.ctrlKey;
22150
22407
  });
22151
22408
  // Check if line navigation modifier is pressed
22152
22409
  _defineProperty(_this2, "isLineModifier", function (e) {
@@ -22603,6 +22860,17 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22603
22860
  });
22604
22861
  _defineProperty(_this2, "handleKeyDown", function (e) {
22605
22862
  try {
22863
+ debugMathRichInput("handleKeyDown:start", {
22864
+ key: e.key,
22865
+ altKey: e.altKey,
22866
+ metaKey: e.metaKey,
22867
+ ctrlKey: e.ctrlKey,
22868
+ shiftKey: e.shiftKey,
22869
+ hasFocus: _this2.state.hasFocus,
22870
+ propsValue: _this2.props.value,
22871
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
22872
+ selection: getSelectionSnapshot(_this2.editableDiv)
22873
+ });
22606
22874
  // Lets set up or update the undo redo history
22607
22875
  _this2.initialiseHistory();
22608
22876
  if ((e.key === "z" || e.key === "Z") && (e.ctrlKey || e.metaKey)) {
@@ -22659,7 +22927,9 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22659
22927
  }
22660
22928
  }
22661
22929
  }
22662
- if (e.altKey && e.key === "Alt") {
22930
+ var _this2$getPlatformMod3 = _this2.getPlatformModifiers(),
22931
+ isMac = _this2$getPlatformMod3.isMac;
22932
+ if (!isMac && e.altKey && e.key === "Alt") {
22663
22933
  var key = _this2.fetchAccentLetter();
22664
22934
  if (key === "a" || key === "e" || key === "i" || key === "o" || key === "u" || key === "c" || key === "l" || key === "n" || key === "s" || key === "?" || key === "!") {
22665
22935
  // console.log("showing/hiding accent bar");
@@ -22676,9 +22946,11 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22676
22946
  return;
22677
22947
  }
22678
22948
  _this2.keyPressed = e.key; // store for use in methods
22679
- _this2.setState({
22680
- keyPressed: e.key
22681
- });
22949
+ if (_this2.state.showAccentBar) {
22950
+ _this2.setState({
22951
+ keyPressed: e.key
22952
+ });
22953
+ }
22682
22954
 
22683
22955
  // Hide the accent bar if a key is typed except when it is an automatic key repeat press
22684
22956
  // If it is automatic key repeat, then keyPressStartTime will be greater than 0
@@ -22698,10 +22970,16 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22698
22970
  }
22699
22971
  if (e.key === "ArrowLeft") {
22700
22972
  _this2.handleKeyDownArrowLeft(e);
22973
+ debugMathRichInput("handleKeyDown:after-arrow-left", {
22974
+ selection: getSelectionSnapshot(_this2.editableDiv)
22975
+ });
22701
22976
  return;
22702
22977
  }
22703
22978
  if (e.key === "ArrowRight") {
22704
22979
  _this2.handleKeyDownArrowRight(e);
22980
+ debugMathRichInput("handleKeyDown:after-arrow-right", {
22981
+ selection: getSelectionSnapshot(_this2.editableDiv)
22982
+ });
22705
22983
  return;
22706
22984
  }
22707
22985
  if (e.key === "ArrowUp") {
@@ -22756,6 +23034,10 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22756
23034
 
22757
23035
  // Stop editing if within rendered katex node, if somehow the cursor ends up there (it shouldn't)
22758
23036
  var params = _this2._getRangeParams();
23037
+ // No range means no caret to protect — a field the browser has not placed a selection in yet. It
23038
+ // reads as null on the first keystroke after a click on a toolbar button, and dereferencing it threw
23039
+ // on every Cmd, every Cmd+A and every Cmd+V the teacher pressed.
23040
+ if (params === null || params === undefined) return;
22759
23041
  var startNode = _this2._findNodeWithIndex(params.startNodeIndex);
22760
23042
  var endNode = _this2._findNodeWithIndex(params.endNodeIndex);
22761
23043
  if (!_this2.editableDivIsPlainText()) {
@@ -22794,13 +23076,26 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22794
23076
  });
22795
23077
  _defineProperty(_this2, "handleKeyUp", function (e) {
22796
23078
  try {
23079
+ debugMathRichInput("handleKeyUp:start", {
23080
+ key: e.key,
23081
+ altKey: e.altKey,
23082
+ metaKey: e.metaKey,
23083
+ ctrlKey: e.ctrlKey,
23084
+ shiftKey: e.shiftKey,
23085
+ selection: getSelectionSnapshot(_this2.editableDiv)
23086
+ });
22797
23087
  _this2.keyPressed = e.key;
22798
23088
  _this2.keyPressStartTime = -1;
22799
23089
 
22800
23090
  // If the cursor is in a katex node, then move it to the next node (unless there is no
22801
23091
  // net node in which case move it to the previous node)
22802
23092
  if (e.key === "ArrowRight") {
23093
+ // `_getRangeParams` returns null whenever the document's selection is not inside this field —
23094
+ // which happens in ordinary use, not only in error: a toolbar button taking focus is enough. It
23095
+ // was dereferenced straight away, so the key handler THREW and everything after it in the same
23096
+ // press was skipped.
22803
23097
  var rangeParams = _this2._getRangeParams();
23098
+ if (!rangeParams) return;
22804
23099
  var index = rangeParams.startNodeIndex;
22805
23100
  if (index >= 0) {
22806
23101
  if (isKatexNode(_this2._findNodeWithIndex(index))) {
@@ -22829,7 +23124,9 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22829
23124
  }
22830
23125
  }
22831
23126
  if (e.key === "ArrowLeft") {
23127
+ // Same guard, same reason — see ArrowRight above.
22832
23128
  var _rangeParams = _this2._getRangeParams();
23129
+ if (!_rangeParams) return;
22833
23130
  var _index = _rangeParams.startNodeIndex;
22834
23131
 
22835
23132
  // If the cursor is in a katex node, then move it to the previous node (unless there is no
@@ -22869,8 +23166,22 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22869
23166
  try {
22870
23167
  // console.log("handleInput")
22871
23168
  if (_this2.isComposing) return;
23169
+ debugMathRichInput("handleInput:start", {
23170
+ inputType: event && event.nativeEvent ? event.nativeEvent.inputType : null,
23171
+ data: event && event.nativeEvent ? event.nativeEvent.data : null,
23172
+ propsValue: _this2.props.value,
23173
+ propsMimeType: _this2.props.mimeType,
23174
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
23175
+ selection: getSelectionSnapshot(_this2.editableDiv)
23176
+ });
23177
+ _this2.cleanupEmptyInlineFormattingElements();
22872
23178
  var raw_text = elementToMarkedRawText(_this2.editableDiv, null, 0, _this2.enableHtml());
22873
23179
  var params = _this2._getRangeParams();
23180
+ debugMathRichInput("handleInput:after-range", {
23181
+ raw_text: raw_text,
23182
+ params: params,
23183
+ selection: getSelectionSnapshot(_this2.editableDiv)
23184
+ });
22874
23185
  if (params === null || params === undefined) {
22875
23186
  // console.log(
22876
23187
  // "handleInput: Empty content detected, using default range params"
@@ -22889,7 +23200,10 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22889
23200
  var _mimeType = _this2.getMimeType(_nodeCounts.html > 0, _nodeCounts.math > 0);
22890
23201
  var _raw_text = elementToMarkedRawText(_this2.editableDiv, null, 0, _this2.enableHtml());
22891
23202
  _raw_text = removeEncodingIfPlainText(_raw_text, _mimeType, _this2.enableHtml());
22892
- _this2.applyChangesToComponent(_raw_text, _mimeType, defaultParams, _this2.props.useExpertMode, _this2.props.selectedTab);
23203
+ _raw_text = _this2.stripEmptyInlineFormattingTags(_raw_text);
23204
+ _this2.applyChangesToComponent(_raw_text, _mimeType, defaultParams, _this2.props.useExpertMode, _this2.props.selectedTab, {
23205
+ skipControlledRender: true
23206
+ });
22893
23207
  return;
22894
23208
  } else if (params.startNodeIndex === null || params.startNodeIndex === undefined) {
22895
23209
  // console.log(
@@ -22910,6 +23224,7 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22910
23224
  var hasMath = hasMathContent || nodeCounts.math > 0;
22911
23225
  var mimeType = _this2.getMimeType(hasHtml, hasMath);
22912
23226
  raw_text = removeEncodingIfPlainText(raw_text, mimeType, _this2.enableHtml());
23227
+ raw_text = _this2.stripEmptyInlineFormattingTags(raw_text);
22913
23228
 
22914
23229
  // Use valid params or fallback to defaults
22915
23230
  var validParams = params || {
@@ -22920,7 +23235,16 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22920
23235
  startGlobalOffset: 0,
22921
23236
  endGlobalOffset: 0
22922
23237
  };
22923
- _this2.applyChangesToComponent(raw_text, mimeType, validParams, _this2.props.useExpertMode, _this2.props.selectedTab);
23238
+ _this2.setOldRangeParams(validParams);
23239
+ _this2.applyChangesToComponent(raw_text, mimeType, validParams, _this2.props.useExpertMode, _this2.props.selectedTab, {
23240
+ skipControlledRender: true
23241
+ });
23242
+ debugMathRichInput("handleInput:after-apply", {
23243
+ raw_text: raw_text,
23244
+ mimeType: mimeType,
23245
+ validParams: validParams,
23246
+ selection: getSelectionSnapshot(_this2.editableDiv)
23247
+ });
22924
23248
  } catch (error) {
22925
23249
  console.error(error);
22926
23250
  }
@@ -22965,44 +23289,8 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
22965
23289
  });
22966
23290
  return;
22967
23291
  }
22968
- var node = _this2._findNodeWithIndex(rangeParams.startNodeIndex);
22969
- var bold = false;
22970
- var italic = false;
22971
- var underline = false;
22972
- var superscript = false;
22973
- var subscript = false;
22974
- do {
22975
- if (node.nodeType === 1) {
22976
- var nodeName = node.nodeName.toLowerCase();
22977
- if (nodeName === "b" || nodeName === "strong") bold = true;else if (nodeName === "i" || nodeName === "em") italic = true;else if (nodeName === "u") underline = true;else if (nodeName === "sub") subscript = true;else if (nodeName === "sup") superscript = true;
22978
- } else if (node.classList !== null && node.classList !== undefined && node.classList.contains("MathRichInput")) break;
22979
- node = node.parentNode;
22980
- } while (node !== null);
22981
- var activeButtons = {
22982
- bold: bold,
22983
- italic: italic,
22984
- underline: underline,
22985
- subscript: subscript,
22986
- superscript: superscript
22987
- };
22988
- // console.log(activeButtons)
22989
-
22990
- var update = true;
22991
- if (_this2.lastSetActiveButtons !== null) {
22992
- update = false;
22993
- for (var style in activeButtons) {
22994
- if (activeButtons[style] !== _this2.lastSetActiveButtons[style]) {
22995
- update = true;
22996
- break;
22997
- }
22998
- }
22999
- }
23000
- if (update) {
23001
- _this2.lastSetActiveButtons = activeButtons;
23002
- _this2.setState({
23003
- activeButtons: activeButtons
23004
- });
23005
- }
23292
+ var activeButtons = _this2.getActiveButtonsFromSelection(rangeParams);
23293
+ _this2.setActiveButtonsIfChanged(activeButtons);
23006
23294
  } catch (error) {
23007
23295
  console.error(error);
23008
23296
  }
@@ -23054,8 +23342,29 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23054
23342
  var pasteHtml = clipboardData.getData("text/html");
23055
23343
  var pasteText = clipboardData.getData("text/plain");
23056
23344
 
23057
- // Get current cursor position
23058
- var rangeParams = _this2._getRangeParams();
23345
+ // Remove whatever was selected FIRST, then read the caret.
23346
+ //
23347
+ // The order used to be the other way round, and that is why pasting over a selection emptied the
23348
+ // field instead of replacing it. `rangeParams` is a NODE INDEX plus an offset, and
23349
+ // `deleteFromDocument()` removes the very nodes it counts: read before the delete, the index points
23350
+ // at something that no longer exists, so `_findNodeWithIndex` below finds nothing, the marked text
23351
+ // it builds carries no mark, and the pasted text is inserted nowhere. The value the component then
23352
+ // reports is correct while the editable div is left empty — which is exactly what a teacher sees.
23353
+ //
23354
+ // It only ever worked on an empty field because the canonical empty state is a single `<p>` holding
23355
+ // one small space: deleting that selection leaves the same node structure standing, so the stale
23356
+ // index still happens to resolve.
23357
+ // Read the caret BEFORE, so there is something to fall back on, and AGAIN after the delete.
23358
+ var paramsBeforeDelete = _this2._getRangeParams();
23359
+ var selection = window.getSelection();
23360
+ if (selection && selection.rangeCount > 0) {
23361
+ selection.deleteFromDocument();
23362
+ }
23363
+
23364
+ // The caret AFTER the delete is the one that counts: `rangeParams` is a node index, and the delete
23365
+ // removed the very nodes it counts. The pre-delete value is kept only as a fallback — bailing out
23366
+ // here having already emptied the selection is how a paste turns into a deletion.
23367
+ var rangeParams = _this2._getRangeParams() || paramsBeforeDelete;
23059
23368
  if (!rangeParams) {
23060
23369
  console.warn("Could not get range params for paste");
23061
23370
  return;
@@ -23063,12 +23372,6 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23063
23372
 
23064
23373
  // Store old range params for cursor positioning (like equation editor)
23065
23374
  _this2.setOldRangeParams(rangeParams);
23066
-
23067
- // Clear any existing selection
23068
- var selection = window.getSelection();
23069
- if (selection.rangeCount > 0) {
23070
- selection.deleteFromDocument();
23071
- }
23072
23375
  var finalText = "";
23073
23376
  var finalMimeType = _this2.props.mimeType || "text/html";
23074
23377
  var hasMathContent = false;
@@ -23171,38 +23474,41 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23171
23474
 
23172
23475
  // Insert the new content at the marked position
23173
23476
  var newRawText = removeMarks(insertCharacterBeforeMarks(markedText, finalText));
23174
-
23175
23477
  // Convert any remaining \[...\] LaTeX expressions to <math>...</math> format
23176
23478
  // This ensures consistency when paste adds <math> tags alongside existing \[...\] expressions
23177
23479
  if (_this2.enableMath()) {
23178
23480
  newRawText = newRawText.replace(/\\\[([^\]]*?)\\\]/g, "<math>$1</math>");
23179
23481
  }
23180
23482
 
23181
- // Calculate cursor position exactly like equation editor does
23483
+ /*
23484
+ * Where the caret lands after a paste: at the END of what was pasted.
23485
+ *
23486
+ * The node index walks to just past the last formula, and that part was always right. The OFFSET
23487
+ * within that node was not: it stopped at the boundary, so pasting "…multiply it by 3." left the
23488
+ * caret between the 3 and the full stop. Everything after the last `</math>` is plain text, and
23489
+ * plain text is the one thing whose rendered length is its written length — so the tail can
23490
+ * simply be stepped over. (Markup in the tail is not that, and keeps the old position rather than
23491
+ * a guessed one.)
23492
+ *
23493
+ * Deriving the position from scratch was tried twice and both attempts landed FURTHER away, each
23494
+ * needing an exact model of how the rendered document is measured and each getting a corner of it
23495
+ * wrong. Walking the structure that is already there needs no such model.
23496
+ */
23182
23497
  var newRangeParams = null;
23183
23498
  if (hasMathContent) {
23184
- // Use same approach as equation editor for math content
23185
23499
  var oldRangeParams = _this2.getOldRangeParams();
23186
23500
  var mathTagCount = (finalText.match(/<math>/gi) || []).length;
23187
- if (_this2.props.value === "") {
23188
- // Special case of previously empty text input (like equation editor)
23189
- newRangeParams = {
23190
- startNodeIndex: mathTagCount * 2,
23191
- startOffset: SMALL_SPACE_LENGTH,
23192
- endNodeIndex: mathTagCount * 2,
23193
- endOffset: SMALL_SPACE_LENGTH
23194
- };
23195
- } else {
23196
- // Normal case - position cursor after inserted math content (like equation editor)
23197
- newRangeParams = {
23198
- startNodeIndex: oldRangeParams.startNodeIndex + mathTagCount * 2,
23199
- startOffset: SMALL_SPACE_LENGTH,
23200
- endNodeIndex: oldRangeParams.startNodeIndex + mathTagCount * 2,
23201
- endOffset: SMALL_SPACE_LENGTH
23202
- };
23203
- }
23501
+ var startNodeIndex = _this2.props.value === "" ? mathTagCount * 2 : oldRangeParams.startNodeIndex + mathTagCount * 2;
23502
+ var lastMathEnd = finalText.toLowerCase().lastIndexOf("</math>");
23503
+ var tail = lastMathEnd === -1 ? "" : finalText.substring(lastMathEnd + "</math>".length);
23504
+ var startOffset = tail.indexOf("<") === -1 ? SMALL_SPACE_LENGTH + tail.length : SMALL_SPACE_LENGTH;
23505
+ newRangeParams = {
23506
+ startNodeIndex: startNodeIndex,
23507
+ startOffset: startOffset,
23508
+ endNodeIndex: startNodeIndex,
23509
+ endOffset: startOffset
23510
+ };
23204
23511
  } else {
23205
- // For text content, use global offset like before
23206
23512
  var newGlobalOffset = rangeParams.startGlobalOffset + finalText.length;
23207
23513
  newRangeParams = {
23208
23514
  startGlobalOffset: newGlobalOffset,
@@ -23225,6 +23531,23 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23225
23531
 
23226
23532
  // Apply the change using the component's standard flow
23227
23533
  _this2.applyChangesToComponent(cleanedRawText, actualMimeType, newRangeParams, _this2.props.useExpertMode, _this2.props.selectedTab);
23534
+
23535
+ // A paste cannot rely on being re-rendered. It removed the selected content from the DOM itself,
23536
+ // and if the host already holds the value being applied — the same words pasted back into the
23537
+ // field they came from — no prop changes, nothing re-renders, and the field is left empty while
23538
+ // the value is correct everywhere else. So: give a real render its chance, then put the content
23539
+ // back if none came.
23540
+ var rangeAfterPaste = newRangeParams;
23541
+ queueMicrotask(function () {
23542
+ try {
23543
+ if (_this2.reconcileEditableDomWithRender(true)) {
23544
+ _this2._setRangeParams(rangeAfterPaste);
23545
+ _this2.updateActiveButtons();
23546
+ }
23547
+ } catch (error) {
23548
+ console.error(error);
23549
+ }
23550
+ });
23228
23551
  }
23229
23552
  } catch (error) {
23230
23553
  console.error("Error in paste handler:", error);
@@ -23237,6 +23560,14 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23237
23560
  var node = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
23238
23561
  try {
23239
23562
  var rangeParams = _this2._getRangeParams();
23563
+ debugMathRichInput("showEquationEditor:start", {
23564
+ moveCursorOnInsert: moveCursorOnInsert,
23565
+ nodeProvided: Boolean(node),
23566
+ rangeParams: rangeParams,
23567
+ oldRangeParams: _this2.oldRangeParams,
23568
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
23569
+ selection: getSelectionSnapshot(_this2.editableDiv)
23570
+ });
23240
23571
  if (rangeParams.startNodeIndex < 0) {
23241
23572
  console.error("Could not find position of cursor in node list");
23242
23573
  _this2.setOldRangeParams({
@@ -23279,6 +23610,7 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23279
23610
  _this2.editingExistingEquation = false;
23280
23611
  _this2.editingNode = null;
23281
23612
  }
23613
+ _this2.setOldRangeParams(rangeParams);
23282
23614
  _this2.markedRawText = elementToMarkedRawText(_this2.editableDiv, node, offset, _this2.enableHtml());
23283
23615
  // console.log("Marked text:"+this.markedRawText)
23284
23616
  _this2.moveCursorOnInsert = moveCursorOnInsert;
@@ -23286,11 +23618,21 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23286
23618
  showEquationEditor: true,
23287
23619
  equationEditorLatex: latex
23288
23620
  });
23621
+ debugMathRichInput("showEquationEditor:after-setState", {
23622
+ rangeParams: rangeParams,
23623
+ markedRawText: _this2.markedRawText,
23624
+ latex: latex,
23625
+ selection: getSelectionSnapshot(_this2.editableDiv)
23626
+ });
23289
23627
  } catch (error) {
23290
23628
  console.error(error);
23291
23629
  }
23292
23630
  });
23293
23631
  _defineProperty(_this2, "handleMouseDown", function (event) {
23632
+ _this2.isMouseDownInEditable = true;
23633
+ window.setTimeout(function () {
23634
+ _this2.isMouseDownInEditable = false;
23635
+ }, 0);
23294
23636
  _this2.showEquationEditorOnMouseUp = false;
23295
23637
  var node = null;
23296
23638
  try {
@@ -23325,7 +23667,12 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23325
23667
 
23326
23668
  // Clear selection anchor on mouse click
23327
23669
  _this2.selectionAnchor = null;
23670
+
23671
+ // A click that lands with the selection outside this field — a toolbar button, another field —
23672
+ // gives no range at all. Dereferenced unguarded, this threw on every such click.
23328
23673
  var params = _this2._getRangeParams();
23674
+ if (!params) return;
23675
+ _this2.setOldRangeParams(params);
23329
23676
  var startNode = _this2._findNodeWithIndex(params.startNodeIndex);
23330
23677
  var endNode = _this2._findNodeWithIndex(params.endNodeIndex);
23331
23678
  if (startNode === null || endNode === null) {
@@ -23381,7 +23728,15 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23381
23728
  _defineProperty(_this2, "handleEquationEditorInsert", function (latex) {
23382
23729
  try {
23383
23730
  // console.log("Inserting latex: "+latex)
23384
-
23731
+ debugMathRichInput("handleEquationEditorInsert:start", {
23732
+ latex: latex,
23733
+ markedRawText: _this2.markedRawText,
23734
+ oldRangeParams: _this2.getOldRangeParams(),
23735
+ moveCursorOnInsert: _this2.moveCursorOnInsert,
23736
+ propsValue: _this2.props.value,
23737
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
23738
+ selection: getSelectionSnapshot(_this2.editableDiv)
23739
+ });
23385
23740
  var start_pos = _this2.markedRawText.indexOf(MARK);
23386
23741
  var end_pos = _this2.markedRawText.indexOf(MARK, start_pos + MARK.length);
23387
23742
  var oldRangeParams = _this2.getOldRangeParams();
@@ -23423,6 +23778,10 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23423
23778
  _this2.applyChangesToComponent(_new_raw_text, _mimeType2, _new_rangeParams7, _this2.props.useExpertMode, _this2.props.selectedTab);
23424
23779
  _this2.setState({
23425
23780
  showEquationEditor: false
23781
+ }, function () {
23782
+ _this2.editableDiv.focus();
23783
+ _this2._setRangeParams(_new_rangeParams7);
23784
+ _this2.restoreRangeAfterBrowserWork(_new_rangeParams7, "equation-empty");
23426
23785
  });
23427
23786
  return;
23428
23787
  }
@@ -23441,28 +23800,48 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23441
23800
  endOffset: SMALL_SPACE_LENGTH
23442
23801
  };
23443
23802
  } else {
23444
- // Position cursor after the inserted math element using globalOffset
23445
- // Find the position right after the last math tag in the content
23446
- var mathTagMatches = _toConsumableArray(new_raw_text.matchAll(/<math>[^<]*<\/math>/gi));
23447
- if (mathTagMatches.length > 0) {
23448
- var lastMatch = mathTagMatches[mathTagMatches.length - 1];
23449
- var afterMathOffset = lastMatch.index + lastMatch[0].length;
23803
+ // In this editor a rendered equation counts as one logical character
23804
+ // in global-offset space. Put the caret immediately after that math
23805
+ // character instead of guessing rendered child-node indexes.
23806
+ var _oldRangeParams = _this2.getOldRangeParams();
23807
+ if (_oldRangeParams !== null && _oldRangeParams.startGlobalOffset !== null && _oldRangeParams.startGlobalOffset !== undefined) {
23808
+ var afterInsertedMathOffset = _oldRangeParams.startGlobalOffset + 1;
23450
23809
  new_rangeParams = {
23451
- startGlobalOffset: afterMathOffset,
23452
- endGlobalOffset: afterMathOffset
23810
+ startGlobalOffset: afterInsertedMathOffset,
23811
+ endGlobalOffset: afterInsertedMathOffset
23812
+ };
23813
+ } else if (_oldRangeParams !== null && _oldRangeParams.startNodeIndex !== null && _oldRangeParams.startNodeIndex !== undefined) {
23814
+ new_rangeParams = {
23815
+ startNodeIndex: _oldRangeParams.startNodeIndex + 2,
23816
+ startOffset: SMALL_SPACE_LENGTH,
23817
+ endNodeIndex: _oldRangeParams.startNodeIndex + 2,
23818
+ endOffset: SMALL_SPACE_LENGTH
23453
23819
  };
23454
23820
  }
23455
- // If no math tags found, keep old range params
23821
+ // If old range params are unavailable, keep the current fallback.
23456
23822
  }
23457
23823
  }
23458
23824
  // console.log("New cursor pos: "+new_rangeParams.startNodeIndex+"->"+new_rangeParams.startOffset)
23459
23825
  _this2.setOldRangeParams(new_rangeParams);
23826
+ debugMathRichInput("handleEquationEditorInsert:before-apply", {
23827
+ new_raw_text: new_raw_text,
23828
+ new_rangeParams: new_rangeParams,
23829
+ selection: getSelectionSnapshot(_this2.editableDiv)
23830
+ });
23460
23831
  var nodeCounts = _this2._countNodeTypes();
23461
23832
  var mimeType = _this2.getMimeType(nodeCounts.html > 0, true);
23462
23833
  new_raw_text = removeEncodingIfPlainText(new_raw_text, mimeType, _this2.enableHtml());
23463
23834
  _this2.applyChangesToComponent(new_raw_text, mimeType, new_rangeParams, _this2.props.useExpertMode, _this2.props.selectedTab);
23464
23835
  _this2.setState({
23465
23836
  showEquationEditor: false
23837
+ }, function () {
23838
+ _this2.editableDiv.focus();
23839
+ _this2._setRangeParams(new_rangeParams);
23840
+ _this2.restoreRangeAfterBrowserWork(new_rangeParams, "equation-insert");
23841
+ debugMathRichInput("handleEquationEditorInsert:after-close-restore", {
23842
+ new_rangeParams: new_rangeParams,
23843
+ selection: getSelectionSnapshot(_this2.editableDiv)
23844
+ });
23466
23845
  });
23467
23846
  } catch (error) {
23468
23847
  console.error(error);
@@ -23479,13 +23858,22 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23479
23858
  _defineProperty(_this2, "styleText", function (event, style) {
23480
23859
  try {
23481
23860
  style = style.toLowerCase();
23861
+ var command = _this2.getStyleCommand(style);
23862
+ var activeBefore = _this2.getActiveButtonsFromSelection();
23863
+ var selection = document.getSelection();
23864
+ var isCollapsed = selection === null || selection.rangeCount === 0 || selection.getRangeAt(0).collapsed;
23482
23865
  var success = false;
23866
+ var insertedTypingStylePlaceholder = false;
23483
23867
 
23484
23868
  // This first part is a workaround whereby executing subscript or superscript
23485
23869
  // within an existing subscript or superscript makes the text even smaller instead
23486
23870
  // of toggling it off. Thus we manually toggle it off with some jigerry pokery!
23487
- if (style === "subscript" && _this2.state.activeButtons.subscript || style === "superscript" && _this2.state.activeButtons.superscript) {
23488
- var selection = document.getSelection();
23871
+ if (isCollapsed && activeBefore[style] && _this2.moveTrailingWhitespaceOutsideInlineStyle(style, command)) {
23872
+ success = true;
23873
+ } else if (isCollapsed && !activeBefore[style] && selection && selection.rangeCount > 0) {
23874
+ success = _this2.insertInlineStylePlaceholder(style);
23875
+ insertedTypingStylePlaceholder = success;
23876
+ } else if (style === "subscript" && activeBefore.subscript || style === "superscript" && activeBefore.superscript) {
23489
23877
  var range = selection.getRangeAt(0);
23490
23878
  if (range.endContainer === range.startContainer && range.endOffset === range.startOffset) {
23491
23879
  success = document.execCommand("insertText", false, " ");
@@ -23494,78 +23882,21 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23494
23882
  selection.addRange(range);
23495
23883
  }
23496
23884
  success = document.execCommand("removeFormat", false, null);
23497
- if (_this2.state.activeButtons.bold) success = document.execCommand("bold", false, null);
23498
- if (_this2.state.activeButtons.italic) success = document.execCommand("italic", false, null);
23499
- if (_this2.state.activeButtons.underline) success = document.execCommand("underline", false, null);
23885
+ if (activeBefore.bold) success = document.execCommand("bold", false, null);
23886
+ if (activeBefore.italic) success = document.execCommand("italic", false, null);
23887
+ if (activeBefore.underline) success = document.execCommand("underline", false, null);
23500
23888
  } else {
23501
- var _selection = document.getSelection();
23502
- if (_selection && _selection.rangeCount > 0) {
23503
- var _range = _selection.getRangeAt(0);
23504
- if (_range.collapsed) {
23505
- // For cursor position (no selection), use insertHTML with styled wrapper
23506
- // This creates a formatted element that will maintain styling for future typing
23507
- var isToggleOff = _this2.state.activeButtons[style];
23508
- if (!isToggleOff) {
23509
- // Turning style ON - insert a styled wrapper element
23510
- var wrapperTag;
23511
- switch (style) {
23512
- case "bold":
23513
- wrapperTag = "strong";
23514
- break;
23515
- case "italic":
23516
- wrapperTag = "em";
23517
- break;
23518
- case "underline":
23519
- wrapperTag = "u";
23520
- break;
23521
- case "subscript":
23522
- wrapperTag = "sub";
23523
- break;
23524
- case "superscript":
23525
- wrapperTag = "sup";
23526
- break;
23527
- default:
23528
- wrapperTag = "span";
23529
- break;
23530
- }
23531
-
23532
- // Insert an invisible character wrapped in the styled element
23533
- // This provides a "landing zone" for future typing
23534
- var styledElement = "<".concat(wrapperTag, ">\u200B</").concat(wrapperTag, ">");
23535
- success = document.execCommand("insertHTML", false, styledElement);
23536
-
23537
- // Move cursor inside the styled element
23538
- setTimeout(function () {
23539
- var newSelection = document.getSelection();
23540
- if (newSelection && newSelection.rangeCount > 0) {
23541
- var newRange = newSelection.getRangeAt(0);
23542
- var styledNode = newRange.startContainer.parentElement;
23543
- if (styledNode && styledNode.tagName.toLowerCase() === wrapperTag) {
23544
- newRange.setStart(styledNode, 0);
23545
- newRange.setEnd(styledNode, 0);
23546
- newSelection.removeAllRanges();
23547
- newSelection.addRange(newRange);
23548
- }
23549
- }
23550
- }, 0);
23551
- } else {
23552
- // Turning style OFF - just apply execCommand normally
23553
- success = document.execCommand(style, false, null);
23554
- }
23555
- } else {
23556
- // For text selection, use regular execCommand
23557
- success = document.execCommand(style, false, null);
23558
- }
23889
+ if (selection && selection.rangeCount > 0) {
23890
+ success = document.execCommand(command, false, null);
23559
23891
  }
23560
23892
  }
23561
- var buttonState = _this2.state.activeButtons[style];
23562
- if (buttonState === null || buttonState === undefined) return success;
23563
- buttonState = !buttonState;
23564
- var new_activeButtons = _objectSpread2({}, _this2.state.activeButtons);
23565
- new_activeButtons[style] = buttonState;
23566
- _this2.setState({
23567
- activeButtons: new_activeButtons
23568
- });
23893
+ var rangeParams = _this2._getRangeParams();
23894
+ var removedEmptyTags = !insertedTypingStylePlaceholder && _this2.cleanupEmptyInlineFormattingElements();
23895
+ if (removedEmptyTags && rangeParams !== null && rangeParams !== undefined) {
23896
+ _this2._setRangeParams(rangeParams);
23897
+ _this2.applyCurrentEditableDomToComponent(rangeParams);
23898
+ }
23899
+ _this2.setActiveButtonsIfChanged(_this2.getActiveButtonsFromSelection(rangeParams));
23569
23900
  return success;
23570
23901
  } catch (error) {
23571
23902
  console.error(error);
@@ -23573,8 +23904,15 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23573
23904
  });
23574
23905
  _defineProperty(_this2, "handleToolbarButtonClick", function (event, buttonName) {
23575
23906
  try {
23907
+ debugMathRichInput("handleToolbarButtonClick:start", {
23908
+ buttonName: buttonName,
23909
+ eventType: event ? event.type : null,
23910
+ selection: getSelectionSnapshot(_this2.editableDiv),
23911
+ oldRangeParams: _this2.oldRangeParams
23912
+ });
23576
23913
  if (buttonName === "Equation") {
23577
23914
  _this2.showEquationEditor(true);
23915
+ return;
23578
23916
  }
23579
23917
  var style = buttonName;
23580
23918
  var success = _this2.styleText(event, style);
@@ -23886,13 +24224,36 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23886
24224
  });
23887
24225
  _defineProperty(_this2, "handleFocus", function (event) {
23888
24226
  var onFocus = _this2.props.onFocus;
24227
+ var currentRangeParams = null;
24228
+ try {
24229
+ currentRangeParams = _this2._getRangeParams();
24230
+ } catch (error) {
24231
+ currentRangeParams = null;
24232
+ }
24233
+ var savedRangeParams = _this2.getOldRangeParams();
24234
+ var shouldRestoreSavedRange = !_this2.isMouseDownInEditable && _this2.props.value !== "" && _this2.rangeIsAtStart(currentRangeParams) && _this2.rangeHasNonZeroPosition(savedRangeParams);
24235
+ debugMathRichInput("handleFocus:start", {
24236
+ propsValue: _this2.props.value,
24237
+ oldRangeParams: _this2.oldRangeParams,
24238
+ currentRangeParams: currentRangeParams,
24239
+ shouldRestoreSavedRange: shouldRestoreSavedRange,
24240
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
24241
+ selection: getSelectionSnapshot(_this2.editableDiv)
24242
+ });
23889
24243
  if (onFocus != null) {
23890
24244
  onFocus();
23891
24245
  }
23892
24246
  try {
23893
- _this2._setRangeParams(_this2.getOldRangeParams());
23894
24247
  _this2.setState({
23895
24248
  hasFocus: true
24249
+ }, function () {
24250
+ if (shouldRestoreSavedRange) {
24251
+ _this2._setRangeParams(savedRangeParams);
24252
+ _this2.restoreRangeAfterBrowserWork(savedRangeParams, "focus-restore");
24253
+ }
24254
+ debugMathRichInput("handleFocus:after-setState", {
24255
+ selection: getSelectionSnapshot(_this2.editableDiv)
24256
+ });
23896
24257
  });
23897
24258
  } catch (error) {
23898
24259
  console.error(error);
@@ -23900,6 +24261,22 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23900
24261
  });
23901
24262
  _defineProperty(_this2, "handleBlur", function (event) {
23902
24263
  var onBlur = _this2.props.onBlur;
24264
+ var currentRangeParams = null;
24265
+ try {
24266
+ currentRangeParams = _this2._getRangeParams();
24267
+ if (_this2.rangeHasNonZeroPosition(currentRangeParams)) {
24268
+ _this2.setOldRangeParams(currentRangeParams);
24269
+ }
24270
+ } catch (error) {
24271
+ currentRangeParams = null;
24272
+ }
24273
+ debugMathRichInput("handleBlur:start", {
24274
+ propsValue: _this2.props.value,
24275
+ oldRangeParams: _this2.oldRangeParams,
24276
+ currentRangeParams: currentRangeParams,
24277
+ innerHTML: _this2.editableDiv ? _this2.editableDiv.innerHTML : null,
24278
+ selection: getSelectionSnapshot(_this2.editableDiv)
24279
+ });
23903
24280
  if (onBlur != null) {
23904
24281
  onBlur();
23905
24282
  }
@@ -23909,6 +24286,9 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
23909
24286
  hasFocus: false,
23910
24287
  showAccentBar: false
23911
24288
  });
24289
+ debugMathRichInput("handleBlur:after-setState", {
24290
+ selection: getSelectionSnapshot(_this2.editableDiv)
24291
+ });
23912
24292
  } catch (error) {
23913
24293
  console.error(error);
23914
24294
  }
@@ -24040,18 +24420,20 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24040
24420
 
24041
24421
  // Elements that need to be set before the equation editor is shown
24042
24422
  _this2.markedRawText = null;
24043
- _this2.oldRangeParams = {
24044
- startNodeIndex: 0,
24045
- startOffset: 0,
24046
- endNodeIndex: 0,
24047
- endOffset: 0
24048
- };
24423
+ // No selection has been captured yet. Do not default this to the start of
24424
+ // the editor: React 19 focus/click ordering makes handleFocus restore that
24425
+ // synthetic 0,0 range and moves the caret to the beginning on every click.
24426
+ _this2.oldRangeParams = null;
24049
24427
  _this2.moveCursorOnInsert = false;
24050
24428
 
24051
24429
  // Elements defining the current state of range
24052
24430
  _this2.afterComponentUpdateData = null; // Set this to update the selection after rendering
24053
- _this2.lastSetRangeParmas = null; // This is set automatically each time the range params are set with setRangeParams
24054
-
24431
+ _this2.lastSetRangeParams = null; // This is set automatically each time the range params are set with setRangeParams
24432
+ _this2.skipNextControlledValueRender = false;
24433
+ _this2.skipNextControlledValue = null;
24434
+ _this2.isMouseDownInEditable = false;
24435
+ _this2.lastRenderedEditableHtml = null;
24436
+ _this2.preserveNativeDomRangeParams = null;
24055
24437
  _this2.lastSetActiveButtons = null;
24056
24438
  _this2.onChangeCallback = _this2.props.onChange;
24057
24439
 
@@ -24141,12 +24523,403 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24141
24523
  var params = _objectSpread2({}, this.oldRangeParams);
24142
24524
  return params;
24143
24525
  }
24526
+ }, {
24527
+ key: "rangeIsAtStart",
24528
+ value: function rangeIsAtStart(params) {
24529
+ if (params === null || params === undefined) return false;
24530
+ if (params.startGlobalOffset !== null && params.startGlobalOffset !== undefined && params.endGlobalOffset !== null && params.endGlobalOffset !== undefined) {
24531
+ return params.startGlobalOffset === 0 && params.endGlobalOffset === 0;
24532
+ }
24533
+ return params.startNodeIndex === 0 && params.endNodeIndex === 0 && params.startOffset === 0 && params.endOffset === 0;
24534
+ }
24535
+ }, {
24536
+ key: "rangeHasNonZeroPosition",
24537
+ value: function rangeHasNonZeroPosition(params) {
24538
+ if (params === null || params === undefined) return false;
24539
+ if (params.startGlobalOffset !== null && params.startGlobalOffset !== undefined) {
24540
+ return params.startGlobalOffset > 0;
24541
+ }
24542
+ return params.startNodeIndex > 0 || params.endNodeIndex > 0 || params.startOffset > 0 || params.endOffset > 0;
24543
+ }
24544
+ }, {
24545
+ key: "getCurrentRawTextForProps",
24546
+ value: function getCurrentRawTextForProps(nextProps) {
24547
+ if (!this.editableDiv) return null;
24548
+ try {
24549
+ var rawText = elementToMarkedRawText(this.editableDiv, null, 0, this.enableHtml());
24550
+ rawText = this.stripEmptyInlineFormattingTags(rawText);
24551
+ return removeEncodingIfPlainText(rawText, nextProps.mimeType, this.enableHtml());
24552
+ } catch (error) {
24553
+ debugMathRichInput("getCurrentRawTextForProps:error", {
24554
+ error: error,
24555
+ selection: getSelectionSnapshot(this.editableDiv)
24556
+ });
24557
+ return null;
24558
+ }
24559
+ }
24560
+ }, {
24561
+ key: "shouldSkipRedundantFocusedRender",
24562
+ value: function shouldSkipRedundantFocusedRender(nextProps, nextState) {
24563
+ if (!this.editableDiv) return false;
24564
+ if (nextState !== this.state) return false;
24565
+ if (nextState.hasFocus !== true) return false;
24566
+ if (this.afterComponentUpdateData !== null && this.afterComponentUpdateData !== undefined) {
24567
+ return false;
24568
+ }
24569
+ var currentRawText = this.getCurrentRawTextForProps(nextProps);
24570
+ var shouldSkip = currentRawText === nextProps.value;
24571
+ debugMathRichInput("shouldComponentUpdate:redundant-focused-check", {
24572
+ shouldSkip: shouldSkip,
24573
+ currentRawText: currentRawText,
24574
+ nextValue: nextProps.value,
24575
+ currentPropsValue: this.props.value,
24576
+ nextMimeType: nextProps.mimeType,
24577
+ currentMimeType: this.props.mimeType,
24578
+ selection: getSelectionSnapshot(this.editableDiv)
24579
+ });
24580
+ if (shouldSkip) {
24581
+ var currentRangeParams = this.getRangeParamsPreservingInlinePlaceholder();
24582
+ if (this.rangeHasNonZeroPosition(currentRangeParams)) {
24583
+ this.setOldRangeParams(currentRangeParams);
24584
+ }
24585
+ }
24586
+ return shouldSkip;
24587
+ }
24588
+ }, {
24589
+ key: "isSelectionInsideInlinePlaceholder",
24590
+ value: function isSelectionInsideInlinePlaceholder() {
24591
+ if (typeof document === "undefined") return false;
24592
+ var selection = document.getSelection ? document.getSelection() : null;
24593
+ if (!selection || selection.rangeCount === 0) return false;
24594
+ var range = selection.getRangeAt(0);
24595
+ if (!range.collapsed) return false;
24596
+ var node = range.startContainer;
24597
+ if (!node || node.nodeType !== 3) return false;
24598
+ if (node.nodeValue !== SMALL_SPACE) return false;
24599
+ if (range.startOffset !== SMALL_SPACE_LENGTH) return false;
24600
+ var parent = node.parentNode;
24601
+ if (!parent || parent.nodeType !== 1) return false;
24602
+ var tagName = parent.nodeName.toLowerCase();
24603
+ if (!INLINE_STYLE_TAG_NAMES.includes(tagName)) return false;
24604
+ return parent.textContent === SMALL_SPACE;
24605
+ }
24606
+ }, {
24607
+ key: "getRangeParamsPreservingInlinePlaceholder",
24608
+ value: function getRangeParamsPreservingInlinePlaceholder() {
24609
+ var selectionInsideInlinePlaceholder = this.isSelectionInsideInlinePlaceholder();
24610
+ var rangeParams = this._getRangeParams();
24611
+ if (!selectionInsideInlinePlaceholder || !rangeParams) {
24612
+ return rangeParams;
24613
+ }
24614
+
24615
+ // Global offsets ignore the zero-width placeholder; node index preserves it.
24616
+ return _objectSpread2(_objectSpread2({}, rangeParams), {}, {
24617
+ startGlobalOffset: null,
24618
+ endGlobalOffset: null
24619
+ });
24620
+ }
24621
+ }, {
24622
+ key: "getEditableHtmlForRender",
24623
+ value: function getEditableHtmlForRender(html) {
24624
+ var currentRawText = this.editableDiv ? this.getCurrentRawTextForProps(this.props) : null;
24625
+ var liveEditableHtml = this.editableDiv ? this.editableDiv.innerHTML : null;
24626
+ var canPreserveNativeDom = this.state.hasFocus === true && this.editableDiv !== null && this.editableDiv !== undefined && (this.afterComponentUpdateData === null || this.afterComponentUpdateData === undefined) && this.lastRenderedEditableHtml !== null && currentRawText === this.props.value;
24627
+ var preserveRangeParams = canPreserveNativeDom ? this.getRangeParamsPreservingInlinePlaceholder() : null;
24628
+ debugMathRichInput("render:editable-html", {
24629
+ canPreserveNativeDom: canPreserveNativeDom,
24630
+ selectionInsideInlinePlaceholder: this.isSelectionInsideInlinePlaceholder(),
24631
+ propsValue: this.props.value,
24632
+ generatedHtml: html,
24633
+ renderedHtml: canPreserveNativeDom ? liveEditableHtml : html,
24634
+ liveEditableHtml: liveEditableHtml,
24635
+ lastRenderedEditableHtml: this.lastRenderedEditableHtml,
24636
+ currentRawText: currentRawText,
24637
+ preserveRangeParams: preserveRangeParams,
24638
+ afterComponentUpdateData: this.afterComponentUpdateData,
24639
+ hasFocus: this.state.hasFocus,
24640
+ selection: getSelectionSnapshot(this.editableDiv)
24641
+ });
24642
+ if (canPreserveNativeDom) {
24643
+ this.lastRenderedEditableHtml = liveEditableHtml;
24644
+ this.preserveNativeDomRangeParams = preserveRangeParams;
24645
+ return liveEditableHtml;
24646
+ }
24647
+ this.lastRenderedEditableHtml = html;
24648
+ this.preserveNativeDomRangeParams = null;
24649
+ return html;
24650
+ }
24651
+ }, {
24652
+ key: "stripEmptyInlineFormattingTags",
24653
+ value: function stripEmptyInlineFormattingTags(rawText) {
24654
+ if (rawText === null || rawText === undefined) return rawText;
24655
+ var cleaned = rawText;
24656
+ var previous = null;
24657
+ while (previous !== cleaned) {
24658
+ previous = cleaned;
24659
+ cleaned = cleaned.replace(EMPTY_INLINE_FORMATTING_REG_EXP, "");
24660
+ }
24661
+ return cleaned;
24662
+ }
24663
+ }, {
24664
+ key: "cleanupEmptyInlineFormattingElements",
24665
+ value: function cleanupEmptyInlineFormattingElements() {
24666
+ if (!this.editableDiv) return false;
24667
+ var removed = false;
24668
+ this.editableDiv.querySelectorAll("b,strong,i,em,u,sub,sup").forEach(function (node) {
24669
+ var text = node.textContent || "";
24670
+ var hasMeaningfulText = text.replace(/[\s\u00a0\u200b\ufeff]/g, "") !== "";
24671
+ var hasProtectedContent = node.querySelector("br,img,math,.katex") !== null;
24672
+ if (!hasMeaningfulText && !hasProtectedContent) {
24673
+ node.remove();
24674
+ removed = true;
24675
+ }
24676
+ });
24677
+ if (removed) {
24678
+ this.editableDiv.normalize();
24679
+ }
24680
+ return removed;
24681
+ }
24682
+ }, {
24683
+ key: "getStyleCommand",
24684
+ value: function getStyleCommand(style) {
24685
+ return INLINE_STYLE_COMMANDS[style] || style;
24686
+ }
24687
+ }, {
24688
+ key: "insertInlineStylePlaceholder",
24689
+ value: function insertInlineStylePlaceholder(style) {
24690
+ var wrapperTag = INLINE_STYLE_WRAPPER_TAGS[style] || "span";
24691
+ var selection = document.getSelection();
24692
+ if (!selection || selection.rangeCount === 0) return false;
24693
+ var range = selection.getRangeAt(0);
24694
+ if (!range.collapsed) return false;
24695
+ range.deleteContents();
24696
+ var wrapper = document.createElement(wrapperTag);
24697
+ var textNode = document.createTextNode(SMALL_SPACE);
24698
+ wrapper.appendChild(textNode);
24699
+ range.insertNode(wrapper);
24700
+ var nextRange = document.createRange();
24701
+ nextRange.setStart(textNode, SMALL_SPACE_LENGTH);
24702
+ nextRange.collapse(true);
24703
+ selection.removeAllRanges();
24704
+ selection.addRange(nextRange);
24705
+ var rangeParams = this.getRangeParamsPreservingInlinePlaceholder();
24706
+ if (rangeParams) this.setOldRangeParams(rangeParams);
24707
+ debugMathRichInput("insertInlineStylePlaceholder:after", {
24708
+ style: style,
24709
+ wrapperTag: wrapperTag,
24710
+ innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
24711
+ rangeParams: rangeParams,
24712
+ selection: getSelectionSnapshot(this.editableDiv)
24713
+ });
24714
+ return true;
24715
+ }
24716
+ }, {
24717
+ key: "getActiveButtonsFromCommandState",
24718
+ value: function getActiveButtonsFromCommandState() {
24719
+ var query = function query(command) {
24720
+ try {
24721
+ return document.queryCommandState(command);
24722
+ } catch (error) {
24723
+ return false;
24724
+ }
24725
+ };
24726
+ return {
24727
+ bold: query("bold"),
24728
+ italic: query("italic"),
24729
+ underline: query("underline"),
24730
+ subscript: query("subscript"),
24731
+ superscript: query("superscript")
24732
+ };
24733
+ }
24734
+ }, {
24735
+ key: "isSelectionCollapsed",
24736
+ value: function isSelectionCollapsed() {
24737
+ var selection = document.getSelection();
24738
+ if (!selection || selection.rangeCount === 0) return true;
24739
+ return selection.getRangeAt(0).collapsed;
24740
+ }
24741
+ }, {
24742
+ key: "getActiveButtonsFromAncestorState",
24743
+ value: function getActiveButtonsFromAncestorState() {
24744
+ var rangeParams = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
24745
+ if (rangeParams === null || rangeParams === undefined) {
24746
+ rangeParams = this._getRangeParams();
24747
+ }
24748
+ var activeButtons = {
24749
+ bold: false,
24750
+ italic: false,
24751
+ underline: false,
24752
+ subscript: false,
24753
+ superscript: false
24754
+ };
24755
+ if (rangeParams === null || rangeParams === undefined || rangeParams.startNodeIndex < 0) {
24756
+ return activeButtons;
24757
+ }
24758
+ var node = this._findNodeWithIndex(rangeParams.startNodeIndex);
24759
+ if (node === null || node === undefined) return activeButtons;
24760
+ do {
24761
+ if (node.nodeType === 1) {
24762
+ var nodeName = node.nodeName.toLowerCase();
24763
+ if (nodeName === "b" || nodeName === "strong") activeButtons.bold = true;else if (nodeName === "i" || nodeName === "em") activeButtons.italic = true;else if (nodeName === "u") activeButtons.underline = true;else if (nodeName === "sub") activeButtons.subscript = true;else if (nodeName === "sup") activeButtons.superscript = true;
24764
+ } else if (node.classList !== null && node.classList !== undefined && node.classList.contains("MathRichInput")) break;
24765
+ node = node.parentNode;
24766
+ } while (node !== null);
24767
+ return activeButtons;
24768
+ }
24769
+ }, {
24770
+ key: "getActiveButtonsFromSelection",
24771
+ value: function getActiveButtonsFromSelection() {
24772
+ var rangeParams = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
24773
+ var commandState = this.getActiveButtonsFromCommandState();
24774
+ var ancestorState = this.getActiveButtonsFromAncestorState(rangeParams);
24775
+ return {
24776
+ bold: commandState.bold || ancestorState.bold,
24777
+ italic: commandState.italic || ancestorState.italic,
24778
+ underline: commandState.underline || ancestorState.underline,
24779
+ subscript: commandState.subscript || ancestorState.subscript,
24780
+ superscript: commandState.superscript || ancestorState.superscript
24781
+ };
24782
+ }
24783
+ }, {
24784
+ key: "setActiveButtonsIfChanged",
24785
+ value: function setActiveButtonsIfChanged(activeButtons) {
24786
+ var update = true;
24787
+ if (this.lastSetActiveButtons !== null) {
24788
+ update = false;
24789
+ for (var style in activeButtons) {
24790
+ if (activeButtons[style] !== this.lastSetActiveButtons[style]) {
24791
+ update = true;
24792
+ break;
24793
+ }
24794
+ }
24795
+ }
24796
+ if (update) {
24797
+ this.lastSetActiveButtons = activeButtons;
24798
+ this.setState({
24799
+ activeButtons: activeButtons
24800
+ });
24801
+ }
24802
+ }
24803
+ }, {
24804
+ key: "getInlineStyleAncestor",
24805
+ value: function getInlineStyleAncestor(style) {
24806
+ var tags = INLINE_STYLE_TAGS[style];
24807
+ if (!tags) return null;
24808
+ var selection = document.getSelection();
24809
+ if (!selection || selection.rangeCount === 0) return null;
24810
+ var node = selection.getRangeAt(0).startContainer;
24811
+ if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
24812
+ while (node && node !== this.editableDiv) {
24813
+ if (node.nodeType === Node.ELEMENT_NODE && tags.includes(node.nodeName.toLowerCase())) {
24814
+ return node;
24815
+ }
24816
+ node = node.parentNode;
24817
+ }
24818
+ return null;
24819
+ }
24820
+ }, {
24821
+ key: "isRangeAtEndOfNode",
24822
+ value: function isRangeAtEndOfNode(range, node) {
24823
+ try {
24824
+ var afterRange = document.createRange();
24825
+ afterRange.setStart(range.startContainer, range.startOffset);
24826
+ afterRange.setEnd(node, node.childNodes.length);
24827
+ var textAfterCaret = afterRange.toString().replace(/[\s\u00a0\u200b\ufeff]/g, "");
24828
+ if (textAfterCaret === "") return true;
24829
+ } catch (error) {
24830
+ // Fall back to boundary comparison below if the browser rejects the range.
24831
+ }
24832
+ var nodeEndRange = document.createRange();
24833
+ nodeEndRange.selectNodeContents(node);
24834
+ nodeEndRange.collapse(false);
24835
+ return range.compareBoundaryPoints(Range.START_TO_START, nodeEndRange) === 0;
24836
+ }
24837
+ }, {
24838
+ key: "moveTrailingWhitespaceOutsideInlineStyle",
24839
+ value: function moveTrailingWhitespaceOutsideInlineStyle(style, command) {
24840
+ var selection = document.getSelection();
24841
+ if (!selection || selection.rangeCount === 0) return false;
24842
+ var range = selection.getRangeAt(0);
24843
+ if (!range.collapsed) return false;
24844
+ var ancestor = this.getInlineStyleAncestor(style);
24845
+ if (!ancestor || !this.isRangeAtEndOfNode(range, ancestor)) return false;
24846
+ if (range.startContainer.nodeType !== Node.TEXT_NODE) return false;
24847
+ var textNode = range.startContainer;
24848
+ var text = textNode.nodeValue || "";
24849
+ var moveStart = range.startOffset;
24850
+ while (moveStart > 0) {
24851
+ var _char = text.charAt(moveStart - 1);
24852
+ if (_char !== " " && _char !== "\xA0") break;
24853
+ moveStart -= 1;
24854
+ }
24855
+ if (moveStart === range.startOffset) return false;
24856
+ var movedText = text.substring(moveStart, range.startOffset);
24857
+ textNode.nodeValue = text.substring(0, moveStart) + text.substring(range.startOffset);
24858
+ var outsideNode = ancestor.nextSibling;
24859
+ if (!outsideNode || outsideNode.nodeType !== Node.TEXT_NODE) {
24860
+ outsideNode = document.createTextNode(movedText);
24861
+ ancestor.parentNode.insertBefore(outsideNode, ancestor.nextSibling);
24862
+ } else {
24863
+ outsideNode.nodeValue = movedText + outsideNode.nodeValue;
24864
+ }
24865
+ var nextRange = document.createRange();
24866
+ nextRange.setStart(outsideNode, movedText.length);
24867
+ nextRange.collapse(true);
24868
+ selection.removeAllRanges();
24869
+ selection.addRange(nextRange);
24870
+ try {
24871
+ if (document.queryCommandState(command)) {
24872
+ document.execCommand(command, false, null);
24873
+ }
24874
+ } catch (error) {
24875
+ // DOM position is the source of truth here; command state is best effort.
24876
+ }
24877
+ var rangeParams = this._getRangeParams();
24878
+ if (rangeParams) {
24879
+ this.setOldRangeParams(rangeParams);
24880
+ this.applyCurrentEditableDomToComponent(rangeParams);
24881
+ }
24882
+ debugMathRichInput("moveTrailingWhitespaceOutsideInlineStyle:after", {
24883
+ style: style,
24884
+ command: command,
24885
+ movedText: movedText,
24886
+ innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
24887
+ rangeParams: rangeParams,
24888
+ selection: getSelectionSnapshot(this.editableDiv)
24889
+ });
24890
+ return true;
24891
+ }
24892
+ }, {
24893
+ key: "applyCurrentEditableDomToComponent",
24894
+ value: function applyCurrentEditableDomToComponent() {
24895
+ var rangeParams = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
24896
+ if (!this.editableDiv) return;
24897
+ if (rangeParams === null || rangeParams === undefined) {
24898
+ rangeParams = this._getRangeParams();
24899
+ }
24900
+ if (rangeParams === null || rangeParams === undefined) return;
24901
+ var rawText = elementToMarkedRawText(this.editableDiv, null, 0, this.enableHtml());
24902
+ rawText = this.stripEmptyInlineFormattingTags(rawText);
24903
+ var nodeCounts = this._countNodeTypes();
24904
+ var hasMath = /<math>/i.test(rawText) || nodeCounts.math > 0;
24905
+ var hasHtml = nodeCounts.html > 0 || /<[^>]+>/i.test(rawText);
24906
+ var mimeType = this.getMimeType(hasHtml, hasMath);
24907
+ rawText = removeEncodingIfPlainText(rawText, mimeType, this.enableHtml());
24908
+ rawText = this.stripEmptyInlineFormattingTags(rawText);
24909
+ this.applyChangesToComponent(rawText, mimeType, rangeParams, this.props.useExpertMode, this.props.selectedTab, {
24910
+ skipControlledRender: true
24911
+ });
24912
+ }
24144
24913
  }, {
24145
24914
  key: "_getRangeParams",
24146
24915
  value: function _getRangeParams() {
24147
24916
  var editableDiv = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
24148
24917
  if (editableDiv === null) editableDiv = this.editableDiv;
24149
24918
  var rangeParams = getRangeParams(editableDiv);
24919
+ debugMathRichInput("MathRichInput:_getRangeParams", {
24920
+ rangeParams: rangeParams,
24921
+ selection: getSelectionSnapshot(editableDiv)
24922
+ });
24150
24923
  return rangeParams;
24151
24924
  }
24152
24925
  }, {
@@ -24175,6 +24948,14 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24175
24948
  value: function componentDidMount() {
24176
24949
  try {
24177
24950
  setupKatex();
24951
+ debugMathRichInput("componentDidMount", {
24952
+ propsValue: this.props.value,
24953
+ propsMimeType: this.props.mimeType,
24954
+ autofocus: this.props.autofocus,
24955
+ autoFocus: this.props.autoFocus,
24956
+ innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
24957
+ selection: getSelectionSnapshot(this.editableDiv)
24958
+ });
24178
24959
  this.editableDiv.addEventListener("keydown", this.handleKeyDown);
24179
24960
  this.editableDiv.addEventListener("keyup", this.handleKeyUp);
24180
24961
  this.editableDiv.addEventListener("paste", this.handlePaste);
@@ -24196,14 +24977,137 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24196
24977
  console.error(error);
24197
24978
  }
24198
24979
  }
24980
+ }, {
24981
+ key: "shouldComponentUpdate",
24982
+ value: function shouldComponentUpdate(nextProps, nextState) {
24983
+ if (this.skipNextControlledValueRender === true) {
24984
+ var canSkip = nextProps.value === this.skipNextControlledValue && nextState === this.state;
24985
+ debugMathRichInput("shouldComponentUpdate:native-input-gate", {
24986
+ canSkip: canSkip,
24987
+ nextValue: nextProps.value,
24988
+ skipNextControlledValue: this.skipNextControlledValue,
24989
+ currentPropsValue: this.props.value,
24990
+ stateChanged: nextState !== this.state,
24991
+ innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
24992
+ selection: getSelectionSnapshot(this.editableDiv)
24993
+ });
24994
+ this.skipNextControlledValueRender = false;
24995
+ this.skipNextControlledValue = null;
24996
+ if (canSkip) return false;
24997
+ }
24998
+ if (this.shouldSkipRedundantFocusedRender(nextProps, nextState)) {
24999
+ debugMathRichInput("shouldComponentUpdate:skip-redundant-focused-render", {
25000
+ nextValue: nextProps.value,
25001
+ currentPropsValue: this.props.value,
25002
+ selection: getSelectionSnapshot(this.editableDiv)
25003
+ });
25004
+ return false;
25005
+ }
25006
+ return true;
25007
+ }
25008
+
25009
+ /**
25010
+ * The browser's own reading of an HTML string.
25011
+ *
25012
+ * React renders `<p>&#8203;…</P>`; the DOM reports `<p>​…</p>` — same content, different bytes. So a
25013
+ * comparison against `innerHTML` has to be made in the DOM's spelling, or it never matches and the
25014
+ * reconciliation below would rewrite the field on every single update.
25015
+ */
25016
+ }, {
25017
+ key: "normaliseHtml",
25018
+ value: function normaliseHtml(html) {
25019
+ var probe = document.createElement("span");
25020
+ probe.innerHTML = html;
25021
+ return probe.innerHTML;
25022
+ }
25023
+
25024
+ /**
25025
+ * Put back what React believes it rendered, when the live DOM has drifted away from it.
25026
+ *
25027
+ * WHY THIS IS NEEDED AT ALL. `dangerouslySetInnerHTML` writes only when the html STRING changes: React
25028
+ * compares the new `__html` with the previous one and, finding them equal, leaves the DOM alone. That is
25029
+ * sound as long as React is the only thing that touches the DOM — and here it is not. `handlePaste`
25030
+ * removes the selected content itself, through `selection.deleteFromDocument()`, so the field empties
25031
+ * behind React's back while React's record of it still says "full".
25032
+ *
25033
+ * Paste the same words back into the field they came from and the two mistakes meet: the DOM is empty,
25034
+ * the html React computes is identical to the html it rendered last time, so it writes nothing — and
25035
+ * the field stays blank while `value`, the command, and the row in the database are all correct. The
25036
+ * teacher sees their text vanish on paste; nothing anywhere reports an error.
25037
+ *
25038
+ * It writes back the SAME string React itself passed to `dangerouslySetInnerHTML` on this render, so
25039
+ * nothing reaches the DOM here that React was not already putting there — the trust boundary is the
25040
+ * one that existed before, not a new one.
25041
+ *
25042
+ * Only reconciled after a PROGRAMMATIC edit (`afterComponentUpdateData` is set), or when the paste
25043
+ * asks directly (`force`). Ordinary typing goes down the native-input path, where the browser has
25044
+ * already put the character in the right place and the caret with it, and rewriting the DOM there
25045
+ * would move the caret to the front on every keystroke.
25046
+ *
25047
+ * `force` exists because a re-render is not guaranteed to happen AT ALL. When the host already holds
25048
+ * the value being applied — paste the same words back into the field they came from — the prop never
25049
+ * changes, so nothing re-renders and `componentDidUpdate` never runs. `lastRenderedEditableHtml` is
25050
+ * then exactly right: it is the html for that unchanged value, which is what the DOM should hold.
25051
+ *
25052
+ * Returns whether it wrote, so the caller knows whether the caret needs putting back.
25053
+ */
25054
+ }, {
25055
+ key: "reconcileEditableDomWithRender",
25056
+ value: function reconcileEditableDomWithRender() {
25057
+ var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
25058
+ if (!this.editableDiv) return false;
25059
+ if (!force && (this.afterComponentUpdateData === null || this.afterComponentUpdateData === undefined)) return false;
25060
+ var html = force ? rawTextToHtml(katex$1, this.props.value || "", this.props.mimeType) : this.lastRenderedEditableHtml;
25061
+ if (html === null || html === undefined) return false;
25062
+ if (this.editableDiv.innerHTML === this.normaliseHtml(html)) return false;
25063
+ debugMathRichInput("reconcile", {
25064
+ force: force,
25065
+ liveInnerHTML: this.editableDiv.innerHTML,
25066
+ html: html
25067
+ });
25068
+ this.editableDiv.innerHTML = html;
25069
+ this.lastRenderedEditableHtml = html;
25070
+ return true;
25071
+ }
24199
25072
  }, {
24200
25073
  key: "componentDidUpdate",
24201
25074
  value: function componentDidUpdate() {
24202
25075
  try {
25076
+ debugMathRichInput("componentDidUpdate:start", {
25077
+ propsValue: this.props.value,
25078
+ propsMimeType: this.props.mimeType,
25079
+ afterComponentUpdateData: this.afterComponentUpdateData,
25080
+ hasFocus: this.state.hasFocus,
25081
+ showEquationEditor: this.state.showEquationEditor,
25082
+ innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
25083
+ selection: getSelectionSnapshot(this.editableDiv)
25084
+ });
25085
+ // Before the caret is restored, not after: the offsets below are counted through the text, so
25086
+ // restoring them against an empty field puts the caret at 0 and loses the position as well.
25087
+ this.reconcileEditableDomWithRender();
24203
25088
  if (this.afterComponentUpdateData !== null && this.afterComponentUpdateData !== undefined && this.afterComponentUpdateData.value === this.props.value) {
25089
+ debugMathRichInput("componentDidUpdate:restore-before", {
25090
+ rangeParams: this.afterComponentUpdateData.rangeParams,
25091
+ selection: getSelectionSnapshot(this.editableDiv)
25092
+ });
24204
25093
  this._setRangeParams(this.afterComponentUpdateData.rangeParams);
24205
25094
  this.afterComponentUpdateData = null;
24206
25095
  this.updateActiveButtons();
25096
+ debugMathRichInput("componentDidUpdate:restore-after", {
25097
+ selection: getSelectionSnapshot(this.editableDiv)
25098
+ });
25099
+ }
25100
+ if (this.preserveNativeDomRangeParams !== null && this.preserveNativeDomRangeParams !== undefined && this.state.hasFocus === true) {
25101
+ var rangeParams = this.preserveNativeDomRangeParams;
25102
+ this.preserveNativeDomRangeParams = null;
25103
+ debugMathRichInput("componentDidUpdate:preserve-restore-before", {
25104
+ rangeParams: rangeParams,
25105
+ selection: getSelectionSnapshot(this.editableDiv)
25106
+ });
25107
+ this._setRangeParams(rangeParams);
25108
+ debugMathRichInput("componentDidUpdate:preserve-restore-after", {
25109
+ selection: getSelectionSnapshot(this.editableDiv)
25110
+ });
24207
25111
  }
24208
25112
  } catch (error) {
24209
25113
  console.error(error);
@@ -24216,20 +25120,31 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24216
25120
  if (rangeParams === null || rangeParams === undefined) rangeParams = this._getRangeParams();
24217
25121
  if (rangeParams.startOffset !== 0) console.warn("Unexpected startOffset " + rangeParams.startOffset + " in insertAfterSmallSpace");
24218
25122
  var node = this._findNodeWithIndex(rangeParams.startNodeIndex);
24219
- var raw_text = elementToMarkedRawText(this.editableDiv, node, 1,
24220
- // after small space
24221
- this.enableHtml());
24222
- var new_raw_text = removeMarks(insertCharacterBeforeMarks(raw_text, new_char));
25123
+ if (node === null || node === undefined || node.nodeValue === null) {
25124
+ console.warn("Could not find small-space text node");
25125
+ return;
25126
+ }
25127
+
25128
+ // The initial empty editor contains a hidden zero-width space. Handling
25129
+ // the first printable key by re-rendering through React replaces the
25130
+ // text node and drops Chrome's selection to the start under React 19.
25131
+ // Mutate the already-focused DOM node, then skip the matching controlled
25132
+ // render exactly like the normal contenteditable input path.
25133
+ node.nodeValue = node.nodeValue.substring(0, 1) + new_char + node.nodeValue.substring(1);
24223
25134
  var new_rangeParams = {
24224
25135
  startNodeIndex: rangeParams.startNodeIndex,
24225
- startNodeOffset: 1 + new_char.length,
25136
+ startOffset: 1 + new_char.length,
24226
25137
  endNodeIndex: rangeParams.startNodeIndex,
24227
- endNodeOffset: 1 + new_char.length,
25138
+ endOffset: 1 + new_char.length,
24228
25139
  startGlobalOffset: rangeParams.startGlobalOffset + new_char.length,
24229
25140
  endGlobalOffset: rangeParams.endGlobalOffset + new_char.length
24230
25141
  };
25142
+ this._setRangeParams(new_rangeParams);
25143
+ var new_raw_text = elementToMarkedRawText(this.editableDiv, null, 0, this.enableHtml());
24231
25144
  new_raw_text = removeEncodingIfPlainText(new_raw_text, this.props.mimeType, this.enableHtml());
24232
- this.applyChangesToComponent(new_raw_text, this.props.mimeType, new_rangeParams, this.props.useExpertMode, this.props.selectedTab);
25145
+ this.applyChangesToComponent(new_raw_text, this.props.mimeType, new_rangeParams, this.props.useExpertMode, this.props.selectedTab, {
25146
+ skipControlledRender: true
25147
+ });
24233
25148
  } catch (error) {
24234
25149
  console.error(error);
24235
25150
  }
@@ -24243,6 +25158,23 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24243
25158
  console.log(this.props);
24244
25159
  }
24245
25160
  var html = rawTextToHtml(katex$1, this.props.value || "", this.props.mimeType);
25161
+ var editableHtml = this.getEditableHtmlForRender(html);
25162
+ debugMathRichInput("render", {
25163
+ propsValue: this.props.value,
25164
+ propsMimeType: this.props.mimeType,
25165
+ html: html,
25166
+ editableHtml: editableHtml,
25167
+ state: {
25168
+ hasFocus: this.state.hasFocus,
25169
+ showEquationEditor: this.state.showEquationEditor,
25170
+ showAccentBar: this.state.showAccentBar,
25171
+ keyPressed: this.state.keyPressed
25172
+ },
25173
+ afterComponentUpdateData: this.afterComponentUpdateData,
25174
+ oldRangeParams: this.oldRangeParams,
25175
+ editableInnerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
25176
+ selection: getSelectionSnapshot(this.editableDiv)
25177
+ });
24246
25178
 
24247
25179
  // Debug render math
24248
25180
  // if (this.props.value && this.props.value.includes("<math>")) {
@@ -24301,7 +25233,7 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24301
25233
  onCompositionStart: this.handleCompositionStart,
24302
25234
  onCompositionEnd: this.handleCompositionEnd,
24303
25235
  dangerouslySetInnerHTML: {
24304
- __html: html
25236
+ __html: editableHtml
24305
25237
  },
24306
25238
  "data-placeholder": this.props.placeholder || ""
24307
25239
  }))), this.state.showEquationEditor && /*#__PURE__*/React__default["default"].createElement(EquationEditorModal, {
@@ -24320,7 +25252,7 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
24320
25252
  function getAllTextNodes(node) {
24321
25253
  var nodes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
24322
25254
  if (node === null) {
24323
- console.warn("Deverloper supplied a null node. Ignoring");
25255
+ console.warn("Developer supplied a null node. Ignoring");
24324
25256
  return nodes;
24325
25257
  }
24326
25258
  if (nodes === null) {
@@ -24345,8 +25277,9 @@ function getAllTextNodes(node) {
24345
25277
  return nodes;
24346
25278
  }
24347
25279
  function renderMathInNode(node) {
25280
+ if (node === null || node === undefined) return;
24348
25281
  try {
24349
- var textNodes = getAllTextNodes(node);
25282
+ var textNodes = getAllTextNodes(node, []);
24350
25283
  for (var i = 0; i < textNodes.length; i++) {
24351
25284
  try {
24352
25285
  var text = textNodes[i].nodeValue;