@zzish/math-rich-input 0.1.51 → 0.1.52

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/src/Toolbar.jsx CHANGED
@@ -33,10 +33,6 @@ export default class Toolbar extends React.Component {
33
33
  this.shouldUpdatePosition = true;
34
34
  }
35
35
 
36
- handleButtonClick = (event, buttonName) => {
37
- this.suppliedClickHandler(event, buttonName);
38
- };
39
-
40
36
  showAccentBar = (event) => {
41
37
  if (!this.state.showAccents)
42
38
  this.accentLetter = this.suppliedAccentLetterFetcher();
@@ -55,6 +51,8 @@ export default class Toolbar extends React.Component {
55
51
  };
56
52
 
57
53
  handleButtonClick = (event, buttonName) => {
54
+ event.preventDefault();
55
+ event.stopPropagation();
58
56
  this.suppliedClickHandler(event, buttonName);
59
57
  };
60
58
 
@@ -236,6 +234,7 @@ export default class Toolbar extends React.Component {
236
234
  <button
237
235
  name="Equation"
238
236
  className="Toolbar-button TB-wide"
237
+ onClick={(e) => e.preventDefault()}
239
238
  onMouseDown={(e) => this.handleButtonClick(e, "Equation")}
240
239
  >
241
240
  <div dangerouslySetInnerHTML={{ __html: katexHTML }} />
@@ -0,0 +1,98 @@
1
+ export const DEBUG_MATH_RICH_INPUT = false;
2
+
3
+ const DEBUG_PREFIX = "[MRI]";
4
+
5
+ function getNodePath(node, root = null) {
6
+ if (!node) return null;
7
+
8
+ const parts = [];
9
+ let current = node;
10
+ while (current && current !== root && parts.length < 8) {
11
+ if (current.nodeType === 3) {
12
+ parts.unshift(`#text("${current.nodeValue}")`);
13
+ } else {
14
+ const name = current.nodeName ? current.nodeName.toLowerCase() : "unknown";
15
+ const className =
16
+ current.className && typeof current.className === "string"
17
+ ? `.${current.className.split(" ").filter(Boolean).join(".")}`
18
+ : "";
19
+ parts.unshift(`${name}${className}`);
20
+ }
21
+ current = current.parentNode;
22
+ }
23
+
24
+ if (root) parts.unshift("root");
25
+ return parts.join(" > ");
26
+ }
27
+
28
+ export function describeNode(node, root = null) {
29
+ if (!node) return null;
30
+
31
+ return {
32
+ nodeName: node.nodeName,
33
+ nodeType: node.nodeType,
34
+ text:
35
+ node.nodeType === 3 && typeof node.nodeValue === "string"
36
+ ? node.nodeValue
37
+ : undefined,
38
+ textLength:
39
+ node.nodeType === 3 && typeof node.nodeValue === "string"
40
+ ? node.nodeValue.length
41
+ : undefined,
42
+ childCount: node.childNodes ? node.childNodes.length : undefined,
43
+ path: getNodePath(node, root),
44
+ };
45
+ }
46
+
47
+ export function getSelectionSnapshot(editableDiv = null) {
48
+ if (typeof window === "undefined" || !window.getSelection) return null;
49
+
50
+ const selection = window.getSelection();
51
+ if (!selection) return null;
52
+
53
+ const snapshot = {
54
+ activeElement:
55
+ typeof document !== "undefined" && document.activeElement
56
+ ? {
57
+ nodeName: document.activeElement.nodeName,
58
+ className: document.activeElement.className,
59
+ }
60
+ : null,
61
+ rangeCount: selection.rangeCount,
62
+ isCollapsed: selection.isCollapsed,
63
+ anchorOffset: selection.anchorOffset,
64
+ focusOffset: selection.focusOffset,
65
+ anchorNode: describeNode(selection.anchorNode, editableDiv),
66
+ focusNode: describeNode(selection.focusNode, editableDiv),
67
+ };
68
+
69
+ if (selection.rangeCount > 0) {
70
+ const range = selection.getRangeAt(0);
71
+ snapshot.range = {
72
+ collapsed: range.collapsed,
73
+ startOffset: range.startOffset,
74
+ endOffset: range.endOffset,
75
+ startContainer: describeNode(range.startContainer, editableDiv),
76
+ endContainer: describeNode(range.endContainer, editableDiv),
77
+ };
78
+ }
79
+
80
+ return snapshot;
81
+ }
82
+
83
+ export function debugMathRichInput(label, payload = {}) {
84
+ if (!DEBUG_MATH_RICH_INPUT) return;
85
+
86
+ try {
87
+ if (typeof window !== "undefined") {
88
+ window.__MRI_DEBUG__ = true;
89
+ }
90
+ console.log(`${DEBUG_PREFIX} ${label}`, payload);
91
+ } catch (error) {
92
+ // Debug logging must never affect editor behavior.
93
+ }
94
+ }
95
+
96
+ if (typeof window !== "undefined") {
97
+ window.__MRI_DEBUG__ = DEBUG_MATH_RICH_INPUT;
98
+ }
@@ -1,4 +1,9 @@
1
1
  import { determineMimeType } from "./mimeTypeHelper";
2
+ import {
3
+ debugMathRichInput,
4
+ describeNode,
5
+ getSelectionSnapshot,
6
+ } from "./mathRichInputDebug";
2
7
 
3
8
  export const RAW_TEXT_MATH_START_TAG_TEX = "\\[";
4
9
  export const RAW_TEXT_MATH_END_TAG_TEX = "\\]";
@@ -849,6 +854,54 @@ export function findFirstTextNode(node) {
849
854
  return null;
850
855
  }
851
856
 
857
+ function findLastTextNode(node) {
858
+ if (isTextNode(node)) return node;
859
+
860
+ const nodes = node.childNodes;
861
+ for (let i = nodes.length - 1; i >= 0; i--) {
862
+ let found_node = findLastTextNode(nodes[i]);
863
+ if (found_node !== null) return found_node;
864
+ }
865
+
866
+ return null;
867
+ }
868
+
869
+ function getTextPositionFromElementRangeEndpoint(container, offset) {
870
+ if (isTextNode(container)) return { node: container, offset };
871
+
872
+ const nodes = container.childNodes;
873
+ if (nodes === null || nodes === undefined || nodes.length === 0) return null;
874
+
875
+ // A collapsed selection inside contentEditable may report the editable SPAN
876
+ // itself as the container and the child index as the offset. The previous
877
+ // implementation always picked the first descendant text node, which moved
878
+ // the caret to the beginning after React re-rendered the controlled input.
879
+ if (offset > 0) {
880
+ const previousChild = nodes[Math.min(offset - 1, nodes.length - 1)];
881
+ const previousTextNode = findLastTextNode(previousChild);
882
+ if (previousTextNode !== null) {
883
+ return {
884
+ node: previousTextNode,
885
+ offset: previousTextNode.nodeValue.length,
886
+ };
887
+ }
888
+ }
889
+
890
+ if (offset < nodes.length) {
891
+ const nextTextNode = findFirstTextNode(nodes[offset]);
892
+ if (nextTextNode !== null) {
893
+ return { node: nextTextNode, offset: 0 };
894
+ }
895
+ }
896
+
897
+ const fallbackTextNode = findFirstTextNode(container);
898
+ if (fallbackTextNode !== null) {
899
+ return { node: fallbackTextNode, offset: 0 };
900
+ }
901
+
902
+ return null;
903
+ }
904
+
852
905
  function findNodeAndOffsetForGlobalOffsetInEditableDiv(
853
906
  editableDiv,
854
907
  globalOffset
@@ -902,6 +955,10 @@ function findNodeAndOffsetForGlobalOffsetInEditableDiv(
902
955
  export function getRangeParams(editableDiv) {
903
956
  // console.log ("getRangeParams")
904
957
  // console.log("Inner html: "+editableDiv.innerHTML)
958
+ debugMathRichInput("getRangeParams:start", {
959
+ innerHTML: editableDiv ? editableDiv.innerHTML : null,
960
+ selection: getSelectionSnapshot(editableDiv),
961
+ });
905
962
  const isSupported = typeof window.getSelection !== "undefined";
906
963
  if (!isSupported) {
907
964
  console.error(
@@ -919,18 +976,30 @@ export function getRangeParams(editableDiv) {
919
976
  }
920
977
 
921
978
  const range = selection.getRangeAt(0);
979
+ debugMathRichInput("getRangeParams:range-before-normalize", {
980
+ startContainer: describeNode(range.startContainer, editableDiv),
981
+ startOffset: range.startOffset,
982
+ endContainer: describeNode(range.endContainer, editableDiv),
983
+ endOffset: range.endOffset,
984
+ });
922
985
 
923
986
  // logAllDescendants(editableDiv)
924
- // If the range is not within a text node, move it to a text node
925
- // This typically happens when the end of a range is at the start of a new line
987
+ // If the range is not within a text node, move it to the text node implied
988
+ // by the element-container offset. This typically happens when the browser
989
+ // reports a collapsed contentEditable selection as (SPAN, childIndex).
926
990
  if (range.startContainer.nodeType !== 3) {
927
- let textNode = findFirstTextNode(range.startContainer);
928
- if (textNode !== null) {
929
- console.warn(
930
- "Adjusting start pos in getRangeParams as container was of type: " +
931
- range.startContainer.nodeName
932
- );
933
- range.setStart(textNode, 0);
991
+ const textPosition = getTextPositionFromElementRangeEndpoint(
992
+ range.startContainer,
993
+ range.startOffset
994
+ );
995
+ if (textPosition !== null) {
996
+ debugMathRichInput("getRangeParams:normalize-start-element", {
997
+ from: describeNode(range.startContainer, editableDiv),
998
+ fromOffset: range.startOffset,
999
+ to: describeNode(textPosition.node, editableDiv),
1000
+ toOffset: textPosition.offset,
1001
+ });
1002
+ range.setStart(textPosition.node, textPosition.offset);
934
1003
  } else {
935
1004
  // Handle empty content case - return default range params
936
1005
  // This is normal behavior after selecting all and deleting content
@@ -950,13 +1019,18 @@ export function getRangeParams(editableDiv) {
950
1019
  }
951
1020
  }
952
1021
  if (range.endContainer.nodeType !== 3) {
953
- let textNode = findFirstTextNode(range.endContainer);
954
- if (textNode !== null) {
955
- console.warn(
956
- "Adjusting end pos in getRangeParams as container was of type: " +
957
- range.endContainer.nodeName
958
- );
959
- range.setEnd(textNode, 0);
1022
+ const textPosition = getTextPositionFromElementRangeEndpoint(
1023
+ range.endContainer,
1024
+ range.endOffset
1025
+ );
1026
+ if (textPosition !== null) {
1027
+ debugMathRichInput("getRangeParams:normalize-end-element", {
1028
+ from: describeNode(range.endContainer, editableDiv),
1029
+ fromOffset: range.endOffset,
1030
+ to: describeNode(textPosition.node, editableDiv),
1031
+ toOffset: textPosition.offset,
1032
+ });
1033
+ range.setEnd(textPosition.node, textPosition.offset);
960
1034
  } else {
961
1035
  // Handle empty content case - return default range params
962
1036
  // This is normal behavior after selecting all and deleting content
@@ -1008,12 +1082,22 @@ export function getRangeParams(editableDiv) {
1008
1082
  endGlobalOffset: endResult.offset,
1009
1083
  };
1010
1084
 
1085
+ debugMathRichInput("getRangeParams:result", {
1086
+ rangeParams,
1087
+ selection: getSelectionSnapshot(editableDiv),
1088
+ });
1089
+
1011
1090
  return rangeParams;
1012
1091
  }
1013
1092
 
1014
1093
  function setRangeParamsFromGlobalOffsets(editableDiv, params) {
1015
1094
  let new_range = null;
1016
1095
  try {
1096
+ debugMathRichInput("setRangeParamsFromGlobalOffsets:start", {
1097
+ params,
1098
+ innerHTML: editableDiv ? editableDiv.innerHTML : null,
1099
+ selectionBefore: getSelectionSnapshot(editableDiv),
1100
+ });
1017
1101
  if (params === null || params === undefined)
1018
1102
  throw new Error("Can't set range params: params === " + params);
1019
1103
  let startResult = findNodeAndOffsetForGlobalOffsetInEditableDiv(
@@ -1043,6 +1127,18 @@ function setRangeParamsFromGlobalOffsets(editableDiv, params) {
1043
1127
  const selection = window.getSelection();
1044
1128
  selection.removeAllRanges();
1045
1129
  selection.addRange(new_range);
1130
+ debugMathRichInput("setRangeParamsFromGlobalOffsets:after", {
1131
+ params,
1132
+ startResult: {
1133
+ node: describeNode(startResult.node, editableDiv),
1134
+ offset: startResult.offset,
1135
+ },
1136
+ endResult: {
1137
+ node: describeNode(endResult.node, editableDiv),
1138
+ offset: endResult.offset,
1139
+ },
1140
+ selectionAfter: getSelectionSnapshot(editableDiv),
1141
+ });
1046
1142
  } catch (error) {
1047
1143
  console.error(error);
1048
1144
  console.log("Trying to set range:");
@@ -1057,6 +1153,11 @@ export function setRangeParams(editableDiv, params) {
1057
1153
 
1058
1154
  let new_range = null;
1059
1155
  try {
1156
+ debugMathRichInput("setRangeParams:start", {
1157
+ params,
1158
+ innerHTML: editableDiv ? editableDiv.innerHTML : null,
1159
+ selectionBefore: getSelectionSnapshot(editableDiv),
1160
+ });
1060
1161
  if (params === null || params === undefined) {
1061
1162
  console.error("Can't set range params: params === " + params);
1062
1163
  return;
@@ -1140,6 +1241,7 @@ export function setRangeParams(editableDiv, params) {
1140
1241
  startNode = findFirstTextNode(editableDiv);
1141
1242
  startOffset = 0;
1142
1243
  }
1244
+ if (startNode === null) return;
1143
1245
 
1144
1246
  if (!isTextNode(startNode)) {
1145
1247
  console.warn("Start node found for range start is not a text a node");
@@ -1159,6 +1261,7 @@ export function setRangeParams(editableDiv, params) {
1159
1261
  endNode = findFirstTextNode(editableDiv);
1160
1262
  endOffset = 0;
1161
1263
  }
1264
+ if (endNode === null) return;
1162
1265
 
1163
1266
  if (!isTextNode(endNode)) {
1164
1267
  console.warn("End node found for range start is not a text a node");
@@ -1179,7 +1282,7 @@ export function setRangeParams(editableDiv, params) {
1179
1282
  " > " +
1180
1283
  startNode.nodeValue.length +
1181
1284
  " - " +
1182
- stateNode.nodeType +
1285
+ startNode.nodeType +
1183
1286
  " - " +
1184
1287
  startNode.nodeValue
1185
1288
  );
@@ -1207,6 +1310,14 @@ export function setRangeParams(editableDiv, params) {
1207
1310
  const selection = window.getSelection();
1208
1311
  selection.removeAllRanges();
1209
1312
  selection.addRange(new_range);
1313
+ debugMathRichInput("setRangeParams:after", {
1314
+ params,
1315
+ startNode: describeNode(startNode, editableDiv),
1316
+ startOffset,
1317
+ endNode: describeNode(endNode, editableDiv),
1318
+ endOffset,
1319
+ selectionAfter: getSelectionSnapshot(editableDiv),
1320
+ });
1210
1321
  } catch (error) {
1211
1322
  console.error(error);
1212
1323
  console.log("Trying to set range:");