@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/dist/index.js +979 -163
- package/dist/standalone.js +1 -1
- package/package.json +3 -3
- package/src/MathRichArea.jsx +5 -2
- package/src/MathRichInput.jsx +991 -207
- package/src/Toolbar.jsx +3 -4
- package/src/mathRichInputDebug.js +98 -0
- package/src/mathRichInputHelper.js +128 -17
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
|
|
925
|
-
//
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
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:");
|