@zzish/math-rich-input 0.1.55 → 0.1.57
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 +598 -39
- package/dist/standalone.js +1 -1
- package/package.json +4 -2
- package/src/MathRichInput.jsx +279 -43
- package/src/editTransaction.js +173 -0
- package/src/standalone.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zzish/math-rich-input",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"description": "React component for user to enter rich text with embedded math equations.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
},
|
|
14
14
|
"main": "dist/index.js",
|
|
15
15
|
"scripts": {
|
|
16
|
+
"test": "playwright test tests/caret-regression.spec.js --workers=1",
|
|
16
17
|
"build": "rollup --config && webpack --config webpack.standalone.config.js",
|
|
17
18
|
"build:legacy": "rollup --config",
|
|
18
19
|
"build:standalone": "webpack --config webpack.standalone.config.js",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
+
"prepublishOnly": "npm test && npm run build"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"katex": "^0.12.0",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"@babel/preset-env": "^7.24.5",
|
|
37
38
|
"@babel/preset-react": "^7.24.1",
|
|
38
39
|
"@babel/register": "^7.13.8",
|
|
40
|
+
"@playwright/test": "^1.62.1",
|
|
39
41
|
"@rollup/plugin-babel": "^5.3.0",
|
|
40
42
|
"@rollup/plugin-commonjs": "^17.1.0",
|
|
41
43
|
"@rollup/plugin-node-resolve": "^11.2.0",
|
package/src/MathRichInput.jsx
CHANGED
|
@@ -39,6 +39,10 @@ import {
|
|
|
39
39
|
debugMathRichInput,
|
|
40
40
|
getSelectionSnapshot,
|
|
41
41
|
} from "./mathRichInputDebug";
|
|
42
|
+
import {
|
|
43
|
+
editableText,
|
|
44
|
+
transactionFromBeforeInput,
|
|
45
|
+
} from "./editTransaction";
|
|
42
46
|
|
|
43
47
|
import "./MathRichInput.css";
|
|
44
48
|
import AccentBar from "./AccentBar";
|
|
@@ -84,6 +88,7 @@ const EMPTY_INLINE_FORMATTING_REG_EXP =
|
|
|
84
88
|
// const LATEX_MARKER_END = "</annotation>"
|
|
85
89
|
|
|
86
90
|
const MAX_HISTORY_LENGTH = 100;
|
|
91
|
+
const CONTROLLED_VALUE_ECHO_TIMEOUT_MS = 50;
|
|
87
92
|
|
|
88
93
|
const DEFAULT_OPTIONS = {
|
|
89
94
|
useExpertMode: false,
|
|
@@ -244,6 +249,9 @@ export default class MathRichInput extends React.Component {
|
|
|
244
249
|
this.initialHistoryState = null;
|
|
245
250
|
this.undoHistory = [];
|
|
246
251
|
this.redoHistory = [];
|
|
252
|
+
this.historySequence = 0;
|
|
253
|
+
this.pendingTransaction = null;
|
|
254
|
+
this.compositionStart = null;
|
|
247
255
|
this.accent = "";
|
|
248
256
|
|
|
249
257
|
// keeps track of whether user is composing in an IME - see https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
|
@@ -358,15 +366,44 @@ export default class MathRichInput extends React.Component {
|
|
|
358
366
|
if (isExpertMode === null) isExpertMode = options.isExpertMode;
|
|
359
367
|
if (selectedTab === null) selectedTab = options.selectedTab;
|
|
360
368
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
369
|
+
const transaction = applyOptions.transaction || null;
|
|
370
|
+
const historyState = { value, mimeType, rangeParams, transaction };
|
|
371
|
+
if (applyOptions.recordHistory !== false) {
|
|
372
|
+
// Add this to the history. The transaction is retained beside the rendered value so a host can
|
|
373
|
+
// restore annotation snapshots on undo/redo instead of interpreting an inverse spelling anew.
|
|
374
|
+
this.undoHistory.push(historyState);
|
|
375
|
+
if (this.undoHistory.length > MAX_HISTORY_LENGTH) this.undoHistory.shift();
|
|
376
|
+
}
|
|
364
377
|
|
|
365
378
|
// Debug what's being passed to parent
|
|
366
379
|
// console.log("APPLY CHANGES:", { value, mimeType });
|
|
367
380
|
|
|
368
381
|
// Call the parent handler to apply the changes
|
|
369
|
-
this.onChangeCallback(
|
|
382
|
+
this.onChangeCallback(
|
|
383
|
+
{
|
|
384
|
+
value,
|
|
385
|
+
mimeType,
|
|
386
|
+
...(transaction?.edit ? { edit: transaction.edit } : {}),
|
|
387
|
+
...(transaction?.intent ? { intent: transaction.intent } : {}),
|
|
388
|
+
...(transaction?.history ? { history: transaction.history } : {}),
|
|
389
|
+
},
|
|
390
|
+
{ isExpertMode, selectedTab }
|
|
391
|
+
);
|
|
392
|
+
if (applyOptions.skipControlledRender === true) {
|
|
393
|
+
const pendingNativeValue = value;
|
|
394
|
+
// Give a scheduled controlled echo one short render window. If the host
|
|
395
|
+
// never echoes the value, release the gate rather than preserving a
|
|
396
|
+
// rejected native edit indefinitely.
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
if (
|
|
399
|
+
this.skipNextControlledValueRender === true &&
|
|
400
|
+
this.skipNextControlledValue === pendingNativeValue
|
|
401
|
+
) {
|
|
402
|
+
this.skipNextControlledValueRender = false;
|
|
403
|
+
this.skipNextControlledValue = null;
|
|
404
|
+
}
|
|
405
|
+
}, CONTROLLED_VALUE_ECHO_TIMEOUT_MS);
|
|
406
|
+
}
|
|
370
407
|
debugMathRichInput("applyChangesToComponent:after-onChange", {
|
|
371
408
|
afterComponentUpdateData: this.afterComponentUpdateData,
|
|
372
409
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
@@ -379,6 +416,7 @@ export default class MathRichInput extends React.Component {
|
|
|
379
416
|
value: this.props.value,
|
|
380
417
|
mimeType: this.props.mimeType,
|
|
381
418
|
rangeParams: this._getRangeParams(),
|
|
419
|
+
transaction: null,
|
|
382
420
|
};
|
|
383
421
|
};
|
|
384
422
|
|
|
@@ -409,37 +447,62 @@ export default class MathRichInput extends React.Component {
|
|
|
409
447
|
};
|
|
410
448
|
|
|
411
449
|
undo = () => {
|
|
412
|
-
const currentState = this.undoHistory.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (previousState === null || previousState === undefined) {
|
|
416
|
-
previousState = this.initialHistoryState;
|
|
417
|
-
}
|
|
450
|
+
const currentState = this.undoHistory[this.undoHistory.length - 1];
|
|
451
|
+
if (!currentState) return;
|
|
452
|
+
this.undoHistory.pop();
|
|
418
453
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.redoHistory.push(currentState);
|
|
423
|
-
} else this.redoHistory.push(currentState);
|
|
454
|
+
const previousState = this.undoHistory[this.undoHistory.length - 1] || this.initialHistoryState;
|
|
455
|
+
if (!previousState) return;
|
|
456
|
+
this.redoHistory.push(currentState);
|
|
424
457
|
|
|
458
|
+
const transaction = this.historyTransaction(currentState, "undo");
|
|
425
459
|
this.applyChangesToComponent(
|
|
426
460
|
previousState.value,
|
|
427
461
|
previousState.mimeType,
|
|
428
|
-
previousState.rangeParams
|
|
462
|
+
previousState.rangeParams,
|
|
463
|
+
null,
|
|
464
|
+
null,
|
|
465
|
+
{ transaction, recordHistory: false }
|
|
429
466
|
);
|
|
430
467
|
};
|
|
431
468
|
|
|
432
469
|
redo = () => {
|
|
433
470
|
const redoState = this.redoHistory.pop();
|
|
434
|
-
if (
|
|
471
|
+
if (redoState !== null && redoState !== undefined) {
|
|
435
472
|
this.applyChangesToComponent(
|
|
436
473
|
redoState.value,
|
|
437
474
|
redoState.mimeType,
|
|
438
|
-
redoState.rangeParams
|
|
475
|
+
redoState.rangeParams,
|
|
476
|
+
null,
|
|
477
|
+
null,
|
|
478
|
+
{ transaction: this.historyTransaction(redoState, "redo"), recordHistory: false }
|
|
439
479
|
);
|
|
480
|
+
this.undoHistory.push(redoState);
|
|
440
481
|
}
|
|
441
482
|
};
|
|
442
483
|
|
|
484
|
+
/** Build an explicit forward/inverse transaction for one history entry. */
|
|
485
|
+
historyTransaction = (state, direction) => {
|
|
486
|
+
const original = state?.transaction;
|
|
487
|
+
if (!original?.history?.id || !original.edit) {
|
|
488
|
+
return { intent: direction, history: original?.history ? { id: original.history.id, direction } : undefined };
|
|
489
|
+
}
|
|
490
|
+
const edit = original.edit;
|
|
491
|
+
const after = edit.before.slice(0, edit.from) + edit.insert + edit.before.slice(edit.to);
|
|
492
|
+
const inverse = {
|
|
493
|
+
before: after,
|
|
494
|
+
from: edit.from,
|
|
495
|
+
to: edit.from + edit.insert.length,
|
|
496
|
+
insert: edit.before.slice(edit.from, edit.to),
|
|
497
|
+
...(edit.boundary ? { boundary: edit.boundary } : {}),
|
|
498
|
+
};
|
|
499
|
+
return {
|
|
500
|
+
intent: direction,
|
|
501
|
+
history: { id: original.history.id, direction },
|
|
502
|
+
edit: direction === "undo" ? inverse : edit,
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
443
506
|
setOldRangeParams(params) {
|
|
444
507
|
this.oldRangeParams = params;
|
|
445
508
|
}
|
|
@@ -587,6 +650,9 @@ export default class MathRichInput extends React.Component {
|
|
|
587
650
|
? this.getCurrentRawTextForProps(this.props)
|
|
588
651
|
: null;
|
|
589
652
|
const liveEditableHtml = this.editableDiv ? this.editableDiv.innerHTML : null;
|
|
653
|
+
const pendingNativeInputMatchesLiveDom =
|
|
654
|
+
this.skipNextControlledValueRender === true &&
|
|
655
|
+
currentRawText === this.skipNextControlledValue;
|
|
590
656
|
const canPreserveNativeDom =
|
|
591
657
|
this.state.hasFocus === true &&
|
|
592
658
|
this.editableDiv !== null &&
|
|
@@ -594,7 +660,7 @@ export default class MathRichInput extends React.Component {
|
|
|
594
660
|
(this.afterComponentUpdateData === null ||
|
|
595
661
|
this.afterComponentUpdateData === undefined) &&
|
|
596
662
|
this.lastRenderedEditableHtml !== null &&
|
|
597
|
-
currentRawText === this.props.value;
|
|
663
|
+
(currentRawText === this.props.value || pendingNativeInputMatchesLiveDom);
|
|
598
664
|
const preserveRangeParams = canPreserveNativeDom
|
|
599
665
|
? this.getRangeParamsPreservingInlinePlaceholder()
|
|
600
666
|
: null;
|
|
@@ -609,6 +675,7 @@ export default class MathRichInput extends React.Component {
|
|
|
609
675
|
liveEditableHtml,
|
|
610
676
|
lastRenderedEditableHtml: this.lastRenderedEditableHtml,
|
|
611
677
|
currentRawText,
|
|
678
|
+
pendingNativeInputMatchesLiveDom,
|
|
612
679
|
preserveRangeParams,
|
|
613
680
|
afterComponentUpdateData: this.afterComponentUpdateData,
|
|
614
681
|
hasFocus: this.state.hasFocus,
|
|
@@ -893,7 +960,11 @@ export default class MathRichInput extends React.Component {
|
|
|
893
960
|
const rangeParams = this._getRangeParams();
|
|
894
961
|
if (rangeParams) {
|
|
895
962
|
this.setOldRangeParams(rangeParams);
|
|
896
|
-
this.applyCurrentEditableDomToComponent(rangeParams
|
|
963
|
+
this.applyCurrentEditableDomToComponent(rangeParams, {
|
|
964
|
+
edit: { before: editableText(this.editableDiv), from: 0, to: 0, insert: "" },
|
|
965
|
+
intent: "format",
|
|
966
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
967
|
+
});
|
|
897
968
|
}
|
|
898
969
|
|
|
899
970
|
debugMathRichInput("moveTrailingWhitespaceOutsideInlineStyle:after", {
|
|
@@ -907,7 +978,7 @@ export default class MathRichInput extends React.Component {
|
|
|
907
978
|
return true;
|
|
908
979
|
}
|
|
909
980
|
|
|
910
|
-
applyCurrentEditableDomToComponent(rangeParams = null) {
|
|
981
|
+
applyCurrentEditableDomToComponent(rangeParams = null, transaction = null) {
|
|
911
982
|
if (!this.editableDiv) return;
|
|
912
983
|
if (rangeParams === null || rangeParams === undefined) {
|
|
913
984
|
rangeParams = this._getRangeParams();
|
|
@@ -940,7 +1011,7 @@ export default class MathRichInput extends React.Component {
|
|
|
940
1011
|
rangeParams,
|
|
941
1012
|
this.props.useExpertMode,
|
|
942
1013
|
this.props.selectedTab,
|
|
943
|
-
{ skipControlledRender: true }
|
|
1014
|
+
{ skipControlledRender: true, transaction }
|
|
944
1015
|
);
|
|
945
1016
|
}
|
|
946
1017
|
|
|
@@ -1008,6 +1079,7 @@ export default class MathRichInput extends React.Component {
|
|
|
1008
1079
|
}
|
|
1009
1080
|
|
|
1010
1081
|
handleKeyDownBackspace = (e) => {
|
|
1082
|
+
const beforeText = editableText(this.editableDiv);
|
|
1011
1083
|
let rangeParams = this._getRangeParams();
|
|
1012
1084
|
let start_node_index = rangeParams.startNodeIndex;
|
|
1013
1085
|
let end_node_index = rangeParams.endNodeIndex;
|
|
@@ -1062,7 +1134,14 @@ export default class MathRichInput extends React.Component {
|
|
|
1062
1134
|
this.getMimeType(false, false),
|
|
1063
1135
|
new_rangeParams,
|
|
1064
1136
|
this.props.useExpertMode,
|
|
1065
|
-
this.props.selectedTab
|
|
1137
|
+
this.props.selectedTab,
|
|
1138
|
+
{
|
|
1139
|
+
transaction: {
|
|
1140
|
+
edit: { before: beforeText, from: 0, to: beforeText.length, insert: "" },
|
|
1141
|
+
intent: "cut",
|
|
1142
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
1143
|
+
},
|
|
1144
|
+
}
|
|
1066
1145
|
);
|
|
1067
1146
|
e.preventDefault();
|
|
1068
1147
|
return;
|
|
@@ -2122,6 +2201,9 @@ export default class MathRichInput extends React.Component {
|
|
|
2122
2201
|
// console.log("handleInput")
|
|
2123
2202
|
if (this.isComposing) return;
|
|
2124
2203
|
|
|
2204
|
+
const transaction = this.pendingTransaction;
|
|
2205
|
+
this.pendingTransaction = null;
|
|
2206
|
+
|
|
2125
2207
|
debugMathRichInput("handleInput:start", {
|
|
2126
2208
|
inputType: event && event.nativeEvent ? event.nativeEvent.inputType : null,
|
|
2127
2209
|
data: event && event.nativeEvent ? event.nativeEvent.data : null,
|
|
@@ -2182,7 +2264,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2182
2264
|
defaultParams,
|
|
2183
2265
|
this.props.useExpertMode,
|
|
2184
2266
|
this.props.selectedTab,
|
|
2185
|
-
{ skipControlledRender: true }
|
|
2267
|
+
{ skipControlledRender: true, transaction }
|
|
2186
2268
|
);
|
|
2187
2269
|
return;
|
|
2188
2270
|
} else if (
|
|
@@ -2232,7 +2314,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2232
2314
|
validParams,
|
|
2233
2315
|
this.props.useExpertMode,
|
|
2234
2316
|
this.props.selectedTab,
|
|
2235
|
-
{ skipControlledRender: true }
|
|
2317
|
+
{ skipControlledRender: true, transaction }
|
|
2236
2318
|
);
|
|
2237
2319
|
debugMathRichInput("handleInput:after-apply", {
|
|
2238
2320
|
raw_text,
|
|
@@ -2330,6 +2412,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2330
2412
|
|
|
2331
2413
|
handlePaste = (event) => {
|
|
2332
2414
|
try {
|
|
2415
|
+
this.pendingTransaction = null;
|
|
2333
2416
|
let clipboardData = event.clipboardData || window.clipboardData;
|
|
2334
2417
|
if (!clipboardData) {
|
|
2335
2418
|
return;
|
|
@@ -2355,6 +2438,8 @@ export default class MathRichInput extends React.Component {
|
|
|
2355
2438
|
console.warn("Could not get range params for paste");
|
|
2356
2439
|
return;
|
|
2357
2440
|
}
|
|
2441
|
+
const beforePaste = editableText(this.editableDiv);
|
|
2442
|
+
const pasteSelection = this.captureSelectionOffsets();
|
|
2358
2443
|
|
|
2359
2444
|
// Store old range params for cursor positioning (like equation editor)
|
|
2360
2445
|
this.setOldRangeParams(rangeParams);
|
|
@@ -2627,12 +2712,32 @@ export default class MathRichInput extends React.Component {
|
|
|
2627
2712
|
);
|
|
2628
2713
|
|
|
2629
2714
|
// Apply the change using the component's standard flow
|
|
2715
|
+
const insertionText = (() => {
|
|
2716
|
+
if (!/<[^>]+>/.test(finalText)) return finalText;
|
|
2717
|
+
const probe = document.createElement("div");
|
|
2718
|
+
probe.innerHTML = finalText;
|
|
2719
|
+
return editableText(probe).replace(/\ufffc/g, "");
|
|
2720
|
+
})();
|
|
2721
|
+
const transaction = pasteSelection
|
|
2722
|
+
? {
|
|
2723
|
+
edit: {
|
|
2724
|
+
before: beforePaste,
|
|
2725
|
+
from: pasteSelection.from,
|
|
2726
|
+
to: pasteSelection.to,
|
|
2727
|
+
insert: insertionText,
|
|
2728
|
+
},
|
|
2729
|
+
intent: "paste",
|
|
2730
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
2731
|
+
}
|
|
2732
|
+
: { intent: "paste", history: { id: `mri-${++this.historySequence}`, direction: "forward" } };
|
|
2733
|
+
|
|
2630
2734
|
this.applyChangesToComponent(
|
|
2631
2735
|
cleanedRawText,
|
|
2632
2736
|
actualMimeType,
|
|
2633
2737
|
newRangeParams,
|
|
2634
2738
|
this.props.useExpertMode,
|
|
2635
|
-
this.props.selectedTab
|
|
2739
|
+
this.props.selectedTab,
|
|
2740
|
+
{ transaction }
|
|
2636
2741
|
);
|
|
2637
2742
|
|
|
2638
2743
|
// A paste cannot rely on being re-rendered. It removed the selected content from the DOM itself,
|
|
@@ -2673,6 +2778,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2673
2778
|
|
|
2674
2779
|
this.editableDiv.addEventListener("keydown", this.handleKeyDown);
|
|
2675
2780
|
this.editableDiv.addEventListener("keyup", this.handleKeyUp);
|
|
2781
|
+
this.editableDiv.addEventListener("beforeinput", this.handleBeforeInput);
|
|
2676
2782
|
this.editableDiv.addEventListener("paste", this.handlePaste);
|
|
2677
2783
|
this.editableDiv.addEventListener("copy", this.handleCopy);
|
|
2678
2784
|
if (this.props.autofocus === true) this.editableDiv.focus();
|
|
@@ -2685,6 +2791,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2685
2791
|
try {
|
|
2686
2792
|
this.editableDiv.removeEventListener("keydown", this.handleKeyDown);
|
|
2687
2793
|
this.editableDiv.removeEventListener("keyup", this.handleKeyUp);
|
|
2794
|
+
this.editableDiv.removeEventListener("beforeinput", this.handleBeforeInput);
|
|
2688
2795
|
this.editableDiv.removeEventListener("paste", this.handlePaste);
|
|
2689
2796
|
this.editableDiv.removeEventListener("copy", this.handleCopy);
|
|
2690
2797
|
} catch (error) {
|
|
@@ -2694,22 +2801,32 @@ export default class MathRichInput extends React.Component {
|
|
|
2694
2801
|
|
|
2695
2802
|
shouldComponentUpdate(nextProps, nextState) {
|
|
2696
2803
|
if (this.skipNextControlledValueRender === true) {
|
|
2804
|
+
const controlledValueArrived =
|
|
2805
|
+
nextProps.value === this.skipNextControlledValue;
|
|
2697
2806
|
const canSkip =
|
|
2698
|
-
|
|
2699
|
-
nextState === this.state;
|
|
2807
|
+
controlledValueArrived && nextState === this.state;
|
|
2700
2808
|
|
|
2701
2809
|
debugMathRichInput("shouldComponentUpdate:native-input-gate", {
|
|
2702
2810
|
canSkip,
|
|
2703
2811
|
nextValue: nextProps.value,
|
|
2704
2812
|
skipNextControlledValue: this.skipNextControlledValue,
|
|
2813
|
+
controlledValueArrived,
|
|
2705
2814
|
currentPropsValue: this.props.value,
|
|
2706
2815
|
stateChanged: nextState !== this.state,
|
|
2707
2816
|
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2708
2817
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
2709
2818
|
});
|
|
2710
2819
|
|
|
2711
|
-
|
|
2712
|
-
this.
|
|
2820
|
+
// A controlled host may commit an unrelated render before it echoes the
|
|
2821
|
+
// value from this native input event. Keep the gate armed through that
|
|
2822
|
+
// stale render so getEditableHtmlForRender can preserve the browser DOM
|
|
2823
|
+
// and its selection. The time-bounded fallback armed by
|
|
2824
|
+
// applyChangesToComponent keeps a host that rejects the value from
|
|
2825
|
+
// leaving the gate armed indefinitely.
|
|
2826
|
+
if (controlledValueArrived) {
|
|
2827
|
+
this.skipNextControlledValueRender = false;
|
|
2828
|
+
this.skipNextControlledValue = null;
|
|
2829
|
+
}
|
|
2713
2830
|
|
|
2714
2831
|
if (canSkip) return false;
|
|
2715
2832
|
}
|
|
@@ -3280,12 +3397,19 @@ export default class MathRichInput extends React.Component {
|
|
|
3280
3397
|
}
|
|
3281
3398
|
|
|
3282
3399
|
const rangeParams = this._getRangeParams();
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
this.cleanupEmptyInlineFormattingElements();
|
|
3286
|
-
if (removedEmptyTags && rangeParams !== null && rangeParams !== undefined) {
|
|
3400
|
+
if (!insertedTypingStylePlaceholder) this.cleanupEmptyInlineFormattingElements();
|
|
3401
|
+
if (rangeParams !== null && rangeParams !== undefined) {
|
|
3287
3402
|
this._setRangeParams(rangeParams);
|
|
3288
|
-
this.applyCurrentEditableDomToComponent(rangeParams
|
|
3403
|
+
this.applyCurrentEditableDomToComponent(rangeParams, {
|
|
3404
|
+
edit: {
|
|
3405
|
+
before: editableText(this.editableDiv),
|
|
3406
|
+
from: 0,
|
|
3407
|
+
to: 0,
|
|
3408
|
+
insert: "",
|
|
3409
|
+
},
|
|
3410
|
+
intent: "format",
|
|
3411
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
3412
|
+
});
|
|
3289
3413
|
}
|
|
3290
3414
|
this.setActiveButtonsIfChanged(
|
|
3291
3415
|
this.getActiveButtonsFromSelection(rangeParams)
|
|
@@ -3341,6 +3465,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3341
3465
|
|
|
3342
3466
|
insertRawText = (new_text, selectInsertedText = false) => {
|
|
3343
3467
|
try {
|
|
3468
|
+
const beforeText = editableText(this.editableDiv);
|
|
3344
3469
|
let rangeParams = this._getRangeParams();
|
|
3345
3470
|
let node = this._findNodeWithIndex(rangeParams.endNodeIndex);
|
|
3346
3471
|
let offset = rangeParams.endOffset;
|
|
@@ -3392,7 +3517,19 @@ export default class MathRichInput extends React.Component {
|
|
|
3392
3517
|
this.props.mimeType,
|
|
3393
3518
|
new_rangeParams,
|
|
3394
3519
|
this.props.useExpertMode,
|
|
3395
|
-
this.props.selectedTab
|
|
3520
|
+
this.props.selectedTab,
|
|
3521
|
+
{
|
|
3522
|
+
transaction: {
|
|
3523
|
+
edit: {
|
|
3524
|
+
before: beforeText,
|
|
3525
|
+
from: rangeParams.startGlobalOffset,
|
|
3526
|
+
to: rangeParams.endGlobalOffset,
|
|
3527
|
+
insert: new_text,
|
|
3528
|
+
},
|
|
3529
|
+
intent: "input",
|
|
3530
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
3531
|
+
},
|
|
3532
|
+
}
|
|
3396
3533
|
);
|
|
3397
3534
|
} catch (error) {
|
|
3398
3535
|
console.error(error);
|
|
@@ -3547,6 +3684,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3547
3684
|
|
|
3548
3685
|
removeNewLine = () => {
|
|
3549
3686
|
try {
|
|
3687
|
+
const beforeText = editableText(this.editableDiv);
|
|
3550
3688
|
let rangeParams = this._getRangeParams();
|
|
3551
3689
|
let node = this._findNodeWithIndex(rangeParams.endNodeIndex);
|
|
3552
3690
|
let prevNode = this._findNodeWithIndex(rangeParams.endNodeIndex - 1);
|
|
@@ -3583,7 +3721,19 @@ export default class MathRichInput extends React.Component {
|
|
|
3583
3721
|
this.props.mimeType,
|
|
3584
3722
|
new_rangeParams,
|
|
3585
3723
|
this.props.useExpertMode,
|
|
3586
|
-
this.props.selectedTab
|
|
3724
|
+
this.props.selectedTab,
|
|
3725
|
+
{
|
|
3726
|
+
transaction: {
|
|
3727
|
+
edit: {
|
|
3728
|
+
before: beforeText,
|
|
3729
|
+
from: Math.max(0, rangeParams.endGlobalOffset - 1),
|
|
3730
|
+
to: rangeParams.endGlobalOffset,
|
|
3731
|
+
insert: "",
|
|
3732
|
+
},
|
|
3733
|
+
intent: "input",
|
|
3734
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
3735
|
+
},
|
|
3736
|
+
}
|
|
3587
3737
|
);
|
|
3588
3738
|
} catch (error) {
|
|
3589
3739
|
console.error(error);
|
|
@@ -3592,6 +3742,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3592
3742
|
|
|
3593
3743
|
insertNewLine = () => {
|
|
3594
3744
|
try {
|
|
3745
|
+
const beforeText = editableText(this.editableDiv);
|
|
3595
3746
|
let rangeParams = this._getRangeParams();
|
|
3596
3747
|
let node = this._findNodeWithIndex(rangeParams.endNodeIndex);
|
|
3597
3748
|
let offset = rangeParams.endOffset;
|
|
@@ -3648,14 +3799,26 @@ export default class MathRichInput extends React.Component {
|
|
|
3648
3799
|
this.props.mimeType,
|
|
3649
3800
|
new_rangeParams,
|
|
3650
3801
|
this.props.useExpertMode,
|
|
3651
|
-
this.props.selectedTab
|
|
3802
|
+
this.props.selectedTab,
|
|
3803
|
+
{
|
|
3804
|
+
transaction: {
|
|
3805
|
+
edit: {
|
|
3806
|
+
before: beforeText,
|
|
3807
|
+
from: rangeParams.startGlobalOffset,
|
|
3808
|
+
to: rangeParams.endGlobalOffset,
|
|
3809
|
+
insert: "\n",
|
|
3810
|
+
},
|
|
3811
|
+
intent: "input",
|
|
3812
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
3813
|
+
},
|
|
3814
|
+
}
|
|
3652
3815
|
);
|
|
3653
3816
|
} catch (error) {
|
|
3654
3817
|
console.error(error);
|
|
3655
3818
|
}
|
|
3656
3819
|
};
|
|
3657
3820
|
|
|
3658
|
-
|
|
3821
|
+
insertAfterSmallSpace(new_char, rangeParams) {
|
|
3659
3822
|
try {
|
|
3660
3823
|
if (rangeParams === null || rangeParams === undefined)
|
|
3661
3824
|
rangeParams = this._getRangeParams();
|
|
@@ -3667,6 +3830,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3667
3830
|
" in insertAfterSmallSpace"
|
|
3668
3831
|
);
|
|
3669
3832
|
|
|
3833
|
+
const beforeText = editableText(this.editableDiv);
|
|
3670
3834
|
let node = this._findNodeWithIndex(rangeParams.startNodeIndex);
|
|
3671
3835
|
if (node === null || node === undefined || node.nodeValue === null) {
|
|
3672
3836
|
console.warn("Could not find small-space text node");
|
|
@@ -3706,14 +3870,21 @@ export default class MathRichInput extends React.Component {
|
|
|
3706
3870
|
this.enableHtml()
|
|
3707
3871
|
);
|
|
3708
3872
|
|
|
3709
|
-
|
|
3873
|
+
this.applyChangesToComponent(
|
|
3710
3874
|
new_raw_text,
|
|
3711
3875
|
this.props.mimeType,
|
|
3712
3876
|
new_rangeParams,
|
|
3713
3877
|
this.props.useExpertMode,
|
|
3714
3878
|
this.props.selectedTab,
|
|
3715
|
-
{
|
|
3716
|
-
|
|
3879
|
+
{
|
|
3880
|
+
skipControlledRender: true,
|
|
3881
|
+
transaction: {
|
|
3882
|
+
edit: { before: beforeText, from: rangeParams.startGlobalOffset, to: rangeParams.startGlobalOffset, insert: new_char },
|
|
3883
|
+
intent: "input",
|
|
3884
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
3885
|
+
},
|
|
3886
|
+
}
|
|
3887
|
+
);
|
|
3717
3888
|
} catch (error) {
|
|
3718
3889
|
console.error(error);
|
|
3719
3890
|
}
|
|
@@ -3721,6 +3892,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3721
3892
|
|
|
3722
3893
|
insertAccentLetter = (event, old_char, new_char) => {
|
|
3723
3894
|
try {
|
|
3895
|
+
const beforeText = editableText(this.editableDiv);
|
|
3724
3896
|
let rangeParams = this._getRangeParams();
|
|
3725
3897
|
let node = this._findNodeWithIndex(rangeParams.startNodeIndex);
|
|
3726
3898
|
let offset = rangeParams.startOffset;
|
|
@@ -3767,7 +3939,19 @@ export default class MathRichInput extends React.Component {
|
|
|
3767
3939
|
this.props.mimeType,
|
|
3768
3940
|
new_rangeParams,
|
|
3769
3941
|
this.props.useExpertMode,
|
|
3770
|
-
this.props.selectedTab
|
|
3942
|
+
this.props.selectedTab,
|
|
3943
|
+
{
|
|
3944
|
+
transaction: {
|
|
3945
|
+
edit: {
|
|
3946
|
+
before: beforeText,
|
|
3947
|
+
from: old_char === null ? rangeParams.startGlobalOffset : Math.max(0, rangeParams.startGlobalOffset - 1),
|
|
3948
|
+
to: old_char === null ? rangeParams.startGlobalOffset : rangeParams.startGlobalOffset,
|
|
3949
|
+
insert: new_char,
|
|
3950
|
+
},
|
|
3951
|
+
intent: "input",
|
|
3952
|
+
history: { id: `mri-${++this.historySequence}`, direction: "forward" },
|
|
3953
|
+
},
|
|
3954
|
+
}
|
|
3771
3955
|
);
|
|
3772
3956
|
} catch (error) {
|
|
3773
3957
|
console.error(error);
|
|
@@ -3883,11 +4067,63 @@ export default class MathRichInput extends React.Component {
|
|
|
3883
4067
|
};
|
|
3884
4068
|
|
|
3885
4069
|
handleCompositionStart = (event) => {
|
|
4070
|
+
const offsets = this.captureSelectionOffsets();
|
|
4071
|
+
this.compositionStart = {
|
|
4072
|
+
before: editableText(this.editableDiv),
|
|
4073
|
+
from: offsets?.from ?? 0,
|
|
4074
|
+
to: offsets?.to ?? 0,
|
|
4075
|
+
};
|
|
3886
4076
|
this.isComposing = true;
|
|
3887
4077
|
};
|
|
3888
4078
|
|
|
3889
4079
|
handleCompositionEnd = (event) => {
|
|
3890
4080
|
this.isComposing = false;
|
|
4081
|
+
this.compositionStart = null;
|
|
4082
|
+
};
|
|
4083
|
+
|
|
4084
|
+
captureSelectionOffsets = () => {
|
|
4085
|
+
try {
|
|
4086
|
+
const selection = window.getSelection();
|
|
4087
|
+
if (!selection || selection.rangeCount === 0) return null;
|
|
4088
|
+
const range = selection.getRangeAt(0);
|
|
4089
|
+
const before = editableText(this.editableDiv);
|
|
4090
|
+
const endpoint = (container, offset) => {
|
|
4091
|
+
const probe = document.createRange();
|
|
4092
|
+
probe.selectNodeContents(this.editableDiv);
|
|
4093
|
+
probe.setEnd(container, offset);
|
|
4094
|
+
const wrapper = document.createElement("span");
|
|
4095
|
+
wrapper.appendChild(probe.cloneContents());
|
|
4096
|
+
return editableText(wrapper).length;
|
|
4097
|
+
};
|
|
4098
|
+
const from = endpoint(range.startContainer, range.startOffset);
|
|
4099
|
+
const to = endpoint(range.endContainer, range.endOffset);
|
|
4100
|
+
return { before, from: Math.min(from, to), to: Math.max(from, to) };
|
|
4101
|
+
} catch (_error) {
|
|
4102
|
+
return null;
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
|
|
4106
|
+
/** Capture an input intent before the browser mutates the contenteditable DOM. */
|
|
4107
|
+
handleBeforeInput = (event) => {
|
|
4108
|
+
try {
|
|
4109
|
+
const captured = transactionFromBeforeInput(
|
|
4110
|
+
this.editableDiv,
|
|
4111
|
+
event,
|
|
4112
|
+
this.compositionStart
|
|
4113
|
+
? { before: this.compositionStart.before, from: this.compositionStart.from, to: this.compositionStart.to }
|
|
4114
|
+
: null
|
|
4115
|
+
);
|
|
4116
|
+
const id = `mri-${++this.historySequence}`;
|
|
4117
|
+
this.pendingTransaction = {
|
|
4118
|
+
edit: captured.edit || undefined,
|
|
4119
|
+
intent: captured.intent,
|
|
4120
|
+
history: { id, direction: "forward" },
|
|
4121
|
+
};
|
|
4122
|
+
} catch (_error) {
|
|
4123
|
+
this.pendingTransaction = {
|
|
4124
|
+
intent: "programmatic",
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
3891
4127
|
};
|
|
3892
4128
|
|
|
3893
4129
|
// Comprehensive LaTeX pattern detection and auto-wrapping in math tags
|