@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 +1125 -192
- package/dist/standalone.js +1 -1
- package/package.json +3 -3
- package/src/MathRichArea.jsx +5 -2
- package/src/MathRichInput.jsx +1139 -237
- package/src/Toolbar.jsx +3 -4
- package/src/mathRichInputDebug.js +98 -0
- package/src/mathRichInputHelper.js +128 -17
package/src/MathRichInput.jsx
CHANGED
|
@@ -34,6 +34,10 @@ import {
|
|
|
34
34
|
globalOffsetToPlainTextOffset,
|
|
35
35
|
plainTextOffsetToGlobalOffset,
|
|
36
36
|
} from "./mathRichInputHelper";
|
|
37
|
+
import {
|
|
38
|
+
debugMathRichInput,
|
|
39
|
+
getSelectionSnapshot,
|
|
40
|
+
} from "./mathRichInputDebug";
|
|
37
41
|
|
|
38
42
|
import "./MathRichInput.css";
|
|
39
43
|
import AccentBar from "./AccentBar";
|
|
@@ -50,6 +54,30 @@ const SMALL_SPACE_LENGTH = SMALL_SPACE.length;
|
|
|
50
54
|
|
|
51
55
|
const MARK = getMark();
|
|
52
56
|
const MARK_REG_EXP = new RegExp(MARK, "g");
|
|
57
|
+
const INLINE_STYLE_COMMANDS = {
|
|
58
|
+
bold: "bold",
|
|
59
|
+
italic: "italic",
|
|
60
|
+
underline: "underline",
|
|
61
|
+
subscript: "subscript",
|
|
62
|
+
superscript: "superscript",
|
|
63
|
+
};
|
|
64
|
+
const INLINE_STYLE_TAGS = {
|
|
65
|
+
bold: ["b", "strong"],
|
|
66
|
+
italic: ["i", "em"],
|
|
67
|
+
underline: ["u"],
|
|
68
|
+
subscript: ["sub"],
|
|
69
|
+
superscript: ["sup"],
|
|
70
|
+
};
|
|
71
|
+
const INLINE_STYLE_TAG_NAMES = ["b", "strong", "i", "em", "u", "sub", "sup"];
|
|
72
|
+
const INLINE_STYLE_WRAPPER_TAGS = {
|
|
73
|
+
bold: "strong",
|
|
74
|
+
italic: "em",
|
|
75
|
+
underline: "u",
|
|
76
|
+
subscript: "sub",
|
|
77
|
+
superscript: "sup",
|
|
78
|
+
};
|
|
79
|
+
const EMPTY_INLINE_FORMATTING_REG_EXP =
|
|
80
|
+
/<(B|STRONG|I|EM|U|SUB|SUP)>(?:\s| |\u00a0|\u200b|\ufeff)*<\/\1>/gi;
|
|
53
81
|
|
|
54
82
|
// const LATEX_MARKER_START = "<annotation encoding=\"application/x-tex\">"
|
|
55
83
|
// const LATEX_MARKER_END = "</annotation>"
|
|
@@ -190,17 +218,20 @@ export default class MathRichInput extends React.Component {
|
|
|
190
218
|
|
|
191
219
|
// Elements that need to be set before the equation editor is shown
|
|
192
220
|
this.markedRawText = null;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
endOffset: 0,
|
|
198
|
-
};
|
|
221
|
+
// No selection has been captured yet. Do not default this to the start of
|
|
222
|
+
// the editor: React 19 focus/click ordering makes handleFocus restore that
|
|
223
|
+
// synthetic 0,0 range and moves the caret to the beginning on every click.
|
|
224
|
+
this.oldRangeParams = null;
|
|
199
225
|
this.moveCursorOnInsert = false;
|
|
200
226
|
|
|
201
227
|
// Elements defining the current state of range
|
|
202
228
|
this.afterComponentUpdateData = null; // Set this to update the selection after rendering
|
|
203
|
-
this.
|
|
229
|
+
this.lastSetRangeParams = null; // This is set automatically each time the range params are set with setRangeParams
|
|
230
|
+
this.skipNextControlledValueRender = false;
|
|
231
|
+
this.skipNextControlledValue = null;
|
|
232
|
+
this.isMouseDownInEditable = false;
|
|
233
|
+
this.lastRenderedEditableHtml = null;
|
|
234
|
+
this.preserveNativeDomRangeParams = null;
|
|
204
235
|
|
|
205
236
|
this.lastSetActiveButtons = null;
|
|
206
237
|
|
|
@@ -292,12 +323,35 @@ export default class MathRichInput extends React.Component {
|
|
|
292
323
|
mimeType,
|
|
293
324
|
rangeParams,
|
|
294
325
|
isExpertMode,
|
|
295
|
-
selectedTab
|
|
326
|
+
selectedTab,
|
|
327
|
+
applyOptions = {}
|
|
296
328
|
) => {
|
|
297
329
|
// rangeParams, isExpertMode and selectedTab can be null, in which case we will set them
|
|
298
330
|
if (rangeParams === null) rangeParams = this.lastSetRangeParams;
|
|
299
331
|
|
|
300
|
-
|
|
332
|
+
debugMathRichInput("applyChangesToComponent:start", {
|
|
333
|
+
value,
|
|
334
|
+
mimeType,
|
|
335
|
+
rangeParams,
|
|
336
|
+
propsValue: this.props.value,
|
|
337
|
+
propsMimeType: this.props.mimeType,
|
|
338
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
339
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
340
|
+
skipControlledRender: applyOptions.skipControlledRender === true,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (applyOptions.skipControlledRender === true) {
|
|
344
|
+
// Native contenteditable input has already mutated the DOM and kept the
|
|
345
|
+
// browser selection in the right place. Re-rendering the same value via
|
|
346
|
+
// dangerouslySetInnerHTML can replace text nodes under React 19 and move
|
|
347
|
+
// the caret to the beginning. Programmatic edits still use the normal
|
|
348
|
+
// render-and-restore path.
|
|
349
|
+
this.skipNextControlledValueRender = true;
|
|
350
|
+
this.skipNextControlledValue = value;
|
|
351
|
+
this.afterComponentUpdateData = null;
|
|
352
|
+
} else {
|
|
353
|
+
this.afterComponentUpdateData = { rangeParams, value };
|
|
354
|
+
}
|
|
301
355
|
|
|
302
356
|
let options = this.props.options || DEFAULT_OPTIONS;
|
|
303
357
|
if (isExpertMode === null) isExpertMode = options.isExpertMode;
|
|
@@ -312,6 +366,10 @@ export default class MathRichInput extends React.Component {
|
|
|
312
366
|
|
|
313
367
|
// Call the parent handler to apply the changes
|
|
314
368
|
this.onChangeCallback({ value, mimeType }, { isExpertMode, selectedTab });
|
|
369
|
+
debugMathRichInput("applyChangesToComponent:after-onChange", {
|
|
370
|
+
afterComponentUpdateData: this.afterComponentUpdateData,
|
|
371
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
372
|
+
});
|
|
315
373
|
};
|
|
316
374
|
|
|
317
375
|
initialiseHistory = () => {
|
|
@@ -392,23 +450,550 @@ export default class MathRichInput extends React.Component {
|
|
|
392
450
|
return params;
|
|
393
451
|
}
|
|
394
452
|
|
|
453
|
+
rangeIsAtStart(params) {
|
|
454
|
+
if (params === null || params === undefined) return false;
|
|
455
|
+
if (
|
|
456
|
+
params.startGlobalOffset !== null &&
|
|
457
|
+
params.startGlobalOffset !== undefined &&
|
|
458
|
+
params.endGlobalOffset !== null &&
|
|
459
|
+
params.endGlobalOffset !== undefined
|
|
460
|
+
) {
|
|
461
|
+
return params.startGlobalOffset === 0 && params.endGlobalOffset === 0;
|
|
462
|
+
}
|
|
463
|
+
return (
|
|
464
|
+
params.startNodeIndex === 0 &&
|
|
465
|
+
params.endNodeIndex === 0 &&
|
|
466
|
+
params.startOffset === 0 &&
|
|
467
|
+
params.endOffset === 0
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
rangeHasNonZeroPosition(params) {
|
|
472
|
+
if (params === null || params === undefined) return false;
|
|
473
|
+
if (
|
|
474
|
+
params.startGlobalOffset !== null &&
|
|
475
|
+
params.startGlobalOffset !== undefined
|
|
476
|
+
) {
|
|
477
|
+
return params.startGlobalOffset > 0;
|
|
478
|
+
}
|
|
479
|
+
return (
|
|
480
|
+
params.startNodeIndex > 0 ||
|
|
481
|
+
params.endNodeIndex > 0 ||
|
|
482
|
+
params.startOffset > 0 ||
|
|
483
|
+
params.endOffset > 0
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
getCurrentRawTextForProps(nextProps) {
|
|
488
|
+
if (!this.editableDiv) return null;
|
|
489
|
+
try {
|
|
490
|
+
let rawText = elementToMarkedRawText(
|
|
491
|
+
this.editableDiv,
|
|
492
|
+
null,
|
|
493
|
+
0,
|
|
494
|
+
this.enableHtml()
|
|
495
|
+
);
|
|
496
|
+
rawText = this.stripEmptyInlineFormattingTags(rawText);
|
|
497
|
+
return removeEncodingIfPlainText(
|
|
498
|
+
rawText,
|
|
499
|
+
nextProps.mimeType,
|
|
500
|
+
this.enableHtml()
|
|
501
|
+
);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
debugMathRichInput("getCurrentRawTextForProps:error", {
|
|
504
|
+
error,
|
|
505
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
506
|
+
});
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
shouldSkipRedundantFocusedRender(nextProps, nextState) {
|
|
512
|
+
if (!this.editableDiv) return false;
|
|
513
|
+
if (nextState !== this.state) return false;
|
|
514
|
+
if (nextState.hasFocus !== true) return false;
|
|
515
|
+
if (
|
|
516
|
+
this.afterComponentUpdateData !== null &&
|
|
517
|
+
this.afterComponentUpdateData !== undefined
|
|
518
|
+
) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const currentRawText = this.getCurrentRawTextForProps(nextProps);
|
|
523
|
+
const shouldSkip = currentRawText === nextProps.value;
|
|
524
|
+
debugMathRichInput("shouldComponentUpdate:redundant-focused-check", {
|
|
525
|
+
shouldSkip,
|
|
526
|
+
currentRawText,
|
|
527
|
+
nextValue: nextProps.value,
|
|
528
|
+
currentPropsValue: this.props.value,
|
|
529
|
+
nextMimeType: nextProps.mimeType,
|
|
530
|
+
currentMimeType: this.props.mimeType,
|
|
531
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (shouldSkip) {
|
|
535
|
+
const currentRangeParams = this.getRangeParamsPreservingInlinePlaceholder();
|
|
536
|
+
if (this.rangeHasNonZeroPosition(currentRangeParams)) {
|
|
537
|
+
this.setOldRangeParams(currentRangeParams);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return shouldSkip;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
isSelectionInsideInlinePlaceholder() {
|
|
545
|
+
if (typeof document === "undefined") return false;
|
|
546
|
+
|
|
547
|
+
const selection = document.getSelection ? document.getSelection() : null;
|
|
548
|
+
if (!selection || selection.rangeCount === 0) return false;
|
|
549
|
+
|
|
550
|
+
const range = selection.getRangeAt(0);
|
|
551
|
+
if (!range.collapsed) return false;
|
|
552
|
+
|
|
553
|
+
const node = range.startContainer;
|
|
554
|
+
if (!node || node.nodeType !== 3) return false;
|
|
555
|
+
if (node.nodeValue !== SMALL_SPACE) return false;
|
|
556
|
+
if (range.startOffset !== SMALL_SPACE_LENGTH) return false;
|
|
557
|
+
|
|
558
|
+
const parent = node.parentNode;
|
|
559
|
+
if (!parent || parent.nodeType !== 1) return false;
|
|
560
|
+
|
|
561
|
+
const tagName = parent.nodeName.toLowerCase();
|
|
562
|
+
if (!INLINE_STYLE_TAG_NAMES.includes(tagName)) return false;
|
|
563
|
+
|
|
564
|
+
return parent.textContent === SMALL_SPACE;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
getRangeParamsPreservingInlinePlaceholder() {
|
|
568
|
+
const selectionInsideInlinePlaceholder =
|
|
569
|
+
this.isSelectionInsideInlinePlaceholder();
|
|
570
|
+
const rangeParams = this._getRangeParams();
|
|
571
|
+
|
|
572
|
+
if (!selectionInsideInlinePlaceholder || !rangeParams) {
|
|
573
|
+
return rangeParams;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Global offsets ignore the zero-width placeholder; node index preserves it.
|
|
577
|
+
return {
|
|
578
|
+
...rangeParams,
|
|
579
|
+
startGlobalOffset: null,
|
|
580
|
+
endGlobalOffset: null,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
getEditableHtmlForRender(html) {
|
|
585
|
+
const currentRawText = this.editableDiv
|
|
586
|
+
? this.getCurrentRawTextForProps(this.props)
|
|
587
|
+
: null;
|
|
588
|
+
const liveEditableHtml = this.editableDiv ? this.editableDiv.innerHTML : null;
|
|
589
|
+
const canPreserveNativeDom =
|
|
590
|
+
this.state.hasFocus === true &&
|
|
591
|
+
this.editableDiv !== null &&
|
|
592
|
+
this.editableDiv !== undefined &&
|
|
593
|
+
(this.afterComponentUpdateData === null ||
|
|
594
|
+
this.afterComponentUpdateData === undefined) &&
|
|
595
|
+
this.lastRenderedEditableHtml !== null &&
|
|
596
|
+
currentRawText === this.props.value;
|
|
597
|
+
const preserveRangeParams = canPreserveNativeDom
|
|
598
|
+
? this.getRangeParamsPreservingInlinePlaceholder()
|
|
599
|
+
: null;
|
|
600
|
+
|
|
601
|
+
debugMathRichInput("render:editable-html", {
|
|
602
|
+
canPreserveNativeDom,
|
|
603
|
+
selectionInsideInlinePlaceholder:
|
|
604
|
+
this.isSelectionInsideInlinePlaceholder(),
|
|
605
|
+
propsValue: this.props.value,
|
|
606
|
+
generatedHtml: html,
|
|
607
|
+
renderedHtml: canPreserveNativeDom ? liveEditableHtml : html,
|
|
608
|
+
liveEditableHtml,
|
|
609
|
+
lastRenderedEditableHtml: this.lastRenderedEditableHtml,
|
|
610
|
+
currentRawText,
|
|
611
|
+
preserveRangeParams,
|
|
612
|
+
afterComponentUpdateData: this.afterComponentUpdateData,
|
|
613
|
+
hasFocus: this.state.hasFocus,
|
|
614
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
if (canPreserveNativeDom) {
|
|
618
|
+
this.lastRenderedEditableHtml = liveEditableHtml;
|
|
619
|
+
this.preserveNativeDomRangeParams = preserveRangeParams;
|
|
620
|
+
return liveEditableHtml;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
this.lastRenderedEditableHtml = html;
|
|
624
|
+
this.preserveNativeDomRangeParams = null;
|
|
625
|
+
return html;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
stripEmptyInlineFormattingTags(rawText) {
|
|
629
|
+
if (rawText === null || rawText === undefined) return rawText;
|
|
630
|
+
let cleaned = rawText;
|
|
631
|
+
let previous = null;
|
|
632
|
+
while (previous !== cleaned) {
|
|
633
|
+
previous = cleaned;
|
|
634
|
+
cleaned = cleaned.replace(EMPTY_INLINE_FORMATTING_REG_EXP, "");
|
|
635
|
+
}
|
|
636
|
+
return cleaned;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
cleanupEmptyInlineFormattingElements() {
|
|
640
|
+
if (!this.editableDiv) return false;
|
|
641
|
+
|
|
642
|
+
let removed = false;
|
|
643
|
+
this.editableDiv
|
|
644
|
+
.querySelectorAll("b,strong,i,em,u,sub,sup")
|
|
645
|
+
.forEach((node) => {
|
|
646
|
+
const text = node.textContent || "";
|
|
647
|
+
const hasMeaningfulText = text.replace(/[\s\u00a0\u200b\ufeff]/g, "") !== "";
|
|
648
|
+
const hasProtectedContent =
|
|
649
|
+
node.querySelector("br,img,math,.katex") !== null;
|
|
650
|
+
|
|
651
|
+
if (!hasMeaningfulText && !hasProtectedContent) {
|
|
652
|
+
node.remove();
|
|
653
|
+
removed = true;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (removed) {
|
|
658
|
+
this.editableDiv.normalize();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return removed;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
getStyleCommand(style) {
|
|
665
|
+
return INLINE_STYLE_COMMANDS[style] || style;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
insertInlineStylePlaceholder(style) {
|
|
669
|
+
const wrapperTag = INLINE_STYLE_WRAPPER_TAGS[style] || "span";
|
|
670
|
+
const selection = document.getSelection();
|
|
671
|
+
if (!selection || selection.rangeCount === 0) return false;
|
|
672
|
+
|
|
673
|
+
const range = selection.getRangeAt(0);
|
|
674
|
+
if (!range.collapsed) return false;
|
|
675
|
+
|
|
676
|
+
range.deleteContents();
|
|
677
|
+
|
|
678
|
+
const wrapper = document.createElement(wrapperTag);
|
|
679
|
+
const textNode = document.createTextNode(SMALL_SPACE);
|
|
680
|
+
wrapper.appendChild(textNode);
|
|
681
|
+
range.insertNode(wrapper);
|
|
682
|
+
|
|
683
|
+
const nextRange = document.createRange();
|
|
684
|
+
nextRange.setStart(textNode, SMALL_SPACE_LENGTH);
|
|
685
|
+
nextRange.collapse(true);
|
|
686
|
+
selection.removeAllRanges();
|
|
687
|
+
selection.addRange(nextRange);
|
|
688
|
+
|
|
689
|
+
const rangeParams = this.getRangeParamsPreservingInlinePlaceholder();
|
|
690
|
+
if (rangeParams) this.setOldRangeParams(rangeParams);
|
|
691
|
+
|
|
692
|
+
debugMathRichInput("insertInlineStylePlaceholder:after", {
|
|
693
|
+
style,
|
|
694
|
+
wrapperTag,
|
|
695
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
696
|
+
rangeParams,
|
|
697
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
getActiveButtonsFromCommandState() {
|
|
704
|
+
const query = (command) => {
|
|
705
|
+
try {
|
|
706
|
+
return document.queryCommandState(command);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
bold: query("bold"),
|
|
714
|
+
italic: query("italic"),
|
|
715
|
+
underline: query("underline"),
|
|
716
|
+
subscript: query("subscript"),
|
|
717
|
+
superscript: query("superscript"),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
isSelectionCollapsed() {
|
|
722
|
+
const selection = document.getSelection();
|
|
723
|
+
if (!selection || selection.rangeCount === 0) return true;
|
|
724
|
+
return selection.getRangeAt(0).collapsed;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
getActiveButtonsFromAncestorState(rangeParams = null) {
|
|
728
|
+
if (rangeParams === null || rangeParams === undefined) {
|
|
729
|
+
rangeParams = this._getRangeParams();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let activeButtons = {
|
|
733
|
+
bold: false,
|
|
734
|
+
italic: false,
|
|
735
|
+
underline: false,
|
|
736
|
+
subscript: false,
|
|
737
|
+
superscript: false,
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
if (
|
|
741
|
+
rangeParams === null ||
|
|
742
|
+
rangeParams === undefined ||
|
|
743
|
+
rangeParams.startNodeIndex < 0
|
|
744
|
+
) {
|
|
745
|
+
return activeButtons;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let node = this._findNodeWithIndex(rangeParams.startNodeIndex);
|
|
749
|
+
if (node === null || node === undefined) return activeButtons;
|
|
750
|
+
|
|
751
|
+
do {
|
|
752
|
+
if (node.nodeType === 1) {
|
|
753
|
+
let nodeName = node.nodeName.toLowerCase();
|
|
754
|
+
if (nodeName === "b" || nodeName === "strong") activeButtons.bold = true;
|
|
755
|
+
else if (nodeName === "i" || nodeName === "em")
|
|
756
|
+
activeButtons.italic = true;
|
|
757
|
+
else if (nodeName === "u") activeButtons.underline = true;
|
|
758
|
+
else if (nodeName === "sub") activeButtons.subscript = true;
|
|
759
|
+
else if (nodeName === "sup") activeButtons.superscript = true;
|
|
760
|
+
} else if (
|
|
761
|
+
node.classList !== null &&
|
|
762
|
+
node.classList !== undefined &&
|
|
763
|
+
node.classList.contains("MathRichInput")
|
|
764
|
+
)
|
|
765
|
+
break;
|
|
766
|
+
node = node.parentNode;
|
|
767
|
+
} while (node !== null);
|
|
768
|
+
|
|
769
|
+
return activeButtons;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
getActiveButtonsFromSelection(rangeParams = null) {
|
|
773
|
+
const commandState = this.getActiveButtonsFromCommandState();
|
|
774
|
+
const ancestorState = this.getActiveButtonsFromAncestorState(rangeParams);
|
|
775
|
+
return {
|
|
776
|
+
bold: commandState.bold || ancestorState.bold,
|
|
777
|
+
italic: commandState.italic || ancestorState.italic,
|
|
778
|
+
underline: commandState.underline || ancestorState.underline,
|
|
779
|
+
subscript: commandState.subscript || ancestorState.subscript,
|
|
780
|
+
superscript: commandState.superscript || ancestorState.superscript,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
setActiveButtonsIfChanged(activeButtons) {
|
|
785
|
+
let update = true;
|
|
786
|
+
if (this.lastSetActiveButtons !== null) {
|
|
787
|
+
update = false;
|
|
788
|
+
for (var style in activeButtons) {
|
|
789
|
+
if (activeButtons[style] !== this.lastSetActiveButtons[style]) {
|
|
790
|
+
update = true;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (update) {
|
|
797
|
+
this.lastSetActiveButtons = activeButtons;
|
|
798
|
+
this.setState({ activeButtons: activeButtons });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
getInlineStyleAncestor(style) {
|
|
803
|
+
const tags = INLINE_STYLE_TAGS[style];
|
|
804
|
+
if (!tags) return null;
|
|
805
|
+
|
|
806
|
+
const selection = document.getSelection();
|
|
807
|
+
if (!selection || selection.rangeCount === 0) return null;
|
|
808
|
+
|
|
809
|
+
let node = selection.getRangeAt(0).startContainer;
|
|
810
|
+
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
|
|
811
|
+
|
|
812
|
+
while (node && node !== this.editableDiv) {
|
|
813
|
+
if (
|
|
814
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
815
|
+
tags.includes(node.nodeName.toLowerCase())
|
|
816
|
+
) {
|
|
817
|
+
return node;
|
|
818
|
+
}
|
|
819
|
+
node = node.parentNode;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
isRangeAtEndOfNode(range, node) {
|
|
826
|
+
try {
|
|
827
|
+
const afterRange = document.createRange();
|
|
828
|
+
afterRange.setStart(range.startContainer, range.startOffset);
|
|
829
|
+
afterRange.setEnd(node, node.childNodes.length);
|
|
830
|
+
const textAfterCaret = afterRange
|
|
831
|
+
.toString()
|
|
832
|
+
.replace(/[\s\u00a0\u200b\ufeff]/g, "");
|
|
833
|
+
if (textAfterCaret === "") return true;
|
|
834
|
+
} catch (error) {
|
|
835
|
+
// Fall back to boundary comparison below if the browser rejects the range.
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const nodeEndRange = document.createRange();
|
|
839
|
+
nodeEndRange.selectNodeContents(node);
|
|
840
|
+
nodeEndRange.collapse(false);
|
|
841
|
+
return range.compareBoundaryPoints(Range.START_TO_START, nodeEndRange) === 0;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
moveTrailingWhitespaceOutsideInlineStyle(style, command) {
|
|
845
|
+
const selection = document.getSelection();
|
|
846
|
+
if (!selection || selection.rangeCount === 0) return false;
|
|
847
|
+
|
|
848
|
+
const range = selection.getRangeAt(0);
|
|
849
|
+
if (!range.collapsed) return false;
|
|
850
|
+
|
|
851
|
+
const ancestor = this.getInlineStyleAncestor(style);
|
|
852
|
+
if (!ancestor || !this.isRangeAtEndOfNode(range, ancestor)) return false;
|
|
853
|
+
if (range.startContainer.nodeType !== Node.TEXT_NODE) return false;
|
|
854
|
+
|
|
855
|
+
const textNode = range.startContainer;
|
|
856
|
+
const text = textNode.nodeValue || "";
|
|
857
|
+
let moveStart = range.startOffset;
|
|
858
|
+
while (moveStart > 0) {
|
|
859
|
+
const char = text.charAt(moveStart - 1);
|
|
860
|
+
if (char !== " " && char !== "\u00a0") break;
|
|
861
|
+
moveStart -= 1;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (moveStart === range.startOffset) return false;
|
|
865
|
+
|
|
866
|
+
const movedText = text.substring(moveStart, range.startOffset);
|
|
867
|
+
textNode.nodeValue =
|
|
868
|
+
text.substring(0, moveStart) + text.substring(range.startOffset);
|
|
869
|
+
|
|
870
|
+
let outsideNode = ancestor.nextSibling;
|
|
871
|
+
if (!outsideNode || outsideNode.nodeType !== Node.TEXT_NODE) {
|
|
872
|
+
outsideNode = document.createTextNode(movedText);
|
|
873
|
+
ancestor.parentNode.insertBefore(outsideNode, ancestor.nextSibling);
|
|
874
|
+
} else {
|
|
875
|
+
outsideNode.nodeValue = movedText + outsideNode.nodeValue;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const nextRange = document.createRange();
|
|
879
|
+
nextRange.setStart(outsideNode, movedText.length);
|
|
880
|
+
nextRange.collapse(true);
|
|
881
|
+
selection.removeAllRanges();
|
|
882
|
+
selection.addRange(nextRange);
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
if (document.queryCommandState(command)) {
|
|
886
|
+
document.execCommand(command, false, null);
|
|
887
|
+
}
|
|
888
|
+
} catch (error) {
|
|
889
|
+
// DOM position is the source of truth here; command state is best effort.
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const rangeParams = this._getRangeParams();
|
|
893
|
+
if (rangeParams) {
|
|
894
|
+
this.setOldRangeParams(rangeParams);
|
|
895
|
+
this.applyCurrentEditableDomToComponent(rangeParams);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
debugMathRichInput("moveTrailingWhitespaceOutsideInlineStyle:after", {
|
|
899
|
+
style,
|
|
900
|
+
command,
|
|
901
|
+
movedText,
|
|
902
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
903
|
+
rangeParams,
|
|
904
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
905
|
+
});
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
applyCurrentEditableDomToComponent(rangeParams = null) {
|
|
910
|
+
if (!this.editableDiv) return;
|
|
911
|
+
if (rangeParams === null || rangeParams === undefined) {
|
|
912
|
+
rangeParams = this._getRangeParams();
|
|
913
|
+
}
|
|
914
|
+
if (rangeParams === null || rangeParams === undefined) return;
|
|
915
|
+
|
|
916
|
+
let rawText = elementToMarkedRawText(
|
|
917
|
+
this.editableDiv,
|
|
918
|
+
null,
|
|
919
|
+
0,
|
|
920
|
+
this.enableHtml()
|
|
921
|
+
);
|
|
922
|
+
rawText = this.stripEmptyInlineFormattingTags(rawText);
|
|
923
|
+
|
|
924
|
+
const nodeCounts = this._countNodeTypes();
|
|
925
|
+
const hasMath = /<math>/i.test(rawText) || nodeCounts.math > 0;
|
|
926
|
+
const hasHtml = nodeCounts.html > 0 || /<[^>]+>/i.test(rawText);
|
|
927
|
+
const mimeType = this.getMimeType(hasHtml, hasMath);
|
|
928
|
+
|
|
929
|
+
rawText = removeEncodingIfPlainText(
|
|
930
|
+
rawText,
|
|
931
|
+
mimeType,
|
|
932
|
+
this.enableHtml()
|
|
933
|
+
);
|
|
934
|
+
rawText = this.stripEmptyInlineFormattingTags(rawText);
|
|
935
|
+
|
|
936
|
+
this.applyChangesToComponent(
|
|
937
|
+
rawText,
|
|
938
|
+
mimeType,
|
|
939
|
+
rangeParams,
|
|
940
|
+
this.props.useExpertMode,
|
|
941
|
+
this.props.selectedTab,
|
|
942
|
+
{ skipControlledRender: true }
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
395
946
|
_getRangeParams(editableDiv = null) {
|
|
396
947
|
if (editableDiv === null) editableDiv = this.editableDiv;
|
|
397
948
|
let rangeParams = getRangeParams(editableDiv);
|
|
949
|
+
debugMathRichInput("MathRichInput:_getRangeParams", {
|
|
950
|
+
rangeParams,
|
|
951
|
+
selection: getSelectionSnapshot(editableDiv),
|
|
952
|
+
});
|
|
398
953
|
return rangeParams;
|
|
399
954
|
}
|
|
400
955
|
|
|
401
956
|
_setRangeParams = (params) => {
|
|
402
957
|
try {
|
|
958
|
+
debugMathRichInput("MathRichInput:_setRangeParams:start", {
|
|
959
|
+
params,
|
|
960
|
+
selectionBefore: getSelectionSnapshot(this.editableDiv),
|
|
961
|
+
});
|
|
403
962
|
setRangeParams(this.editableDiv, params);
|
|
404
963
|
this.setOldRangeParams(params);
|
|
405
|
-
this.
|
|
964
|
+
this.lastSetRangeParams = params;
|
|
965
|
+
debugMathRichInput("MathRichInput:_setRangeParams:after", {
|
|
966
|
+
params,
|
|
967
|
+
oldRangeParams: this.oldRangeParams,
|
|
968
|
+
selectionAfter: getSelectionSnapshot(this.editableDiv),
|
|
969
|
+
});
|
|
406
970
|
} catch (error) {
|
|
407
971
|
console.error(error);
|
|
408
972
|
// console.log(this.editableDiv.childNodes);
|
|
409
973
|
}
|
|
410
974
|
};
|
|
411
975
|
|
|
976
|
+
restoreRangeAfterBrowserWork = (params, reason) => {
|
|
977
|
+
if (params === null || params === undefined) return;
|
|
978
|
+
const restore = (delay) => {
|
|
979
|
+
window.setTimeout(() => {
|
|
980
|
+
if (this.state.showEquationEditor) return;
|
|
981
|
+
if (!this.state.hasFocus && document.activeElement !== this.editableDiv) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
debugMathRichInput("restoreRangeAfterBrowserWork", {
|
|
985
|
+
reason,
|
|
986
|
+
delay,
|
|
987
|
+
params,
|
|
988
|
+
selectionBefore: getSelectionSnapshot(this.editableDiv),
|
|
989
|
+
});
|
|
990
|
+
this._setRangeParams(params);
|
|
991
|
+
}, delay);
|
|
992
|
+
};
|
|
993
|
+
restore(0);
|
|
994
|
+
restore(50);
|
|
995
|
+
};
|
|
996
|
+
|
|
412
997
|
_findNodeWithIndex(index) {
|
|
413
998
|
return findNodeWithIndex(this.editableDiv, index);
|
|
414
999
|
}
|
|
@@ -596,7 +1181,11 @@ export default class MathRichInput extends React.Component {
|
|
|
596
1181
|
// Check if word navigation modifier is pressed
|
|
597
1182
|
isWordModifier = (e) => {
|
|
598
1183
|
const { isMac } = this.getPlatformModifiers();
|
|
599
|
-
|
|
1184
|
+
// Borough production is pinned to math-rich-input 0.1.48, where Option +
|
|
1185
|
+
// Arrow did not get custom word navigation. Keep Mac Option + Arrow as
|
|
1186
|
+
// normal character movement to preserve the editor's existing UX; Command +
|
|
1187
|
+
// Arrow is handled by isLineModifier below.
|
|
1188
|
+
return isMac ? false : e.ctrlKey;
|
|
600
1189
|
};
|
|
601
1190
|
|
|
602
1191
|
// Check if line navigation modifier is pressed
|
|
@@ -1160,6 +1749,17 @@ export default class MathRichInput extends React.Component {
|
|
|
1160
1749
|
|
|
1161
1750
|
handleKeyDown = (e) => {
|
|
1162
1751
|
try {
|
|
1752
|
+
debugMathRichInput("handleKeyDown:start", {
|
|
1753
|
+
key: e.key,
|
|
1754
|
+
altKey: e.altKey,
|
|
1755
|
+
metaKey: e.metaKey,
|
|
1756
|
+
ctrlKey: e.ctrlKey,
|
|
1757
|
+
shiftKey: e.shiftKey,
|
|
1758
|
+
hasFocus: this.state.hasFocus,
|
|
1759
|
+
propsValue: this.props.value,
|
|
1760
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
1761
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
1762
|
+
});
|
|
1163
1763
|
// Lets set up or update the undo redo history
|
|
1164
1764
|
this.initialiseHistory();
|
|
1165
1765
|
|
|
@@ -1246,7 +1846,8 @@ export default class MathRichInput extends React.Component {
|
|
|
1246
1846
|
}
|
|
1247
1847
|
}
|
|
1248
1848
|
|
|
1249
|
-
|
|
1849
|
+
const { isMac } = this.getPlatformModifiers();
|
|
1850
|
+
if (!isMac && e.altKey && e.key === "Alt") {
|
|
1250
1851
|
let key = this.fetchAccentLetter();
|
|
1251
1852
|
if (
|
|
1252
1853
|
key === "a" ||
|
|
@@ -1262,7 +1863,9 @@ export default class MathRichInput extends React.Component {
|
|
|
1262
1863
|
key === "!"
|
|
1263
1864
|
) {
|
|
1264
1865
|
// console.log("showing/hiding accent bar");
|
|
1265
|
-
this.setState({
|
|
1866
|
+
this.setState({
|
|
1867
|
+
showAccentBar: !this.state.showAccentBar,
|
|
1868
|
+
});
|
|
1266
1869
|
} else {
|
|
1267
1870
|
// console.log("hiding accent bar");
|
|
1268
1871
|
this.setState({ showAccentBar: false });
|
|
@@ -1272,7 +1875,9 @@ export default class MathRichInput extends React.Component {
|
|
|
1272
1875
|
}
|
|
1273
1876
|
|
|
1274
1877
|
this.keyPressed = e.key; // store for use in methods
|
|
1275
|
-
this.
|
|
1878
|
+
if (this.state.showAccentBar) {
|
|
1879
|
+
this.setState({ keyPressed: e.key });
|
|
1880
|
+
}
|
|
1276
1881
|
|
|
1277
1882
|
// Hide the accent bar if a key is typed except when it is an automatic key repeat press
|
|
1278
1883
|
// If it is automatic key repeat, then keyPressStartTime will be greater than 0
|
|
@@ -1291,10 +1896,16 @@ export default class MathRichInput extends React.Component {
|
|
|
1291
1896
|
}
|
|
1292
1897
|
if (e.key === "ArrowLeft") {
|
|
1293
1898
|
this.handleKeyDownArrowLeft(e);
|
|
1899
|
+
debugMathRichInput("handleKeyDown:after-arrow-left", {
|
|
1900
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
1901
|
+
});
|
|
1294
1902
|
return;
|
|
1295
1903
|
}
|
|
1296
1904
|
if (e.key === "ArrowRight") {
|
|
1297
1905
|
this.handleKeyDownArrowRight(e);
|
|
1906
|
+
debugMathRichInput("handleKeyDown:after-arrow-right", {
|
|
1907
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
1908
|
+
});
|
|
1298
1909
|
return;
|
|
1299
1910
|
}
|
|
1300
1911
|
if (e.key === "ArrowUp") {
|
|
@@ -1361,6 +1972,10 @@ export default class MathRichInput extends React.Component {
|
|
|
1361
1972
|
|
|
1362
1973
|
// Stop editing if within rendered katex node, if somehow the cursor ends up there (it shouldn't)
|
|
1363
1974
|
let params = this._getRangeParams();
|
|
1975
|
+
// No range means no caret to protect — a field the browser has not placed a selection in yet. It
|
|
1976
|
+
// reads as null on the first keystroke after a click on a toolbar button, and dereferencing it threw
|
|
1977
|
+
// on every Cmd, every Cmd+A and every Cmd+V the teacher pressed.
|
|
1978
|
+
if (params === null || params === undefined) return;
|
|
1364
1979
|
let startNode = this._findNodeWithIndex(params.startNodeIndex);
|
|
1365
1980
|
let endNode = this._findNodeWithIndex(params.endNodeIndex);
|
|
1366
1981
|
if (!this.editableDivIsPlainText()) {
|
|
@@ -1403,13 +2018,26 @@ export default class MathRichInput extends React.Component {
|
|
|
1403
2018
|
|
|
1404
2019
|
handleKeyUp = (e) => {
|
|
1405
2020
|
try {
|
|
2021
|
+
debugMathRichInput("handleKeyUp:start", {
|
|
2022
|
+
key: e.key,
|
|
2023
|
+
altKey: e.altKey,
|
|
2024
|
+
metaKey: e.metaKey,
|
|
2025
|
+
ctrlKey: e.ctrlKey,
|
|
2026
|
+
shiftKey: e.shiftKey,
|
|
2027
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2028
|
+
});
|
|
1406
2029
|
this.keyPressed = e.key;
|
|
1407
2030
|
this.keyPressStartTime = -1;
|
|
1408
2031
|
|
|
1409
2032
|
// If the cursor is in a katex node, then move it to the next node (unless there is no
|
|
1410
2033
|
// net node in which case move it to the previous node)
|
|
1411
2034
|
if (e.key === "ArrowRight") {
|
|
2035
|
+
// `_getRangeParams` returns null whenever the document's selection is not inside this field —
|
|
2036
|
+
// which happens in ordinary use, not only in error: a toolbar button taking focus is enough. It
|
|
2037
|
+
// was dereferenced straight away, so the key handler THREW and everything after it in the same
|
|
2038
|
+
// press was skipped.
|
|
1412
2039
|
let rangeParams = this._getRangeParams();
|
|
2040
|
+
if (!rangeParams) return;
|
|
1413
2041
|
let index = rangeParams.startNodeIndex;
|
|
1414
2042
|
|
|
1415
2043
|
if (index >= 0) {
|
|
@@ -1444,7 +2072,9 @@ export default class MathRichInput extends React.Component {
|
|
|
1444
2072
|
}
|
|
1445
2073
|
|
|
1446
2074
|
if (e.key === "ArrowLeft") {
|
|
2075
|
+
// Same guard, same reason — see ArrowRight above.
|
|
1447
2076
|
let rangeParams = this._getRangeParams();
|
|
2077
|
+
if (!rangeParams) return;
|
|
1448
2078
|
let index = rangeParams.startNodeIndex;
|
|
1449
2079
|
|
|
1450
2080
|
// If the cursor is in a katex node, then move it to the previous node (unless there is no
|
|
@@ -1491,6 +2121,17 @@ export default class MathRichInput extends React.Component {
|
|
|
1491
2121
|
// console.log("handleInput")
|
|
1492
2122
|
if (this.isComposing) return;
|
|
1493
2123
|
|
|
2124
|
+
debugMathRichInput("handleInput:start", {
|
|
2125
|
+
inputType: event && event.nativeEvent ? event.nativeEvent.inputType : null,
|
|
2126
|
+
data: event && event.nativeEvent ? event.nativeEvent.data : null,
|
|
2127
|
+
propsValue: this.props.value,
|
|
2128
|
+
propsMimeType: this.props.mimeType,
|
|
2129
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2130
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
this.cleanupEmptyInlineFormattingElements();
|
|
2134
|
+
|
|
1494
2135
|
let raw_text = elementToMarkedRawText(
|
|
1495
2136
|
this.editableDiv,
|
|
1496
2137
|
null,
|
|
@@ -1498,6 +2139,11 @@ export default class MathRichInput extends React.Component {
|
|
|
1498
2139
|
this.enableHtml()
|
|
1499
2140
|
);
|
|
1500
2141
|
const params = this._getRangeParams();
|
|
2142
|
+
debugMathRichInput("handleInput:after-range", {
|
|
2143
|
+
raw_text,
|
|
2144
|
+
params,
|
|
2145
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2146
|
+
});
|
|
1501
2147
|
if (params === null || params === undefined) {
|
|
1502
2148
|
// console.log(
|
|
1503
2149
|
// "handleInput: Empty content detected, using default range params"
|
|
@@ -1528,12 +2174,14 @@ export default class MathRichInput extends React.Component {
|
|
|
1528
2174
|
mimeType,
|
|
1529
2175
|
this.enableHtml()
|
|
1530
2176
|
);
|
|
2177
|
+
raw_text = this.stripEmptyInlineFormattingTags(raw_text);
|
|
1531
2178
|
this.applyChangesToComponent(
|
|
1532
2179
|
raw_text,
|
|
1533
2180
|
mimeType,
|
|
1534
2181
|
defaultParams,
|
|
1535
2182
|
this.props.useExpertMode,
|
|
1536
|
-
this.props.selectedTab
|
|
2183
|
+
this.props.selectedTab,
|
|
2184
|
+
{ skipControlledRender: true }
|
|
1537
2185
|
);
|
|
1538
2186
|
return;
|
|
1539
2187
|
} else if (
|
|
@@ -1564,23 +2212,33 @@ export default class MathRichInput extends React.Component {
|
|
|
1564
2212
|
this.enableHtml()
|
|
1565
2213
|
);
|
|
1566
2214
|
|
|
1567
|
-
|
|
1568
|
-
const validParams = params || {
|
|
1569
|
-
startNodeIndex: 0,
|
|
1570
|
-
startOffset: 0,
|
|
1571
|
-
endNodeIndex: 0,
|
|
1572
|
-
endOffset: 0,
|
|
1573
|
-
startGlobalOffset: 0,
|
|
1574
|
-
endGlobalOffset: 0,
|
|
1575
|
-
};
|
|
2215
|
+
raw_text = this.stripEmptyInlineFormattingTags(raw_text);
|
|
1576
2216
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2217
|
+
// Use valid params or fallback to defaults
|
|
2218
|
+
const validParams = params || {
|
|
2219
|
+
startNodeIndex: 0,
|
|
2220
|
+
startOffset: 0,
|
|
2221
|
+
endNodeIndex: 0,
|
|
2222
|
+
endOffset: 0,
|
|
2223
|
+
startGlobalOffset: 0,
|
|
2224
|
+
endGlobalOffset: 0,
|
|
2225
|
+
};
|
|
2226
|
+
this.setOldRangeParams(validParams);
|
|
2227
|
+
|
|
2228
|
+
this.applyChangesToComponent(
|
|
2229
|
+
raw_text,
|
|
2230
|
+
mimeType,
|
|
1580
2231
|
validParams,
|
|
1581
2232
|
this.props.useExpertMode,
|
|
1582
|
-
this.props.selectedTab
|
|
2233
|
+
this.props.selectedTab,
|
|
2234
|
+
{ skipControlledRender: true }
|
|
1583
2235
|
);
|
|
2236
|
+
debugMathRichInput("handleInput:after-apply", {
|
|
2237
|
+
raw_text,
|
|
2238
|
+
mimeType,
|
|
2239
|
+
validParams,
|
|
2240
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2241
|
+
});
|
|
1584
2242
|
} catch (error) {
|
|
1585
2243
|
console.error(error);
|
|
1586
2244
|
}
|
|
@@ -1622,46 +2280,8 @@ export default class MathRichInput extends React.Component {
|
|
|
1622
2280
|
this.setState({ activeButtons: activeButtons });
|
|
1623
2281
|
return;
|
|
1624
2282
|
}
|
|
1625
|
-
let
|
|
1626
|
-
|
|
1627
|
-
let italic = false;
|
|
1628
|
-
let underline = false;
|
|
1629
|
-
let superscript = false;
|
|
1630
|
-
let subscript = false;
|
|
1631
|
-
do {
|
|
1632
|
-
if (node.nodeType === 1) {
|
|
1633
|
-
let nodeName = node.nodeName.toLowerCase();
|
|
1634
|
-
if (nodeName === "b" || nodeName === "strong") bold = true;
|
|
1635
|
-
else if (nodeName === "i" || nodeName === "em") italic = true;
|
|
1636
|
-
else if (nodeName === "u") underline = true;
|
|
1637
|
-
else if (nodeName === "sub") subscript = true;
|
|
1638
|
-
else if (nodeName === "sup") superscript = true;
|
|
1639
|
-
} else if (
|
|
1640
|
-
node.classList !== null &&
|
|
1641
|
-
node.classList !== undefined &&
|
|
1642
|
-
node.classList.contains("MathRichInput")
|
|
1643
|
-
)
|
|
1644
|
-
break;
|
|
1645
|
-
node = node.parentNode;
|
|
1646
|
-
} while (node !== null);
|
|
1647
|
-
|
|
1648
|
-
let activeButtons = { bold, italic, underline, subscript, superscript };
|
|
1649
|
-
// console.log(activeButtons)
|
|
1650
|
-
|
|
1651
|
-
let update = true;
|
|
1652
|
-
if (this.lastSetActiveButtons !== null) {
|
|
1653
|
-
update = false;
|
|
1654
|
-
for (var style in activeButtons) {
|
|
1655
|
-
if (activeButtons[style] !== this.lastSetActiveButtons[style]) {
|
|
1656
|
-
update = true;
|
|
1657
|
-
break;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
if (update) {
|
|
1662
|
-
this.lastSetActiveButtons = activeButtons;
|
|
1663
|
-
this.setState({ activeButtons: activeButtons });
|
|
1664
|
-
}
|
|
2283
|
+
let activeButtons = this.getActiveButtonsFromSelection(rangeParams);
|
|
2284
|
+
this.setActiveButtonsIfChanged(activeButtons);
|
|
1665
2285
|
} catch (error) {
|
|
1666
2286
|
console.error(error);
|
|
1667
2287
|
}
|
|
@@ -1718,8 +2338,30 @@ export default class MathRichInput extends React.Component {
|
|
|
1718
2338
|
let pasteHtml = clipboardData.getData("text/html");
|
|
1719
2339
|
let pasteText = clipboardData.getData("text/plain");
|
|
1720
2340
|
|
|
1721
|
-
//
|
|
1722
|
-
|
|
2341
|
+
// Remove whatever was selected FIRST, then read the caret.
|
|
2342
|
+
//
|
|
2343
|
+
// The order used to be the other way round, and that is why pasting over a selection emptied the
|
|
2344
|
+
// field instead of replacing it. `rangeParams` is a NODE INDEX plus an offset, and
|
|
2345
|
+
// `deleteFromDocument()` removes the very nodes it counts: read before the delete, the index points
|
|
2346
|
+
// at something that no longer exists, so `_findNodeWithIndex` below finds nothing, the marked text
|
|
2347
|
+
// it builds carries no mark, and the pasted text is inserted nowhere. The value the component then
|
|
2348
|
+
// reports is correct while the editable div is left empty — which is exactly what a teacher sees.
|
|
2349
|
+
//
|
|
2350
|
+
// It only ever worked on an empty field because the canonical empty state is a single `<p>` holding
|
|
2351
|
+
// one small space: deleting that selection leaves the same node structure standing, so the stale
|
|
2352
|
+
// index still happens to resolve.
|
|
2353
|
+
// Read the caret BEFORE, so there is something to fall back on, and AGAIN after the delete.
|
|
2354
|
+
const paramsBeforeDelete = this._getRangeParams();
|
|
2355
|
+
|
|
2356
|
+
const selection = window.getSelection();
|
|
2357
|
+
if (selection && selection.rangeCount > 0) {
|
|
2358
|
+
selection.deleteFromDocument();
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// The caret AFTER the delete is the one that counts: `rangeParams` is a node index, and the delete
|
|
2362
|
+
// removed the very nodes it counts. The pre-delete value is kept only as a fallback — bailing out
|
|
2363
|
+
// here having already emptied the selection is how a paste turns into a deletion.
|
|
2364
|
+
const rangeParams = this._getRangeParams() || paramsBeforeDelete;
|
|
1723
2365
|
if (!rangeParams) {
|
|
1724
2366
|
console.warn("Could not get range params for paste");
|
|
1725
2367
|
return;
|
|
@@ -1728,12 +2370,6 @@ export default class MathRichInput extends React.Component {
|
|
|
1728
2370
|
// Store old range params for cursor positioning (like equation editor)
|
|
1729
2371
|
this.setOldRangeParams(rangeParams);
|
|
1730
2372
|
|
|
1731
|
-
// Clear any existing selection
|
|
1732
|
-
const selection = window.getSelection();
|
|
1733
|
-
if (selection.rangeCount > 0) {
|
|
1734
|
-
selection.deleteFromDocument();
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
2373
|
let finalText = "";
|
|
1738
2374
|
let finalMimeType = this.props.mimeType || "text/html";
|
|
1739
2375
|
let hasMathContent = false;
|
|
@@ -1855,7 +2491,6 @@ export default class MathRichInput extends React.Component {
|
|
|
1855
2491
|
let newRawText = removeMarks(
|
|
1856
2492
|
insertCharacterBeforeMarks(markedText, finalText)
|
|
1857
2493
|
);
|
|
1858
|
-
|
|
1859
2494
|
// Convert any remaining \[...\] LaTeX expressions to <math>...</math> format
|
|
1860
2495
|
// This ensures consistency when paste adds <math> tags alongside existing \[...\] expressions
|
|
1861
2496
|
if (this.enableMath()) {
|
|
@@ -1865,35 +2500,46 @@ export default class MathRichInput extends React.Component {
|
|
|
1865
2500
|
);
|
|
1866
2501
|
}
|
|
1867
2502
|
|
|
1868
|
-
|
|
2503
|
+
/*
|
|
2504
|
+
* Where the caret lands after a paste: at the END of what was pasted.
|
|
2505
|
+
*
|
|
2506
|
+
* The node index walks to just past the last formula, and that part was always right. The OFFSET
|
|
2507
|
+
* within that node was not: it stopped at the boundary, so pasting "…multiply it by 3." left the
|
|
2508
|
+
* caret between the 3 and the full stop. Everything after the last `</math>` is plain text, and
|
|
2509
|
+
* plain text is the one thing whose rendered length is its written length — so the tail can
|
|
2510
|
+
* simply be stepped over. (Markup in the tail is not that, and keeps the old position rather than
|
|
2511
|
+
* a guessed one.)
|
|
2512
|
+
*
|
|
2513
|
+
* Deriving the position from scratch was tried twice and both attempts landed FURTHER away, each
|
|
2514
|
+
* needing an exact model of how the rendered document is measured and each getting a corner of it
|
|
2515
|
+
* wrong. Walking the structure that is already there needs no such model.
|
|
2516
|
+
*/
|
|
1869
2517
|
let newRangeParams = null;
|
|
1870
2518
|
|
|
1871
2519
|
if (hasMathContent) {
|
|
1872
|
-
// Use same approach as equation editor for math content
|
|
1873
2520
|
const oldRangeParams = this.getOldRangeParams();
|
|
1874
2521
|
const mathTagCount = (finalText.match(/<math>/gi) || []).length;
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2522
|
+
const startNodeIndex =
|
|
2523
|
+
this.props.value === ""
|
|
2524
|
+
? mathTagCount * 2
|
|
2525
|
+
: oldRangeParams.startNodeIndex + mathTagCount * 2;
|
|
2526
|
+
const lastMathEnd = finalText.toLowerCase().lastIndexOf("</math>");
|
|
2527
|
+
const tail =
|
|
2528
|
+
lastMathEnd === -1
|
|
2529
|
+
? ""
|
|
2530
|
+
: finalText.substring(lastMathEnd + "</math>".length);
|
|
2531
|
+
const startOffset =
|
|
2532
|
+
tail.indexOf("<") === -1
|
|
2533
|
+
? SMALL_SPACE_LENGTH + tail.length
|
|
2534
|
+
: SMALL_SPACE_LENGTH;
|
|
2535
|
+
newRangeParams = {
|
|
2536
|
+
startNodeIndex,
|
|
2537
|
+
startOffset,
|
|
2538
|
+
endNodeIndex: startNodeIndex,
|
|
2539
|
+
endOffset: startOffset,
|
|
2540
|
+
};
|
|
1893
2541
|
} else {
|
|
1894
|
-
|
|
1895
|
-
const newGlobalOffset =
|
|
1896
|
-
rangeParams.startGlobalOffset + finalText.length;
|
|
2542
|
+
const newGlobalOffset = rangeParams.startGlobalOffset + finalText.length;
|
|
1897
2543
|
newRangeParams = {
|
|
1898
2544
|
startGlobalOffset: newGlobalOffset,
|
|
1899
2545
|
endGlobalOffset: newGlobalOffset,
|
|
@@ -1928,6 +2574,23 @@ export default class MathRichInput extends React.Component {
|
|
|
1928
2574
|
this.props.useExpertMode,
|
|
1929
2575
|
this.props.selectedTab
|
|
1930
2576
|
);
|
|
2577
|
+
|
|
2578
|
+
// A paste cannot rely on being re-rendered. It removed the selected content from the DOM itself,
|
|
2579
|
+
// and if the host already holds the value being applied — the same words pasted back into the
|
|
2580
|
+
// field they came from — no prop changes, nothing re-renders, and the field is left empty while
|
|
2581
|
+
// the value is correct everywhere else. So: give a real render its chance, then put the content
|
|
2582
|
+
// back if none came.
|
|
2583
|
+
const rangeAfterPaste = newRangeParams;
|
|
2584
|
+
queueMicrotask(() => {
|
|
2585
|
+
try {
|
|
2586
|
+
if (this.reconcileEditableDomWithRender(true)) {
|
|
2587
|
+
this._setRangeParams(rangeAfterPaste);
|
|
2588
|
+
this.updateActiveButtons();
|
|
2589
|
+
}
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
console.error(error);
|
|
2592
|
+
}
|
|
2593
|
+
});
|
|
1931
2594
|
}
|
|
1932
2595
|
} catch (error) {
|
|
1933
2596
|
console.error("Error in paste handler:", error);
|
|
@@ -1939,6 +2602,14 @@ export default class MathRichInput extends React.Component {
|
|
|
1939
2602
|
componentDidMount() {
|
|
1940
2603
|
try {
|
|
1941
2604
|
setupKatex();
|
|
2605
|
+
debugMathRichInput("componentDidMount", {
|
|
2606
|
+
propsValue: this.props.value,
|
|
2607
|
+
propsMimeType: this.props.mimeType,
|
|
2608
|
+
autofocus: this.props.autofocus,
|
|
2609
|
+
autoFocus: this.props.autoFocus,
|
|
2610
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2611
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2612
|
+
});
|
|
1942
2613
|
|
|
1943
2614
|
this.editableDiv.addEventListener("keydown", this.handleKeyDown);
|
|
1944
2615
|
this.editableDiv.addEventListener("keyup", this.handleKeyUp);
|
|
@@ -1961,16 +2632,143 @@ export default class MathRichInput extends React.Component {
|
|
|
1961
2632
|
}
|
|
1962
2633
|
}
|
|
1963
2634
|
|
|
2635
|
+
shouldComponentUpdate(nextProps, nextState) {
|
|
2636
|
+
if (this.skipNextControlledValueRender === true) {
|
|
2637
|
+
const canSkip =
|
|
2638
|
+
nextProps.value === this.skipNextControlledValue &&
|
|
2639
|
+
nextState === this.state;
|
|
2640
|
+
|
|
2641
|
+
debugMathRichInput("shouldComponentUpdate:native-input-gate", {
|
|
2642
|
+
canSkip,
|
|
2643
|
+
nextValue: nextProps.value,
|
|
2644
|
+
skipNextControlledValue: this.skipNextControlledValue,
|
|
2645
|
+
currentPropsValue: this.props.value,
|
|
2646
|
+
stateChanged: nextState !== this.state,
|
|
2647
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2648
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
this.skipNextControlledValueRender = false;
|
|
2652
|
+
this.skipNextControlledValue = null;
|
|
2653
|
+
|
|
2654
|
+
if (canSkip) return false;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
if (this.shouldSkipRedundantFocusedRender(nextProps, nextState)) {
|
|
2658
|
+
debugMathRichInput("shouldComponentUpdate:skip-redundant-focused-render", {
|
|
2659
|
+
nextValue: nextProps.value,
|
|
2660
|
+
currentPropsValue: this.props.value,
|
|
2661
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2662
|
+
});
|
|
2663
|
+
return false;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
return true;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
/**
|
|
2670
|
+
* The browser's own reading of an HTML string.
|
|
2671
|
+
*
|
|
2672
|
+
* React renders `<p>​…</P>`; the DOM reports `<p>…</p>` — same content, different bytes. So a
|
|
2673
|
+
* comparison against `innerHTML` has to be made in the DOM's spelling, or it never matches and the
|
|
2674
|
+
* reconciliation below would rewrite the field on every single update.
|
|
2675
|
+
*/
|
|
2676
|
+
normaliseHtml(html) {
|
|
2677
|
+
const probe = document.createElement("span");
|
|
2678
|
+
probe.innerHTML = html;
|
|
2679
|
+
return probe.innerHTML;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
* Put back what React believes it rendered, when the live DOM has drifted away from it.
|
|
2684
|
+
*
|
|
2685
|
+
* WHY THIS IS NEEDED AT ALL. `dangerouslySetInnerHTML` writes only when the html STRING changes: React
|
|
2686
|
+
* compares the new `__html` with the previous one and, finding them equal, leaves the DOM alone. That is
|
|
2687
|
+
* sound as long as React is the only thing that touches the DOM — and here it is not. `handlePaste`
|
|
2688
|
+
* removes the selected content itself, through `selection.deleteFromDocument()`, so the field empties
|
|
2689
|
+
* behind React's back while React's record of it still says "full".
|
|
2690
|
+
*
|
|
2691
|
+
* Paste the same words back into the field they came from and the two mistakes meet: the DOM is empty,
|
|
2692
|
+
* the html React computes is identical to the html it rendered last time, so it writes nothing — and
|
|
2693
|
+
* the field stays blank while `value`, the command, and the row in the database are all correct. The
|
|
2694
|
+
* teacher sees their text vanish on paste; nothing anywhere reports an error.
|
|
2695
|
+
*
|
|
2696
|
+
* It writes back the SAME string React itself passed to `dangerouslySetInnerHTML` on this render, so
|
|
2697
|
+
* nothing reaches the DOM here that React was not already putting there — the trust boundary is the
|
|
2698
|
+
* one that existed before, not a new one.
|
|
2699
|
+
*
|
|
2700
|
+
* Only reconciled after a PROGRAMMATIC edit (`afterComponentUpdateData` is set), or when the paste
|
|
2701
|
+
* asks directly (`force`). Ordinary typing goes down the native-input path, where the browser has
|
|
2702
|
+
* already put the character in the right place and the caret with it, and rewriting the DOM there
|
|
2703
|
+
* would move the caret to the front on every keystroke.
|
|
2704
|
+
*
|
|
2705
|
+
* `force` exists because a re-render is not guaranteed to happen AT ALL. When the host already holds
|
|
2706
|
+
* the value being applied — paste the same words back into the field they came from — the prop never
|
|
2707
|
+
* changes, so nothing re-renders and `componentDidUpdate` never runs. `lastRenderedEditableHtml` is
|
|
2708
|
+
* then exactly right: it is the html for that unchanged value, which is what the DOM should hold.
|
|
2709
|
+
*
|
|
2710
|
+
* Returns whether it wrote, so the caller knows whether the caret needs putting back.
|
|
2711
|
+
*/
|
|
2712
|
+
reconcileEditableDomWithRender(force = false) {
|
|
2713
|
+
if (!this.editableDiv) return false;
|
|
2714
|
+
if (!force && (this.afterComponentUpdateData === null || this.afterComponentUpdateData === undefined))
|
|
2715
|
+
return false;
|
|
2716
|
+
const html = force
|
|
2717
|
+
? rawTextToHtml(katex, this.props.value || "", this.props.mimeType)
|
|
2718
|
+
: this.lastRenderedEditableHtml;
|
|
2719
|
+
if (html === null || html === undefined) return false;
|
|
2720
|
+
if (this.editableDiv.innerHTML === this.normaliseHtml(html)) return false;
|
|
2721
|
+
debugMathRichInput("reconcile", { force, liveInnerHTML: this.editableDiv.innerHTML, html });
|
|
2722
|
+
this.editableDiv.innerHTML = html;
|
|
2723
|
+
this.lastRenderedEditableHtml = html;
|
|
2724
|
+
return true;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
1964
2727
|
componentDidUpdate() {
|
|
1965
2728
|
try {
|
|
2729
|
+
debugMathRichInput("componentDidUpdate:start", {
|
|
2730
|
+
propsValue: this.props.value,
|
|
2731
|
+
propsMimeType: this.props.mimeType,
|
|
2732
|
+
afterComponentUpdateData: this.afterComponentUpdateData,
|
|
2733
|
+
hasFocus: this.state.hasFocus,
|
|
2734
|
+
showEquationEditor: this.state.showEquationEditor,
|
|
2735
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2736
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2737
|
+
});
|
|
2738
|
+
// Before the caret is restored, not after: the offsets below are counted through the text, so
|
|
2739
|
+
// restoring them against an empty field puts the caret at 0 and loses the position as well.
|
|
2740
|
+
this.reconcileEditableDomWithRender();
|
|
1966
2741
|
if (
|
|
1967
2742
|
this.afterComponentUpdateData !== null &&
|
|
1968
2743
|
this.afterComponentUpdateData !== undefined &&
|
|
1969
2744
|
this.afterComponentUpdateData.value === this.props.value
|
|
1970
2745
|
) {
|
|
2746
|
+
debugMathRichInput("componentDidUpdate:restore-before", {
|
|
2747
|
+
rangeParams: this.afterComponentUpdateData.rangeParams,
|
|
2748
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2749
|
+
});
|
|
1971
2750
|
this._setRangeParams(this.afterComponentUpdateData.rangeParams);
|
|
1972
2751
|
this.afterComponentUpdateData = null;
|
|
1973
2752
|
this.updateActiveButtons();
|
|
2753
|
+
debugMathRichInput("componentDidUpdate:restore-after", {
|
|
2754
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
if (
|
|
2758
|
+
this.preserveNativeDomRangeParams !== null &&
|
|
2759
|
+
this.preserveNativeDomRangeParams !== undefined &&
|
|
2760
|
+
this.state.hasFocus === true
|
|
2761
|
+
) {
|
|
2762
|
+
const rangeParams = this.preserveNativeDomRangeParams;
|
|
2763
|
+
this.preserveNativeDomRangeParams = null;
|
|
2764
|
+
debugMathRichInput("componentDidUpdate:preserve-restore-before", {
|
|
2765
|
+
rangeParams,
|
|
2766
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2767
|
+
});
|
|
2768
|
+
this._setRangeParams(rangeParams);
|
|
2769
|
+
debugMathRichInput("componentDidUpdate:preserve-restore-after", {
|
|
2770
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2771
|
+
});
|
|
1974
2772
|
}
|
|
1975
2773
|
} catch (error) {
|
|
1976
2774
|
console.error(error);
|
|
@@ -1980,6 +2778,14 @@ export default class MathRichInput extends React.Component {
|
|
|
1980
2778
|
showEquationEditor = (moveCursorOnInsert = false, node = null) => {
|
|
1981
2779
|
try {
|
|
1982
2780
|
let rangeParams = this._getRangeParams();
|
|
2781
|
+
debugMathRichInput("showEquationEditor:start", {
|
|
2782
|
+
moveCursorOnInsert,
|
|
2783
|
+
nodeProvided: Boolean(node),
|
|
2784
|
+
rangeParams,
|
|
2785
|
+
oldRangeParams: this.oldRangeParams,
|
|
2786
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2787
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2788
|
+
});
|
|
1983
2789
|
if (rangeParams.startNodeIndex < 0) {
|
|
1984
2790
|
console.error("Could not find position of cursor in node list");
|
|
1985
2791
|
|
|
@@ -2030,6 +2836,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2030
2836
|
this.editingNode = null;
|
|
2031
2837
|
}
|
|
2032
2838
|
|
|
2839
|
+
this.setOldRangeParams(rangeParams);
|
|
2033
2840
|
this.markedRawText = elementToMarkedRawText(
|
|
2034
2841
|
this.editableDiv,
|
|
2035
2842
|
node,
|
|
@@ -2042,12 +2849,22 @@ export default class MathRichInput extends React.Component {
|
|
|
2042
2849
|
showEquationEditor: true,
|
|
2043
2850
|
equationEditorLatex: latex,
|
|
2044
2851
|
});
|
|
2852
|
+
debugMathRichInput("showEquationEditor:after-setState", {
|
|
2853
|
+
rangeParams,
|
|
2854
|
+
markedRawText: this.markedRawText,
|
|
2855
|
+
latex,
|
|
2856
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2857
|
+
});
|
|
2045
2858
|
} catch (error) {
|
|
2046
2859
|
console.error(error);
|
|
2047
2860
|
}
|
|
2048
2861
|
};
|
|
2049
2862
|
|
|
2050
2863
|
handleMouseDown = (event) => {
|
|
2864
|
+
this.isMouseDownInEditable = true;
|
|
2865
|
+
window.setTimeout(() => {
|
|
2866
|
+
this.isMouseDownInEditable = false;
|
|
2867
|
+
}, 0);
|
|
2051
2868
|
this.showEquationEditorOnMouseUp = false;
|
|
2052
2869
|
let node = null;
|
|
2053
2870
|
try {
|
|
@@ -2086,7 +2903,11 @@ export default class MathRichInput extends React.Component {
|
|
|
2086
2903
|
// Clear selection anchor on mouse click
|
|
2087
2904
|
this.selectionAnchor = null;
|
|
2088
2905
|
|
|
2906
|
+
// A click that lands with the selection outside this field — a toolbar button, another field —
|
|
2907
|
+
// gives no range at all. Dereferenced unguarded, this threw on every such click.
|
|
2089
2908
|
let params = this._getRangeParams();
|
|
2909
|
+
if (!params) return;
|
|
2910
|
+
this.setOldRangeParams(params);
|
|
2090
2911
|
let startNode = this._findNodeWithIndex(params.startNodeIndex);
|
|
2091
2912
|
let endNode = this._findNodeWithIndex(params.endNodeIndex);
|
|
2092
2913
|
|
|
@@ -2153,6 +2974,15 @@ export default class MathRichInput extends React.Component {
|
|
|
2153
2974
|
handleEquationEditorInsert = (latex) => {
|
|
2154
2975
|
try {
|
|
2155
2976
|
// console.log("Inserting latex: "+latex)
|
|
2977
|
+
debugMathRichInput("handleEquationEditorInsert:start", {
|
|
2978
|
+
latex,
|
|
2979
|
+
markedRawText: this.markedRawText,
|
|
2980
|
+
oldRangeParams: this.getOldRangeParams(),
|
|
2981
|
+
moveCursorOnInsert: this.moveCursorOnInsert,
|
|
2982
|
+
propsValue: this.props.value,
|
|
2983
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2984
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
2985
|
+
});
|
|
2156
2986
|
|
|
2157
2987
|
let start_pos = this.markedRawText.indexOf(MARK);
|
|
2158
2988
|
let end_pos = this.markedRawText.indexOf(MARK, start_pos + MARK.length);
|
|
@@ -2205,15 +3035,19 @@ export default class MathRichInput extends React.Component {
|
|
|
2205
3035
|
this.enableHtml()
|
|
2206
3036
|
);
|
|
2207
3037
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
3038
|
+
this.applyChangesToComponent(
|
|
3039
|
+
new_raw_text,
|
|
3040
|
+
mimeType,
|
|
3041
|
+
new_rangeParams,
|
|
3042
|
+
this.props.useExpertMode,
|
|
3043
|
+
this.props.selectedTab
|
|
3044
|
+
);
|
|
3045
|
+
this.setState({ showEquationEditor: false }, () => {
|
|
3046
|
+
this.editableDiv.focus();
|
|
3047
|
+
this._setRangeParams(new_rangeParams);
|
|
3048
|
+
this.restoreRangeAfterBrowserWork(new_rangeParams, "equation-empty");
|
|
3049
|
+
});
|
|
3050
|
+
return;
|
|
2217
3051
|
}
|
|
2218
3052
|
|
|
2219
3053
|
// Handle case of latex returned, in which case we update the katex node
|
|
@@ -2235,25 +3069,42 @@ export default class MathRichInput extends React.Component {
|
|
|
2235
3069
|
endOffset: SMALL_SPACE_LENGTH,
|
|
2236
3070
|
};
|
|
2237
3071
|
} else {
|
|
2238
|
-
//
|
|
2239
|
-
//
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
3072
|
+
// In this editor a rendered equation counts as one logical character
|
|
3073
|
+
// in global-offset space. Put the caret immediately after that math
|
|
3074
|
+
// character instead of guessing rendered child-node indexes.
|
|
3075
|
+
const oldRangeParams = this.getOldRangeParams();
|
|
3076
|
+
if (
|
|
3077
|
+
oldRangeParams !== null &&
|
|
3078
|
+
oldRangeParams.startGlobalOffset !== null &&
|
|
3079
|
+
oldRangeParams.startGlobalOffset !== undefined
|
|
3080
|
+
) {
|
|
3081
|
+
const afterInsertedMathOffset = oldRangeParams.startGlobalOffset + 1;
|
|
2247
3082
|
new_rangeParams = {
|
|
2248
|
-
startGlobalOffset:
|
|
2249
|
-
endGlobalOffset:
|
|
3083
|
+
startGlobalOffset: afterInsertedMathOffset,
|
|
3084
|
+
endGlobalOffset: afterInsertedMathOffset,
|
|
3085
|
+
};
|
|
3086
|
+
} else if (
|
|
3087
|
+
oldRangeParams !== null &&
|
|
3088
|
+
oldRangeParams.startNodeIndex !== null &&
|
|
3089
|
+
oldRangeParams.startNodeIndex !== undefined
|
|
3090
|
+
) {
|
|
3091
|
+
new_rangeParams = {
|
|
3092
|
+
startNodeIndex: oldRangeParams.startNodeIndex + 2,
|
|
3093
|
+
startOffset: SMALL_SPACE_LENGTH,
|
|
3094
|
+
endNodeIndex: oldRangeParams.startNodeIndex + 2,
|
|
3095
|
+
endOffset: SMALL_SPACE_LENGTH,
|
|
2250
3096
|
};
|
|
2251
3097
|
}
|
|
2252
|
-
// If
|
|
3098
|
+
// If old range params are unavailable, keep the current fallback.
|
|
2253
3099
|
}
|
|
2254
3100
|
}
|
|
2255
3101
|
// console.log("New cursor pos: "+new_rangeParams.startNodeIndex+"->"+new_rangeParams.startOffset)
|
|
2256
3102
|
this.setOldRangeParams(new_rangeParams);
|
|
3103
|
+
debugMathRichInput("handleEquationEditorInsert:before-apply", {
|
|
3104
|
+
new_raw_text,
|
|
3105
|
+
new_rangeParams,
|
|
3106
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3107
|
+
});
|
|
2257
3108
|
|
|
2258
3109
|
let nodeCounts = this._countNodeTypes();
|
|
2259
3110
|
let mimeType = this.getMimeType(nodeCounts.html > 0, true);
|
|
@@ -2270,7 +3121,15 @@ export default class MathRichInput extends React.Component {
|
|
|
2270
3121
|
this.props.useExpertMode,
|
|
2271
3122
|
this.props.selectedTab
|
|
2272
3123
|
);
|
|
2273
|
-
|
|
3124
|
+
this.setState({ showEquationEditor: false }, () => {
|
|
3125
|
+
this.editableDiv.focus();
|
|
3126
|
+
this._setRangeParams(new_rangeParams);
|
|
3127
|
+
this.restoreRangeAfterBrowserWork(new_rangeParams, "equation-insert");
|
|
3128
|
+
debugMathRichInput("handleEquationEditorInsert:after-close-restore", {
|
|
3129
|
+
new_rangeParams,
|
|
3130
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3131
|
+
});
|
|
3132
|
+
});
|
|
2274
3133
|
} catch (error) {
|
|
2275
3134
|
console.error(error);
|
|
2276
3135
|
}
|
|
@@ -2288,16 +3147,37 @@ export default class MathRichInput extends React.Component {
|
|
|
2288
3147
|
styleText = (event, style) => {
|
|
2289
3148
|
try {
|
|
2290
3149
|
style = style.toLowerCase();
|
|
3150
|
+
const command = this.getStyleCommand(style);
|
|
3151
|
+
const activeBefore = this.getActiveButtonsFromSelection();
|
|
3152
|
+
const selection = document.getSelection();
|
|
3153
|
+
const isCollapsed =
|
|
3154
|
+
selection === null ||
|
|
3155
|
+
selection.rangeCount === 0 ||
|
|
3156
|
+
selection.getRangeAt(0).collapsed;
|
|
2291
3157
|
let success = false;
|
|
3158
|
+
let insertedTypingStylePlaceholder = false;
|
|
2292
3159
|
|
|
2293
3160
|
// This first part is a workaround whereby executing subscript or superscript
|
|
2294
3161
|
// within an existing subscript or superscript makes the text even smaller instead
|
|
2295
3162
|
// of toggling it off. Thus we manually toggle it off with some jigerry pokery!
|
|
2296
3163
|
if (
|
|
2297
|
-
|
|
2298
|
-
|
|
3164
|
+
isCollapsed &&
|
|
3165
|
+
activeBefore[style] &&
|
|
3166
|
+
this.moveTrailingWhitespaceOutsideInlineStyle(style, command)
|
|
3167
|
+
) {
|
|
3168
|
+
success = true;
|
|
3169
|
+
} else if (
|
|
3170
|
+
isCollapsed &&
|
|
3171
|
+
!activeBefore[style] &&
|
|
3172
|
+
selection &&
|
|
3173
|
+
selection.rangeCount > 0
|
|
3174
|
+
) {
|
|
3175
|
+
success = this.insertInlineStylePlaceholder(style);
|
|
3176
|
+
insertedTypingStylePlaceholder = success;
|
|
3177
|
+
} else if (
|
|
3178
|
+
(style === "subscript" && activeBefore.subscript) ||
|
|
3179
|
+
(style === "superscript" && activeBefore.superscript)
|
|
2299
3180
|
) {
|
|
2300
|
-
const selection = document.getSelection();
|
|
2301
3181
|
let range = selection.getRangeAt(0);
|
|
2302
3182
|
if (
|
|
2303
3183
|
range.endContainer === range.startContainer &&
|
|
@@ -2309,90 +3189,29 @@ export default class MathRichInput extends React.Component {
|
|
|
2309
3189
|
selection.addRange(range);
|
|
2310
3190
|
}
|
|
2311
3191
|
success = document.execCommand("removeFormat", false, null);
|
|
2312
|
-
if (
|
|
3192
|
+
if (activeBefore.bold)
|
|
2313
3193
|
success = document.execCommand("bold", false, null);
|
|
2314
|
-
if (
|
|
3194
|
+
if (activeBefore.italic)
|
|
2315
3195
|
success = document.execCommand("italic", false, null);
|
|
2316
|
-
if (
|
|
3196
|
+
if (activeBefore.underline)
|
|
2317
3197
|
success = document.execCommand("underline", false, null);
|
|
2318
3198
|
} else {
|
|
2319
|
-
const selection = document.getSelection();
|
|
2320
3199
|
if (selection && selection.rangeCount > 0) {
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
if (range.collapsed) {
|
|
2324
|
-
// For cursor position (no selection), use insertHTML with styled wrapper
|
|
2325
|
-
// This creates a formatted element that will maintain styling for future typing
|
|
2326
|
-
const isToggleOff = this.state.activeButtons[style];
|
|
2327
|
-
|
|
2328
|
-
if (!isToggleOff) {
|
|
2329
|
-
// Turning style ON - insert a styled wrapper element
|
|
2330
|
-
let wrapperTag;
|
|
2331
|
-
switch (style) {
|
|
2332
|
-
case "bold":
|
|
2333
|
-
wrapperTag = "strong";
|
|
2334
|
-
break;
|
|
2335
|
-
case "italic":
|
|
2336
|
-
wrapperTag = "em";
|
|
2337
|
-
break;
|
|
2338
|
-
case "underline":
|
|
2339
|
-
wrapperTag = "u";
|
|
2340
|
-
break;
|
|
2341
|
-
case "subscript":
|
|
2342
|
-
wrapperTag = "sub";
|
|
2343
|
-
break;
|
|
2344
|
-
case "superscript":
|
|
2345
|
-
wrapperTag = "sup";
|
|
2346
|
-
break;
|
|
2347
|
-
default:
|
|
2348
|
-
wrapperTag = "span";
|
|
2349
|
-
break;
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
// Insert an invisible character wrapped in the styled element
|
|
2353
|
-
// This provides a "landing zone" for future typing
|
|
2354
|
-
const styledElement = `<${wrapperTag}>\u200B</${wrapperTag}>`;
|
|
2355
|
-
success = document.execCommand(
|
|
2356
|
-
"insertHTML",
|
|
2357
|
-
false,
|
|
2358
|
-
styledElement
|
|
2359
|
-
);
|
|
2360
|
-
|
|
2361
|
-
// Move cursor inside the styled element
|
|
2362
|
-
setTimeout(() => {
|
|
2363
|
-
const newSelection = document.getSelection();
|
|
2364
|
-
if (newSelection && newSelection.rangeCount > 0) {
|
|
2365
|
-
const newRange = newSelection.getRangeAt(0);
|
|
2366
|
-
const styledNode = newRange.startContainer.parentElement;
|
|
2367
|
-
if (
|
|
2368
|
-
styledNode &&
|
|
2369
|
-
styledNode.tagName.toLowerCase() === wrapperTag
|
|
2370
|
-
) {
|
|
2371
|
-
newRange.setStart(styledNode, 0);
|
|
2372
|
-
newRange.setEnd(styledNode, 0);
|
|
2373
|
-
newSelection.removeAllRanges();
|
|
2374
|
-
newSelection.addRange(newRange);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
}, 0);
|
|
2378
|
-
} else {
|
|
2379
|
-
// Turning style OFF - just apply execCommand normally
|
|
2380
|
-
success = document.execCommand(style, false, null);
|
|
2381
|
-
}
|
|
2382
|
-
} else {
|
|
2383
|
-
// For text selection, use regular execCommand
|
|
2384
|
-
success = document.execCommand(style, false, null);
|
|
2385
|
-
}
|
|
3200
|
+
success = document.execCommand(command, false, null);
|
|
2386
3201
|
}
|
|
2387
3202
|
}
|
|
2388
3203
|
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
3204
|
+
const rangeParams = this._getRangeParams();
|
|
3205
|
+
const removedEmptyTags =
|
|
3206
|
+
!insertedTypingStylePlaceholder &&
|
|
3207
|
+
this.cleanupEmptyInlineFormattingElements();
|
|
3208
|
+
if (removedEmptyTags && rangeParams !== null && rangeParams !== undefined) {
|
|
3209
|
+
this._setRangeParams(rangeParams);
|
|
3210
|
+
this.applyCurrentEditableDomToComponent(rangeParams);
|
|
3211
|
+
}
|
|
3212
|
+
this.setActiveButtonsIfChanged(
|
|
3213
|
+
this.getActiveButtonsFromSelection(rangeParams)
|
|
3214
|
+
);
|
|
2396
3215
|
return success;
|
|
2397
3216
|
} catch (error) {
|
|
2398
3217
|
console.error(error);
|
|
@@ -2401,8 +3220,15 @@ export default class MathRichInput extends React.Component {
|
|
|
2401
3220
|
|
|
2402
3221
|
handleToolbarButtonClick = (event, buttonName) => {
|
|
2403
3222
|
try {
|
|
3223
|
+
debugMathRichInput("handleToolbarButtonClick:start", {
|
|
3224
|
+
buttonName,
|
|
3225
|
+
eventType: event ? event.type : null,
|
|
3226
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3227
|
+
oldRangeParams: this.oldRangeParams,
|
|
3228
|
+
});
|
|
2404
3229
|
if (buttonName === "Equation") {
|
|
2405
3230
|
this.showEquationEditor(true);
|
|
3231
|
+
return;
|
|
2406
3232
|
}
|
|
2407
3233
|
let style = buttonName;
|
|
2408
3234
|
let success = this.styleText(event, style);
|
|
@@ -2751,10 +3577,10 @@ export default class MathRichInput extends React.Component {
|
|
|
2751
3577
|
}
|
|
2752
3578
|
};
|
|
2753
3579
|
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
3580
|
+
insertAfterSmallSpace(new_char, rangeParams) {
|
|
3581
|
+
try {
|
|
3582
|
+
if (rangeParams === null || rangeParams === undefined)
|
|
3583
|
+
rangeParams = this._getRangeParams();
|
|
2758
3584
|
|
|
2759
3585
|
if (rangeParams.startOffset !== 0)
|
|
2760
3586
|
console.warn(
|
|
@@ -2763,29 +3589,41 @@ export default class MathRichInput extends React.Component {
|
|
|
2763
3589
|
" in insertAfterSmallSpace"
|
|
2764
3590
|
);
|
|
2765
3591
|
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
3592
|
+
let node = this._findNodeWithIndex(rangeParams.startNodeIndex);
|
|
3593
|
+
if (node === null || node === undefined || node.nodeValue === null) {
|
|
3594
|
+
console.warn("Could not find small-space text node");
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// The initial empty editor contains a hidden zero-width space. Handling
|
|
3599
|
+
// the first printable key by re-rendering through React replaces the
|
|
3600
|
+
// text node and drops Chrome's selection to the start under React 19.
|
|
3601
|
+
// Mutate the already-focused DOM node, then skip the matching controlled
|
|
3602
|
+
// render exactly like the normal contenteditable input path.
|
|
3603
|
+
node.nodeValue =
|
|
3604
|
+
node.nodeValue.substring(0, 1) +
|
|
3605
|
+
new_char +
|
|
3606
|
+
node.nodeValue.substring(1);
|
|
3607
|
+
|
|
3608
|
+
let new_rangeParams = {
|
|
3609
|
+
startNodeIndex: rangeParams.startNodeIndex,
|
|
3610
|
+
startOffset: 1 + new_char.length,
|
|
3611
|
+
endNodeIndex: rangeParams.startNodeIndex,
|
|
3612
|
+
endOffset: 1 + new_char.length,
|
|
3613
|
+
startGlobalOffset: rangeParams.startGlobalOffset + new_char.length,
|
|
3614
|
+
endGlobalOffset: rangeParams.endGlobalOffset + new_char.length,
|
|
3615
|
+
};
|
|
3616
|
+
this._setRangeParams(new_rangeParams);
|
|
3617
|
+
|
|
3618
|
+
let new_raw_text = elementToMarkedRawText(
|
|
3619
|
+
this.editableDiv,
|
|
3620
|
+
null,
|
|
3621
|
+
0,
|
|
3622
|
+
this.enableHtml()
|
|
3623
|
+
);
|
|
3624
|
+
|
|
3625
|
+
new_raw_text = removeEncodingIfPlainText(
|
|
3626
|
+
new_raw_text,
|
|
2789
3627
|
this.props.mimeType,
|
|
2790
3628
|
this.enableHtml()
|
|
2791
3629
|
);
|
|
@@ -2793,12 +3631,13 @@ export default class MathRichInput extends React.Component {
|
|
|
2793
3631
|
this.applyChangesToComponent(
|
|
2794
3632
|
new_raw_text,
|
|
2795
3633
|
this.props.mimeType,
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3634
|
+
new_rangeParams,
|
|
3635
|
+
this.props.useExpertMode,
|
|
3636
|
+
this.props.selectedTab,
|
|
3637
|
+
{ skipControlledRender: true }
|
|
3638
|
+
);
|
|
3639
|
+
} catch (error) {
|
|
3640
|
+
console.error(error);
|
|
2802
3641
|
}
|
|
2803
3642
|
}
|
|
2804
3643
|
|
|
@@ -2895,12 +3734,39 @@ export default class MathRichInput extends React.Component {
|
|
|
2895
3734
|
|
|
2896
3735
|
handleFocus = (event) => {
|
|
2897
3736
|
const { onFocus } = this.props;
|
|
3737
|
+
let currentRangeParams = null;
|
|
3738
|
+
try {
|
|
3739
|
+
currentRangeParams = this._getRangeParams();
|
|
3740
|
+
} catch (error) {
|
|
3741
|
+
currentRangeParams = null;
|
|
3742
|
+
}
|
|
3743
|
+
const savedRangeParams = this.getOldRangeParams();
|
|
3744
|
+
const shouldRestoreSavedRange =
|
|
3745
|
+
!this.isMouseDownInEditable &&
|
|
3746
|
+
this.props.value !== "" &&
|
|
3747
|
+
this.rangeIsAtStart(currentRangeParams) &&
|
|
3748
|
+
this.rangeHasNonZeroPosition(savedRangeParams);
|
|
3749
|
+
debugMathRichInput("handleFocus:start", {
|
|
3750
|
+
propsValue: this.props.value,
|
|
3751
|
+
oldRangeParams: this.oldRangeParams,
|
|
3752
|
+
currentRangeParams,
|
|
3753
|
+
shouldRestoreSavedRange,
|
|
3754
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
3755
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3756
|
+
});
|
|
2898
3757
|
if (onFocus != null) {
|
|
2899
3758
|
onFocus();
|
|
2900
3759
|
}
|
|
2901
3760
|
try {
|
|
2902
|
-
this.
|
|
2903
|
-
|
|
3761
|
+
this.setState({ hasFocus: true }, () => {
|
|
3762
|
+
if (shouldRestoreSavedRange) {
|
|
3763
|
+
this._setRangeParams(savedRangeParams);
|
|
3764
|
+
this.restoreRangeAfterBrowserWork(savedRangeParams, "focus-restore");
|
|
3765
|
+
}
|
|
3766
|
+
debugMathRichInput("handleFocus:after-setState", {
|
|
3767
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3768
|
+
});
|
|
3769
|
+
});
|
|
2904
3770
|
} catch (error) {
|
|
2905
3771
|
console.error(error);
|
|
2906
3772
|
}
|
|
@@ -2908,12 +3774,31 @@ export default class MathRichInput extends React.Component {
|
|
|
2908
3774
|
|
|
2909
3775
|
handleBlur = (event) => {
|
|
2910
3776
|
const { onBlur } = this.props;
|
|
3777
|
+
let currentRangeParams = null;
|
|
3778
|
+
try {
|
|
3779
|
+
currentRangeParams = this._getRangeParams();
|
|
3780
|
+
if (this.rangeHasNonZeroPosition(currentRangeParams)) {
|
|
3781
|
+
this.setOldRangeParams(currentRangeParams);
|
|
3782
|
+
}
|
|
3783
|
+
} catch (error) {
|
|
3784
|
+
currentRangeParams = null;
|
|
3785
|
+
}
|
|
3786
|
+
debugMathRichInput("handleBlur:start", {
|
|
3787
|
+
propsValue: this.props.value,
|
|
3788
|
+
oldRangeParams: this.oldRangeParams,
|
|
3789
|
+
currentRangeParams,
|
|
3790
|
+
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
3791
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3792
|
+
});
|
|
2911
3793
|
if (onBlur != null) {
|
|
2912
3794
|
onBlur();
|
|
2913
3795
|
}
|
|
2914
3796
|
try {
|
|
2915
3797
|
// console.log("Hiding accent bar 4");
|
|
2916
3798
|
this.setState({ hasFocus: false, showAccentBar: false });
|
|
3799
|
+
debugMathRichInput("handleBlur:after-setState", {
|
|
3800
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3801
|
+
});
|
|
2917
3802
|
} catch (error) {
|
|
2918
3803
|
console.error(error);
|
|
2919
3804
|
}
|
|
@@ -3039,6 +3924,23 @@ export default class MathRichInput extends React.Component {
|
|
|
3039
3924
|
this.props.value || "",
|
|
3040
3925
|
this.props.mimeType
|
|
3041
3926
|
);
|
|
3927
|
+
const editableHtml = this.getEditableHtmlForRender(html);
|
|
3928
|
+
debugMathRichInput("render", {
|
|
3929
|
+
propsValue: this.props.value,
|
|
3930
|
+
propsMimeType: this.props.mimeType,
|
|
3931
|
+
html,
|
|
3932
|
+
editableHtml,
|
|
3933
|
+
state: {
|
|
3934
|
+
hasFocus: this.state.hasFocus,
|
|
3935
|
+
showEquationEditor: this.state.showEquationEditor,
|
|
3936
|
+
showAccentBar: this.state.showAccentBar,
|
|
3937
|
+
keyPressed: this.state.keyPressed,
|
|
3938
|
+
},
|
|
3939
|
+
afterComponentUpdateData: this.afterComponentUpdateData,
|
|
3940
|
+
oldRangeParams: this.oldRangeParams,
|
|
3941
|
+
editableInnerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
3942
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3943
|
+
});
|
|
3042
3944
|
|
|
3043
3945
|
// Debug render math
|
|
3044
3946
|
// if (this.props.value && this.props.value.includes("<math>")) {
|
|
@@ -3103,7 +4005,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3103
4005
|
onCompositionStart={this.handleCompositionStart}
|
|
3104
4006
|
onCompositionEnd={this.handleCompositionEnd}
|
|
3105
4007
|
dangerouslySetInnerHTML={{
|
|
3106
|
-
__html:
|
|
4008
|
+
__html: editableHtml,
|
|
3107
4009
|
}}
|
|
3108
4010
|
data-placeholder={this.props.placeholder || ""}
|
|
3109
4011
|
></span>
|