@zzish/math-rich-input 0.1.52 → 0.1.54
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 +206 -27
- package/dist/standalone.js +1 -1
- package/package.json +1 -1
- package/src/MathRichInput.jsx +213 -35
- package/src/mathRichInputHelper.js +9 -0
package/dist/index.js
CHANGED
|
@@ -21963,6 +21963,15 @@ function plainTextOffsetToGlobalOffset(editableDiv, plainTextOffset) {
|
|
|
21963
21963
|
return Math.min(plainTextOffset, plainText.length);
|
|
21964
21964
|
}
|
|
21965
21965
|
|
|
21966
|
+
/**
|
|
21967
|
+
* The character used to mark a position in text while content is being rewritten.
|
|
21968
|
+
*
|
|
21969
|
+
* Exported so a caller can tell whether a mark was actually placed. `insertCharacterBeforeMarks` warns
|
|
21970
|
+
* and returns its input UNCHANGED when there is no mark, which reads as success at the call site — the
|
|
21971
|
+
* value comes back looking valid with the inserted text silently missing.
|
|
21972
|
+
*/
|
|
21973
|
+
var SELECTION_MARK = MARK$1;
|
|
21974
|
+
|
|
21966
21975
|
var MIME_TYPE_PLAIN_TEXT = "text/plain";
|
|
21967
21976
|
var MIME_TYPE_HTML = "text/html";
|
|
21968
21977
|
var MIME_TYPE_TEX = "application/x-tex";
|
|
@@ -23034,6 +23043,10 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23034
23043
|
|
|
23035
23044
|
// Stop editing if within rendered katex node, if somehow the cursor ends up there (it shouldn't)
|
|
23036
23045
|
var params = _this2._getRangeParams();
|
|
23046
|
+
// No range means no caret to protect — a field the browser has not placed a selection in yet. It
|
|
23047
|
+
// reads as null on the first keystroke after a click on a toolbar button, and dereferencing it threw
|
|
23048
|
+
// on every Cmd, every Cmd+A and every Cmd+V the teacher pressed.
|
|
23049
|
+
if (params === null || params === undefined) return;
|
|
23037
23050
|
var startNode = _this2._findNodeWithIndex(params.startNodeIndex);
|
|
23038
23051
|
var endNode = _this2._findNodeWithIndex(params.endNodeIndex);
|
|
23039
23052
|
if (!_this2.editableDivIsPlainText()) {
|
|
@@ -23086,7 +23099,12 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23086
23099
|
// If the cursor is in a katex node, then move it to the next node (unless there is no
|
|
23087
23100
|
// net node in which case move it to the previous node)
|
|
23088
23101
|
if (e.key === "ArrowRight") {
|
|
23102
|
+
// `_getRangeParams` returns null whenever the document's selection is not inside this field —
|
|
23103
|
+
// which happens in ordinary use, not only in error: a toolbar button taking focus is enough. It
|
|
23104
|
+
// was dereferenced straight away, so the key handler THREW and everything after it in the same
|
|
23105
|
+
// press was skipped.
|
|
23089
23106
|
var rangeParams = _this2._getRangeParams();
|
|
23107
|
+
if (!rangeParams) return;
|
|
23090
23108
|
var index = rangeParams.startNodeIndex;
|
|
23091
23109
|
if (index >= 0) {
|
|
23092
23110
|
if (isKatexNode(_this2._findNodeWithIndex(index))) {
|
|
@@ -23115,7 +23133,9 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23115
23133
|
}
|
|
23116
23134
|
}
|
|
23117
23135
|
if (e.key === "ArrowLeft") {
|
|
23136
|
+
// Same guard, same reason — see ArrowRight above.
|
|
23118
23137
|
var _rangeParams = _this2._getRangeParams();
|
|
23138
|
+
if (!_rangeParams) return;
|
|
23119
23139
|
var _index = _rangeParams.startNodeIndex;
|
|
23120
23140
|
|
|
23121
23141
|
// If the cursor is in a katex node, then move it to the previous node (unless there is no
|
|
@@ -23331,7 +23351,17 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23331
23351
|
var pasteHtml = clipboardData.getData("text/html");
|
|
23332
23352
|
var pasteText = clipboardData.getData("text/plain");
|
|
23333
23353
|
|
|
23334
|
-
|
|
23354
|
+
/*
|
|
23355
|
+
* The caret is read BEFORE the selection is removed, and that order is deliberate.
|
|
23356
|
+
*
|
|
23357
|
+
* Reading it afterwards looks more correct — `deleteFromDocument()` removes the very nodes the
|
|
23358
|
+
* index counts — and it was tried, on the theory that a stale index was why pasting over a
|
|
23359
|
+
* selection emptied the field. It was not: the field emptied because React never wrote the
|
|
23360
|
+
* content back (see `reconcileEditableDomWithRender`), and the reordering was left in on a theory
|
|
23361
|
+
* that had already been disproved. In a host whose field holds unwrapped text rather than a `<p>`,
|
|
23362
|
+
* the emptied div yields a caret that `elementToMarkedRawText` cannot mark at all — no mark, so the
|
|
23363
|
+
* pasted text is inserted nowhere and the paste becomes a deletion.
|
|
23364
|
+
*/
|
|
23335
23365
|
var rangeParams = _this2._getRangeParams();
|
|
23336
23366
|
if (!rangeParams) {
|
|
23337
23367
|
console.warn("Could not get range params for paste");
|
|
@@ -23343,7 +23373,7 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23343
23373
|
|
|
23344
23374
|
// Clear any existing selection
|
|
23345
23375
|
var selection = window.getSelection();
|
|
23346
|
-
if (selection.rangeCount > 0) {
|
|
23376
|
+
if (selection && selection.rangeCount > 0) {
|
|
23347
23377
|
selection.deleteFromDocument();
|
|
23348
23378
|
}
|
|
23349
23379
|
var finalText = "";
|
|
@@ -23442,44 +23472,105 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23442
23472
|
|
|
23443
23473
|
// Use the component's insert mechanism to handle paste properly
|
|
23444
23474
|
if (finalText) {
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23475
|
+
/*
|
|
23476
|
+
* WHERE TO PUT THE PASTED TEXT, found in the document rather than by index.
|
|
23477
|
+
*
|
|
23478
|
+
* `elementToMarkedRawText` marks ONE position — a node and an offset — and the selection has to
|
|
23479
|
+
* be gone from the document before that position means anything. It was, and then the position
|
|
23480
|
+
* was looked up by INDEX, which is where every version of this went wrong: an index read before
|
|
23481
|
+
* the delete counts nodes the delete then removes, and an index read after it does not resolve in
|
|
23482
|
+
* a field whose content is unwrapped text. Either way `_findNodeWithIndex` returns nothing, no
|
|
23483
|
+
* mark is placed, `insertCharacterBeforeMarks` warns "Start: -1" and returns the text UNCHANGED —
|
|
23484
|
+
* so the pasted words go nowhere, and what the teacher sees is their field emptied.
|
|
23485
|
+
*
|
|
23486
|
+
* The browser's own selection is the answer: after `deleteFromDocument()` it is left collapsed at
|
|
23487
|
+
* exactly the point where the pasted text belongs, in a node that exists by construction. The
|
|
23488
|
+
* index lookup stays as a fallback for the case where the selection has gone elsewhere.
|
|
23489
|
+
*/
|
|
23490
|
+
var live = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
23491
|
+
var liveNode = live && _this2.editableDiv.contains(live.startContainer) ? live.startContainer : null;
|
|
23492
|
+
var currentNode = liveNode !== null ? liveNode : _this2._findNodeWithIndex(rangeParams.startNodeIndex);
|
|
23493
|
+
var currentOffset = liveNode !== null ? live.startOffset : rangeParams.startOffset;
|
|
23494
|
+
var markedText = elementToMarkedRawText(_this2.editableDiv, currentNode, currentOffset, _this2.enableHtml());
|
|
23495
|
+
|
|
23496
|
+
/*
|
|
23497
|
+
* A FIELD WITH NOTHING LEFT IN IT NEEDS NO MARK.
|
|
23498
|
+
*
|
|
23499
|
+
* Marking only works on a TEXT node, and an emptied field has none: the browser leaves the caret
|
|
23500
|
+
* in the element itself, so `elementToMarkedRawText` has nothing to mark and returns text with no
|
|
23501
|
+
* mark in it. `insertCharacterBeforeMarks` then warns and hands back its input UNCHANGED, which
|
|
23502
|
+
* reads as success — the value comes out valid with the pasted words missing, and the teacher
|
|
23503
|
+
* watches their field empty itself.
|
|
23504
|
+
*
|
|
23505
|
+
* But an empty field after the delete says something exact: the selection covered everything, so
|
|
23506
|
+
* the result of the paste IS the pasted text. Nothing needs to be worked out. This is the
|
|
23507
|
+
* select-all-and-paste that every report of this bug has been.
|
|
23508
|
+
*/
|
|
23509
|
+
var remainingText = _this2.editableDiv.textContent.split(SMALL_SPACE).join("").trim();
|
|
23510
|
+
var fieldIsEmpty = remainingText === "" && _this2.editableDiv.querySelector(".katex") === null;
|
|
23511
|
+
var marked = markedText.indexOf(SELECTION_MARK) !== -1;
|
|
23512
|
+
|
|
23513
|
+
/*
|
|
23514
|
+
* NOTHING IS BETTER THAN DESTRUCTION.
|
|
23515
|
+
*
|
|
23516
|
+
* No mark and content still standing means the caret cannot be located inside text that must be
|
|
23517
|
+
* kept. Carrying on would write a value with the pasted text missing over a field whose selection
|
|
23518
|
+
* has already been removed from the document — a paste that performs a deletion. Stopping leaves
|
|
23519
|
+
* `props.value` untouched and `reconcileEditableDomWithRender` puts the document back from it:
|
|
23520
|
+
* the paste does nothing, which is a disappointment rather than a loss.
|
|
23521
|
+
*/
|
|
23522
|
+
if (!marked && !fieldIsEmpty) {
|
|
23523
|
+
console.warn("[math-rich-input] paste found nowhere to insert; the field is left as it was", {
|
|
23524
|
+
rangeParams: rangeParams,
|
|
23525
|
+
hadLiveSelection: liveNode !== null
|
|
23526
|
+
});
|
|
23527
|
+
queueMicrotask(function () {
|
|
23528
|
+
try {
|
|
23529
|
+
_this2.reconcileEditableDomWithRender(true);
|
|
23530
|
+
} catch (error) {
|
|
23531
|
+
console.error(error);
|
|
23532
|
+
}
|
|
23533
|
+
});
|
|
23534
|
+
return;
|
|
23535
|
+
}
|
|
23448
23536
|
|
|
23449
23537
|
// Insert the new content at the marked position
|
|
23450
|
-
var newRawText = removeMarks(insertCharacterBeforeMarks(markedText, finalText));
|
|
23451
|
-
|
|
23538
|
+
var newRawText = marked ? removeMarks(insertCharacterBeforeMarks(markedText, finalText)) : finalText;
|
|
23452
23539
|
// Convert any remaining \[...\] LaTeX expressions to <math>...</math> format
|
|
23453
23540
|
// This ensures consistency when paste adds <math> tags alongside existing \[...\] expressions
|
|
23454
23541
|
if (_this2.enableMath()) {
|
|
23455
23542
|
newRawText = newRawText.replace(/\\\[([^\]]*?)\\\]/g, "<math>$1</math>");
|
|
23456
23543
|
}
|
|
23457
23544
|
|
|
23458
|
-
|
|
23545
|
+
/*
|
|
23546
|
+
* Where the caret lands after a paste: at the END of what was pasted.
|
|
23547
|
+
*
|
|
23548
|
+
* The node index walks to just past the last formula, and that part was always right. The OFFSET
|
|
23549
|
+
* within that node was not: it stopped at the boundary, so pasting "…multiply it by 3." left the
|
|
23550
|
+
* caret between the 3 and the full stop. Everything after the last `</math>` is plain text, and
|
|
23551
|
+
* plain text is the one thing whose rendered length is its written length — so the tail can
|
|
23552
|
+
* simply be stepped over. (Markup in the tail is not that, and keeps the old position rather than
|
|
23553
|
+
* a guessed one.)
|
|
23554
|
+
*
|
|
23555
|
+
* Deriving the position from scratch was tried twice and both attempts landed FURTHER away, each
|
|
23556
|
+
* needing an exact model of how the rendered document is measured and each getting a corner of it
|
|
23557
|
+
* wrong. Walking the structure that is already there needs no such model.
|
|
23558
|
+
*/
|
|
23459
23559
|
var newRangeParams = null;
|
|
23460
23560
|
if (hasMathContent) {
|
|
23461
|
-
// Use same approach as equation editor for math content
|
|
23462
23561
|
var oldRangeParams = _this2.getOldRangeParams();
|
|
23463
23562
|
var mathTagCount = (finalText.match(/<math>/gi) || []).length;
|
|
23464
|
-
|
|
23465
|
-
|
|
23466
|
-
|
|
23467
|
-
|
|
23468
|
-
|
|
23469
|
-
|
|
23470
|
-
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
|
|
23474
|
-
newRangeParams = {
|
|
23475
|
-
startNodeIndex: oldRangeParams.startNodeIndex + mathTagCount * 2,
|
|
23476
|
-
startOffset: SMALL_SPACE_LENGTH,
|
|
23477
|
-
endNodeIndex: oldRangeParams.startNodeIndex + mathTagCount * 2,
|
|
23478
|
-
endOffset: SMALL_SPACE_LENGTH
|
|
23479
|
-
};
|
|
23480
|
-
}
|
|
23563
|
+
var startNodeIndex = _this2.props.value === "" ? mathTagCount * 2 : oldRangeParams.startNodeIndex + mathTagCount * 2;
|
|
23564
|
+
var lastMathEnd = finalText.toLowerCase().lastIndexOf("</math>");
|
|
23565
|
+
var tail = lastMathEnd === -1 ? "" : finalText.substring(lastMathEnd + "</math>".length);
|
|
23566
|
+
var startOffset = tail.indexOf("<") === -1 ? SMALL_SPACE_LENGTH + tail.length : SMALL_SPACE_LENGTH;
|
|
23567
|
+
newRangeParams = {
|
|
23568
|
+
startNodeIndex: startNodeIndex,
|
|
23569
|
+
startOffset: startOffset,
|
|
23570
|
+
endNodeIndex: startNodeIndex,
|
|
23571
|
+
endOffset: startOffset
|
|
23572
|
+
};
|
|
23481
23573
|
} else {
|
|
23482
|
-
// For text content, use global offset like before
|
|
23483
23574
|
var newGlobalOffset = rangeParams.startGlobalOffset + finalText.length;
|
|
23484
23575
|
newRangeParams = {
|
|
23485
23576
|
startGlobalOffset: newGlobalOffset,
|
|
@@ -23502,6 +23593,23 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23502
23593
|
|
|
23503
23594
|
// Apply the change using the component's standard flow
|
|
23504
23595
|
_this2.applyChangesToComponent(cleanedRawText, actualMimeType, newRangeParams, _this2.props.useExpertMode, _this2.props.selectedTab);
|
|
23596
|
+
|
|
23597
|
+
// A paste cannot rely on being re-rendered. It removed the selected content from the DOM itself,
|
|
23598
|
+
// and if the host already holds the value being applied — the same words pasted back into the
|
|
23599
|
+
// field they came from — no prop changes, nothing re-renders, and the field is left empty while
|
|
23600
|
+
// the value is correct everywhere else. So: give a real render its chance, then put the content
|
|
23601
|
+
// back if none came.
|
|
23602
|
+
var rangeAfterPaste = newRangeParams;
|
|
23603
|
+
queueMicrotask(function () {
|
|
23604
|
+
try {
|
|
23605
|
+
if (_this2.reconcileEditableDomWithRender(true)) {
|
|
23606
|
+
_this2._setRangeParams(rangeAfterPaste);
|
|
23607
|
+
_this2.updateActiveButtons();
|
|
23608
|
+
}
|
|
23609
|
+
} catch (error) {
|
|
23610
|
+
console.error(error);
|
|
23611
|
+
}
|
|
23612
|
+
});
|
|
23505
23613
|
}
|
|
23506
23614
|
} catch (error) {
|
|
23507
23615
|
console.error("Error in paste handler:", error);
|
|
@@ -23621,7 +23729,11 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
23621
23729
|
|
|
23622
23730
|
// Clear selection anchor on mouse click
|
|
23623
23731
|
_this2.selectionAnchor = null;
|
|
23732
|
+
|
|
23733
|
+
// A click that lands with the selection outside this field — a toolbar button, another field —
|
|
23734
|
+
// gives no range at all. Dereferenced unguarded, this threw on every such click.
|
|
23624
23735
|
var params = _this2._getRangeParams();
|
|
23736
|
+
if (!params) return;
|
|
23625
23737
|
_this2.setOldRangeParams(params);
|
|
23626
23738
|
var startNode = _this2._findNodeWithIndex(params.startNodeIndex);
|
|
23627
23739
|
var endNode = _this2._findNodeWithIndex(params.endNodeIndex);
|
|
@@ -24955,6 +25067,70 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
24955
25067
|
}
|
|
24956
25068
|
return true;
|
|
24957
25069
|
}
|
|
25070
|
+
|
|
25071
|
+
/**
|
|
25072
|
+
* The browser's own reading of an HTML string.
|
|
25073
|
+
*
|
|
25074
|
+
* React renders `<p>​…</P>`; the DOM reports `<p>…</p>` — same content, different bytes. So a
|
|
25075
|
+
* comparison against `innerHTML` has to be made in the DOM's spelling, or it never matches and the
|
|
25076
|
+
* reconciliation below would rewrite the field on every single update.
|
|
25077
|
+
*/
|
|
25078
|
+
}, {
|
|
25079
|
+
key: "normaliseHtml",
|
|
25080
|
+
value: function normaliseHtml(html) {
|
|
25081
|
+
var probe = document.createElement("span");
|
|
25082
|
+
probe.innerHTML = html;
|
|
25083
|
+
return probe.innerHTML;
|
|
25084
|
+
}
|
|
25085
|
+
|
|
25086
|
+
/**
|
|
25087
|
+
* Put back what React believes it rendered, when the live DOM has drifted away from it.
|
|
25088
|
+
*
|
|
25089
|
+
* WHY THIS IS NEEDED AT ALL. `dangerouslySetInnerHTML` writes only when the html STRING changes: React
|
|
25090
|
+
* compares the new `__html` with the previous one and, finding them equal, leaves the DOM alone. That is
|
|
25091
|
+
* sound as long as React is the only thing that touches the DOM — and here it is not. `handlePaste`
|
|
25092
|
+
* removes the selected content itself, through `selection.deleteFromDocument()`, so the field empties
|
|
25093
|
+
* behind React's back while React's record of it still says "full".
|
|
25094
|
+
*
|
|
25095
|
+
* Paste the same words back into the field they came from and the two mistakes meet: the DOM is empty,
|
|
25096
|
+
* the html React computes is identical to the html it rendered last time, so it writes nothing — and
|
|
25097
|
+
* the field stays blank while `value`, the command, and the row in the database are all correct. The
|
|
25098
|
+
* teacher sees their text vanish on paste; nothing anywhere reports an error.
|
|
25099
|
+
*
|
|
25100
|
+
* It writes back the SAME string React itself passed to `dangerouslySetInnerHTML` on this render, so
|
|
25101
|
+
* nothing reaches the DOM here that React was not already putting there — the trust boundary is the
|
|
25102
|
+
* one that existed before, not a new one.
|
|
25103
|
+
*
|
|
25104
|
+
* Only reconciled after a PROGRAMMATIC edit (`afterComponentUpdateData` is set), or when the paste
|
|
25105
|
+
* asks directly (`force`). Ordinary typing goes down the native-input path, where the browser has
|
|
25106
|
+
* already put the character in the right place and the caret with it, and rewriting the DOM there
|
|
25107
|
+
* would move the caret to the front on every keystroke.
|
|
25108
|
+
*
|
|
25109
|
+
* `force` exists because a re-render is not guaranteed to happen AT ALL. When the host already holds
|
|
25110
|
+
* the value being applied — paste the same words back into the field they came from — the prop never
|
|
25111
|
+
* changes, so nothing re-renders and `componentDidUpdate` never runs. `lastRenderedEditableHtml` is
|
|
25112
|
+
* then exactly right: it is the html for that unchanged value, which is what the DOM should hold.
|
|
25113
|
+
*
|
|
25114
|
+
* Returns whether it wrote, so the caller knows whether the caret needs putting back.
|
|
25115
|
+
*/
|
|
25116
|
+
}, {
|
|
25117
|
+
key: "reconcileEditableDomWithRender",
|
|
25118
|
+
value: function reconcileEditableDomWithRender() {
|
|
25119
|
+
var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
25120
|
+
if (!this.editableDiv) return false;
|
|
25121
|
+
if (!force && (this.afterComponentUpdateData === null || this.afterComponentUpdateData === undefined)) return false;
|
|
25122
|
+
var html = force ? rawTextToHtml(katex$1, this.props.value || "", this.props.mimeType) : this.lastRenderedEditableHtml;
|
|
25123
|
+
if (html === null || html === undefined) return false;
|
|
25124
|
+
if (this.editableDiv.innerHTML === this.normaliseHtml(html)) return false;
|
|
25125
|
+
debugMathRichInput("reconcile", {
|
|
25126
|
+
force: force,
|
|
25127
|
+
liveInnerHTML: this.editableDiv.innerHTML,
|
|
25128
|
+
html: html
|
|
25129
|
+
});
|
|
25130
|
+
this.editableDiv.innerHTML = html;
|
|
25131
|
+
this.lastRenderedEditableHtml = html;
|
|
25132
|
+
return true;
|
|
25133
|
+
}
|
|
24958
25134
|
}, {
|
|
24959
25135
|
key: "componentDidUpdate",
|
|
24960
25136
|
value: function componentDidUpdate() {
|
|
@@ -24968,6 +25144,9 @@ var MathRichInput = /*#__PURE__*/function (_React$Component) {
|
|
|
24968
25144
|
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
24969
25145
|
selection: getSelectionSnapshot(this.editableDiv)
|
|
24970
25146
|
});
|
|
25147
|
+
// Before the caret is restored, not after: the offsets below are counted through the text, so
|
|
25148
|
+
// restoring them against an empty field puts the caret at 0 and loses the position as well.
|
|
25149
|
+
this.reconcileEditableDomWithRender();
|
|
24971
25150
|
if (this.afterComponentUpdateData !== null && this.afterComponentUpdateData !== undefined && this.afterComponentUpdateData.value === this.props.value) {
|
|
24972
25151
|
debugMathRichInput("componentDidUpdate:restore-before", {
|
|
24973
25152
|
rangeParams: this.afterComponentUpdateData.rangeParams,
|