@zzish/math-rich-input 0.1.57 → 0.1.59
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/README.md +22 -0
- package/dist/index.js +489 -112
- package/dist/math-rich-input.css +1 -1
- package/dist/standalone.js +1 -1
- package/package.json +7 -2
- package/scripts/build-katex-stylesheet.js +40 -0
- package/src/AccentBar.jsx +21 -21
- package/src/EquationEditor.jsx +44 -3
- package/src/EquationEditorModal.jsx +88 -0
- package/src/MathRichInput.css +3 -0
- package/src/MathRichInput.jsx +269 -29
- package/src/Toolbar.jsx +29 -15
- package/src/katexHelper.js +20 -38
- package/src/katexStylesheet.generated.js +3 -0
- package/src/mathRichInputHelper.js +7 -3
- package/src/standalone.js +48 -5
package/src/MathRichInput.jsx
CHANGED
|
@@ -238,6 +238,14 @@ export default class MathRichInput extends React.Component {
|
|
|
238
238
|
this.isMouseDownInEditable = false;
|
|
239
239
|
this.lastRenderedEditableHtml = null;
|
|
240
240
|
this.preserveNativeDomRangeParams = null;
|
|
241
|
+
// The `dangerouslySetInnerHTML` prop object, reused while the html is unchanged. React re-applies
|
|
242
|
+
// the markup on a commit when it is handed a fresh object, replacing the editable's nodes and the
|
|
243
|
+
// caret with them; the same object tells React there is nothing to write.
|
|
244
|
+
this.lastEditableHtmlProp = undefined;
|
|
245
|
+
this.lastEditableHtmlPropHtml = null;
|
|
246
|
+
// Which of the field's formulas the current mouse press started on. The node itself cannot be
|
|
247
|
+
// kept: focusing a field redraws it, so the element that was pressed is already gone at mouseup.
|
|
248
|
+
this.equationEditorMouseDownKatexIndex = -1;
|
|
241
249
|
|
|
242
250
|
this.lastSetActiveButtons = null;
|
|
243
251
|
|
|
@@ -645,6 +653,36 @@ export default class MathRichInput extends React.Component {
|
|
|
645
653
|
};
|
|
646
654
|
}
|
|
647
655
|
|
|
656
|
+
/** The caret's params, only when the selection is inside this field. */
|
|
657
|
+
selectionRangeParams() {
|
|
658
|
+
const selection = window.getSelection();
|
|
659
|
+
if (!selection || selection.rangeCount === 0) return null;
|
|
660
|
+
if (!this.editableDiv || !this.editableDiv.contains(selection.anchorNode)) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
return this.getRangeParamsPreservingInlinePlaceholder();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The `dangerouslySetInnerHTML` prop, REUSED while the html is unchanged.
|
|
668
|
+
*
|
|
669
|
+
* React re-applies `dangerouslySetInnerHTML` on a commit when it is handed a new prop object, even
|
|
670
|
+
* when the string in it is the one already in the DOM (measured with React 19: three writes of an
|
|
671
|
+
* identical-length string, each replacing the editable's children — and with them the caret, which
|
|
672
|
+
* then sat at the start of the field). The same object is what tells React there is nothing to
|
|
673
|
+
* write, and that is how the field keeps the browser's own DOM and selection.
|
|
674
|
+
*/
|
|
675
|
+
editableHtmlProp(html) {
|
|
676
|
+
if (
|
|
677
|
+
this.lastEditableHtmlProp === undefined ||
|
|
678
|
+
this.lastEditableHtmlPropHtml !== html
|
|
679
|
+
) {
|
|
680
|
+
this.lastEditableHtmlPropHtml = html;
|
|
681
|
+
this.lastEditableHtmlProp = { __html: html };
|
|
682
|
+
}
|
|
683
|
+
return this.lastEditableHtmlProp;
|
|
684
|
+
}
|
|
685
|
+
|
|
648
686
|
getEditableHtmlForRender(html) {
|
|
649
687
|
const currentRawText = this.editableDiv
|
|
650
688
|
? this.getCurrentRawTextForProps(this.props)
|
|
@@ -688,8 +726,14 @@ export default class MathRichInput extends React.Component {
|
|
|
688
726
|
return liveEditableHtml;
|
|
689
727
|
}
|
|
690
728
|
|
|
729
|
+
// React will write this string: it differs from the one it last rendered. Remember where the
|
|
730
|
+
// caret is so it can be put back if the write takes the selection with it. A click elsewhere
|
|
731
|
+
// blurs the field before the commit, so the restore below never overrides another control.
|
|
732
|
+
this.preserveNativeDomRangeParams =
|
|
733
|
+
this.state.hasFocus === true && html !== this.lastRenderedEditableHtml
|
|
734
|
+
? this.selectionRangeParams()
|
|
735
|
+
: null;
|
|
691
736
|
this.lastRenderedEditableHtml = html;
|
|
692
|
-
this.preserveNativeDomRangeParams = null;
|
|
693
737
|
return html;
|
|
694
738
|
}
|
|
695
739
|
|
|
@@ -2200,6 +2244,9 @@ export default class MathRichInput extends React.Component {
|
|
|
2200
2244
|
try {
|
|
2201
2245
|
// console.log("handleInput")
|
|
2202
2246
|
if (this.isComposing) return;
|
|
2247
|
+
// execCommand emits a synchronous input event; styleText records the
|
|
2248
|
+
// complete format transaction once after the command returns.
|
|
2249
|
+
if (this.formatCommandActive) return;
|
|
2203
2250
|
|
|
2204
2251
|
const transaction = this.pendingTransaction;
|
|
2205
2252
|
this.pendingTransaction = null;
|
|
@@ -2956,14 +3003,23 @@ export default class MathRichInput extends React.Component {
|
|
|
2956
3003
|
) {
|
|
2957
3004
|
const rangeParams = this.preserveNativeDomRangeParams;
|
|
2958
3005
|
this.preserveNativeDomRangeParams = null;
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3006
|
+
// Only when the commit really did take the caret. A selection still in the field is the
|
|
3007
|
+
// user's and is not ours to move; a click elsewhere has blurred the field, which is why the
|
|
3008
|
+
// focus check above owns that case.
|
|
3009
|
+
const liveRangeParams = this._getRangeParams();
|
|
3010
|
+
if (
|
|
3011
|
+
liveRangeParams === null ||
|
|
3012
|
+
this.rangeIsAtStart(liveRangeParams)
|
|
3013
|
+
) {
|
|
3014
|
+
debugMathRichInput("componentDidUpdate:preserve-restore-before", {
|
|
3015
|
+
rangeParams,
|
|
3016
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3017
|
+
});
|
|
3018
|
+
this._setRangeParams(rangeParams);
|
|
3019
|
+
debugMathRichInput("componentDidUpdate:preserve-restore-after", {
|
|
3020
|
+
selection: getSelectionSnapshot(this.editableDiv),
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
2967
3023
|
}
|
|
2968
3024
|
} catch (error) {
|
|
2969
3025
|
console.error(error);
|
|
@@ -2973,6 +3029,7 @@ export default class MathRichInput extends React.Component {
|
|
|
2973
3029
|
showEquationEditor = (moveCursorOnInsert = false, node = null) => {
|
|
2974
3030
|
try {
|
|
2975
3031
|
let rangeParams = this._getRangeParams();
|
|
3032
|
+
const hasNode = node !== null && node !== undefined;
|
|
2976
3033
|
debugMathRichInput("showEquationEditor:start", {
|
|
2977
3034
|
moveCursorOnInsert,
|
|
2978
3035
|
nodeProvided: Boolean(node),
|
|
@@ -2981,8 +3038,30 @@ export default class MathRichInput extends React.Component {
|
|
|
2981
3038
|
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
2982
3039
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
2983
3040
|
});
|
|
2984
|
-
|
|
2985
|
-
|
|
3041
|
+
// A node handed in by a click is enough to open the editor, even when the selection resolves to
|
|
3042
|
+
// nothing (-1) or is missing entirely: the node carries the latex and where it sits.
|
|
3043
|
+
if (
|
|
3044
|
+
!hasNode &&
|
|
3045
|
+
(rangeParams === null || rangeParams.startNodeIndex < 0) &&
|
|
3046
|
+
!this.isVisuallyEmpty()
|
|
3047
|
+
) {
|
|
3048
|
+
// No usable caret in a field that has content. The empty-field fallback below must never run
|
|
3049
|
+
// here: its marks stand for the WHOLE value, so Insert would replace the answer with the formula.
|
|
3050
|
+
// Put the caret at the end of the field and resolve it again; if that still fails, do not open.
|
|
3051
|
+
const endOfField = document.createRange();
|
|
3052
|
+
endOfField.selectNodeContents(this.editableDiv);
|
|
3053
|
+
endOfField.collapse(false);
|
|
3054
|
+
this.setCaretToRange(endOfField);
|
|
3055
|
+
rangeParams = this._getRangeParams();
|
|
3056
|
+
if (rangeParams === null || rangeParams.startNodeIndex < 0) {
|
|
3057
|
+
console.error("Could not find position of cursor in node list");
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
if (!hasNode && (rangeParams === null || rangeParams.startNodeIndex < 0)) {
|
|
3063
|
+
// An empty field: there is nothing to keep, so the equation becomes its whole value.
|
|
3064
|
+
debugMathRichInput("showEquationEditor:empty-field", { rangeParams });
|
|
2986
3065
|
|
|
2987
3066
|
this.setOldRangeParams({
|
|
2988
3067
|
startNodeIndex: 2,
|
|
@@ -2991,6 +3070,9 @@ export default class MathRichInput extends React.Component {
|
|
|
2991
3070
|
endOffset: 1,
|
|
2992
3071
|
});
|
|
2993
3072
|
this.markedRawText = MARK + " " + MARK;
|
|
3073
|
+
// A new equation: never the formula an earlier opening was editing.
|
|
3074
|
+
this.editingExistingEquation = false;
|
|
3075
|
+
this.editingNode = null;
|
|
2994
3076
|
this.moveCursorOnInsert = moveCursorOnInsert;
|
|
2995
3077
|
this.setState({
|
|
2996
3078
|
showEquationEditor: true,
|
|
@@ -3002,9 +3084,23 @@ export default class MathRichInput extends React.Component {
|
|
|
3002
3084
|
let latex = "";
|
|
3003
3085
|
let offset = 0;
|
|
3004
3086
|
|
|
3005
|
-
if (
|
|
3087
|
+
if (!hasNode)
|
|
3006
3088
|
node = this._findNodeWithIndex(rangeParams.startNodeIndex);
|
|
3007
3089
|
|
|
3090
|
+
// The toolbar preserves a caret AFTER the formula, in the renderer's zero-width text node.
|
|
3091
|
+
// Resolve that boundary to the preceding formula so f(x) edits it instead of opening empty.
|
|
3092
|
+
// Ordinary text or a non-collapsed selection still starts a new equation.
|
|
3093
|
+
if (
|
|
3094
|
+
!hasNode &&
|
|
3095
|
+
node?.nodeType === Node.TEXT_NODE &&
|
|
3096
|
+
rangeParams.startNodeIndex === rangeParams.endNodeIndex &&
|
|
3097
|
+
rangeParams.startOffset === rangeParams.endOffset &&
|
|
3098
|
+
node.textContent.slice(0, rangeParams.startOffset).replace(/\u200b/g, "") === "" &&
|
|
3099
|
+
isKatexNode(node.previousSibling)
|
|
3100
|
+
) {
|
|
3101
|
+
node = node.previousSibling;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3008
3104
|
// console.log(rangeParams)
|
|
3009
3105
|
// console.log(this.editableDiv.childNodes)
|
|
3010
3106
|
// console.log(node)
|
|
@@ -3026,7 +3122,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3026
3122
|
this.editingNode = node;
|
|
3027
3123
|
} else {
|
|
3028
3124
|
latex = "";
|
|
3029
|
-
offset = rangeParams.startOffset;
|
|
3125
|
+
offset = rangeParams ? rangeParams.startOffset : 0;
|
|
3030
3126
|
this.editingExistingEquation = false;
|
|
3031
3127
|
this.editingNode = null;
|
|
3032
3128
|
}
|
|
@@ -3061,6 +3157,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3061
3157
|
this.isMouseDownInEditable = false;
|
|
3062
3158
|
}, 0);
|
|
3063
3159
|
this.showEquationEditorOnMouseUp = false;
|
|
3160
|
+
this.equationEditorMouseDownKatexIndex = -1;
|
|
3064
3161
|
let node = null;
|
|
3065
3162
|
try {
|
|
3066
3163
|
node = event.target;
|
|
@@ -3078,6 +3175,12 @@ export default class MathRichInput extends React.Component {
|
|
|
3078
3175
|
}
|
|
3079
3176
|
|
|
3080
3177
|
this.showEquationEditorOnMouseUp = true;
|
|
3178
|
+
// The browser has no selection yet at mousedown and a click inside KaTeX may leave it inside the
|
|
3179
|
+
// rendered subtree or outside the field. The formula's POSITION is what survives until mouseup:
|
|
3180
|
+
// focusing the field redraws the editable and replaces every node with an identical one, so the
|
|
3181
|
+
// element pressed here is detached by the time the mouse is released.
|
|
3182
|
+
this.equationEditorMouseDownKatexIndex =
|
|
3183
|
+
this.katexNodesInField().indexOf(node);
|
|
3081
3184
|
// Save current range parameters so that we can restore them later if we wish
|
|
3082
3185
|
this.setOldRangeParams(this._getRangeParams());
|
|
3083
3186
|
|
|
@@ -3091,6 +3194,47 @@ export default class MathRichInput extends React.Component {
|
|
|
3091
3194
|
}
|
|
3092
3195
|
};
|
|
3093
3196
|
|
|
3197
|
+
/** The field's rendered formulas, in document order — the positions a click can land on. */
|
|
3198
|
+
katexNodesInField() {
|
|
3199
|
+
return [...this.editableDiv.querySelectorAll(".katex, .katex-error")];
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Put the browser's selection at `range` and remember the position it resolves to.
|
|
3204
|
+
*
|
|
3205
|
+
* Used to place the caret where the component — not the browser — knows it belongs: after a formula
|
|
3206
|
+
* that was clicked, and at the end of a field that was focused without a click.
|
|
3207
|
+
*/
|
|
3208
|
+
setCaretToRange(range) {
|
|
3209
|
+
try {
|
|
3210
|
+
const selection = window.getSelection();
|
|
3211
|
+
if (!selection) return;
|
|
3212
|
+
selection.removeAllRanges();
|
|
3213
|
+
selection.addRange(range);
|
|
3214
|
+
const params = this._getRangeParams();
|
|
3215
|
+
if (params) this.setOldRangeParams(params);
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
console.error(error);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
/**
|
|
3222
|
+
* A range that starts immediately after `node`, in the text that follows it.
|
|
3223
|
+
*
|
|
3224
|
+
* `setStartAfter` alone reads back as a position INSIDE the KaTeX subtree — the last text node of the
|
|
3225
|
+
* formula is the nearest text — and the text model treats that as the position BEFORE the maths. The
|
|
3226
|
+
* renderer always leaves a zero-width text node after a formula, so the caret starts there instead:
|
|
3227
|
+
* after the maths, where typing continues.
|
|
3228
|
+
*/
|
|
3229
|
+
caretRangeAfterNode(node) {
|
|
3230
|
+
const range = document.createRange();
|
|
3231
|
+
const next = node.nextSibling;
|
|
3232
|
+
if (next !== null && next !== undefined) range.setStart(next, 0);
|
|
3233
|
+
else range.setStartAfter(node);
|
|
3234
|
+
range.collapse(true);
|
|
3235
|
+
return range;
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3094
3238
|
handleMouseUp = (event) => {
|
|
3095
3239
|
try {
|
|
3096
3240
|
this.initialiseHistory();
|
|
@@ -3098,6 +3242,30 @@ export default class MathRichInput extends React.Component {
|
|
|
3098
3242
|
// Clear selection anchor on mouse click
|
|
3099
3243
|
this.selectionAnchor = null;
|
|
3100
3244
|
|
|
3245
|
+
const selection = window.getSelection();
|
|
3246
|
+
const collapsedClick =
|
|
3247
|
+
selection === null ||
|
|
3248
|
+
selection.rangeCount === 0 ||
|
|
3249
|
+
selection.isCollapsed;
|
|
3250
|
+
|
|
3251
|
+
// A click on a formula opens the equation editor for THAT formula. The selection is not used to
|
|
3252
|
+
// find it: clicking KaTeX leaves the selection inside the rendered subtree (which the text model
|
|
3253
|
+
// reads as the position before the maths) or outside the field, and the editor then opened empty
|
|
3254
|
+
// and the caret came back at the start. The caret is put after the formula, which is one logical
|
|
3255
|
+
// character, so typing and Insert continue to its right.
|
|
3256
|
+
if (collapsedClick && this.showEquationEditorOnMouseUp) {
|
|
3257
|
+
// Focus BEFORE the node is looked up: focusing redraws the field, and a node resolved from the
|
|
3258
|
+
// old DOM would be detached by the time the range is built.
|
|
3259
|
+
this.editableDiv.focus();
|
|
3260
|
+
const mathNode =
|
|
3261
|
+
this.katexNodesInField()[this.equationEditorMouseDownKatexIndex] || null;
|
|
3262
|
+
if (mathNode !== null) {
|
|
3263
|
+
this.setCaretToRange(this.caretRangeAfterNode(mathNode));
|
|
3264
|
+
this.showEquationEditor(false, mathNode);
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3101
3269
|
// A click that lands with the selection outside this field — a toolbar button, another field —
|
|
3102
3270
|
// gives no range at all. Dereferenced unguarded, this threw on every such click.
|
|
3103
3271
|
let params = this._getRangeParams();
|
|
@@ -3111,16 +3279,6 @@ export default class MathRichInput extends React.Component {
|
|
|
3111
3279
|
return;
|
|
3112
3280
|
}
|
|
3113
3281
|
|
|
3114
|
-
if (
|
|
3115
|
-
params.startNodeIndex === params.endNodeIndex &&
|
|
3116
|
-
params.startOffset === params.endOffset &&
|
|
3117
|
-
this.showEquationEditorOnMouseUp
|
|
3118
|
-
) {
|
|
3119
|
-
this.editableDiv.focus();
|
|
3120
|
-
this.showEquationEditor(false, startNode);
|
|
3121
|
-
return;
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
3282
|
// If our range starts or ends inside a katex node, move it out
|
|
3125
3283
|
let new_rangeParams = null;
|
|
3126
3284
|
if (isKatexNode(startNode)) {
|
|
@@ -3144,6 +3302,10 @@ export default class MathRichInput extends React.Component {
|
|
|
3144
3302
|
}
|
|
3145
3303
|
|
|
3146
3304
|
if (new_rangeParams !== null) {
|
|
3305
|
+
// These node positions replace a selection inside KaTeX. Its old global offsets still point
|
|
3306
|
+
// before the atomic formula, so they must not override the corrected positions after it.
|
|
3307
|
+
new_rangeParams.startGlobalOffset = null;
|
|
3308
|
+
new_rangeParams.endGlobalOffset = null;
|
|
3147
3309
|
this._setRangeParams(new_rangeParams);
|
|
3148
3310
|
event.preventDefault();
|
|
3149
3311
|
return;
|
|
@@ -3179,6 +3341,32 @@ export default class MathRichInput extends React.Component {
|
|
|
3179
3341
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
3180
3342
|
});
|
|
3181
3343
|
|
|
3344
|
+
// Insert with nothing to write: a formula that was opened and not edited, or a new equation left
|
|
3345
|
+
// empty. Writing back would send the host a change event for a value it already holds, and hosts
|
|
3346
|
+
// show "unsaved changes" on any change event. Close as Escape does and put the caret back.
|
|
3347
|
+
if (
|
|
3348
|
+
(this.editingExistingEquation &&
|
|
3349
|
+
latex === this.state.equationEditorLatex) ||
|
|
3350
|
+
(!this.editingExistingEquation && latex === "")
|
|
3351
|
+
) {
|
|
3352
|
+
const unchangedRangeParams = this.getOldRangeParams();
|
|
3353
|
+
this.setState({ showEquationEditor: false }, () => {
|
|
3354
|
+
try {
|
|
3355
|
+
this.editableDiv.focus();
|
|
3356
|
+
if (unchangedRangeParams !== null) {
|
|
3357
|
+
this._setRangeParams(unchangedRangeParams);
|
|
3358
|
+
this.restoreRangeAfterBrowserWork(
|
|
3359
|
+
unchangedRangeParams,
|
|
3360
|
+
"equation-unchanged"
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
console.error(error);
|
|
3365
|
+
}
|
|
3366
|
+
});
|
|
3367
|
+
return;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3182
3370
|
let start_pos = this.markedRawText.indexOf(MARK);
|
|
3183
3371
|
let end_pos = this.markedRawText.indexOf(MARK, start_pos + MARK.length);
|
|
3184
3372
|
let oldRangeParams = this.getOldRangeParams();
|
|
@@ -3341,6 +3529,9 @@ export default class MathRichInput extends React.Component {
|
|
|
3341
3529
|
|
|
3342
3530
|
styleText = (event, style) => {
|
|
3343
3531
|
try {
|
|
3532
|
+
this.initialiseHistory();
|
|
3533
|
+
this.resetHistoryRedo();
|
|
3534
|
+
this.formatCommandActive = true;
|
|
3344
3535
|
style = style.toLowerCase();
|
|
3345
3536
|
const command = this.getStyleCommand(style);
|
|
3346
3537
|
const activeBefore = this.getActiveButtonsFromSelection();
|
|
@@ -3351,6 +3542,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3351
3542
|
selection.getRangeAt(0).collapsed;
|
|
3352
3543
|
let success = false;
|
|
3353
3544
|
let insertedTypingStylePlaceholder = false;
|
|
3545
|
+
let publishedByWhitespaceMove = false;
|
|
3354
3546
|
|
|
3355
3547
|
// This first part is a workaround whereby executing subscript or superscript
|
|
3356
3548
|
// within an existing subscript or superscript makes the text even smaller instead
|
|
@@ -3361,6 +3553,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3361
3553
|
this.moveTrailingWhitespaceOutsideInlineStyle(style, command)
|
|
3362
3554
|
) {
|
|
3363
3555
|
success = true;
|
|
3556
|
+
publishedByWhitespaceMove = true;
|
|
3364
3557
|
} else if (
|
|
3365
3558
|
isCollapsed &&
|
|
3366
3559
|
!activeBefore[style] &&
|
|
@@ -3398,7 +3591,7 @@ export default class MathRichInput extends React.Component {
|
|
|
3398
3591
|
|
|
3399
3592
|
const rangeParams = this._getRangeParams();
|
|
3400
3593
|
if (!insertedTypingStylePlaceholder) this.cleanupEmptyInlineFormattingElements();
|
|
3401
|
-
if (rangeParams !== null && rangeParams !== undefined) {
|
|
3594
|
+
if (!publishedByWhitespaceMove && rangeParams !== null && rangeParams !== undefined) {
|
|
3402
3595
|
this._setRangeParams(rangeParams);
|
|
3403
3596
|
this.applyCurrentEditableDomToComponent(rangeParams, {
|
|
3404
3597
|
edit: {
|
|
@@ -3417,6 +3610,8 @@ export default class MathRichInput extends React.Component {
|
|
|
3417
3610
|
return success;
|
|
3418
3611
|
} catch (error) {
|
|
3419
3612
|
console.error(error);
|
|
3613
|
+
} finally {
|
|
3614
|
+
this.formatCommandActive = false;
|
|
3420
3615
|
}
|
|
3421
3616
|
};
|
|
3422
3617
|
|
|
@@ -4003,16 +4198,30 @@ export default class MathRichInput extends React.Component {
|
|
|
4003
4198
|
currentRangeParams = null;
|
|
4004
4199
|
}
|
|
4005
4200
|
const savedRangeParams = this.getOldRangeParams();
|
|
4201
|
+
// A missing selection at focus time is not "somewhere else": the browser has not placed a caret
|
|
4202
|
+
// yet, which reads as the START of the field. Treating null as "leave it alone" skipped both the
|
|
4203
|
+
// restore below and the move to the end, so the caret stayed where the browser had left it — 0.
|
|
4204
|
+
const currentIsAtStart =
|
|
4205
|
+
currentRangeParams === null || this.rangeIsAtStart(currentRangeParams);
|
|
4006
4206
|
const shouldRestoreSavedRange =
|
|
4007
4207
|
!this.isMouseDownInEditable &&
|
|
4008
4208
|
this.props.value !== "" &&
|
|
4009
|
-
|
|
4209
|
+
currentIsAtStart &&
|
|
4010
4210
|
this.rangeHasNonZeroPosition(savedRangeParams);
|
|
4211
|
+
// Focus without a click inside the field (a host focusing it, Tab, a click on the row around it)
|
|
4212
|
+
// carries no position of its own. Editing an existing answer means continuing at its end, so the
|
|
4213
|
+
// caret goes there; a click inside the field keeps the position the browser gave it.
|
|
4214
|
+
const shouldMoveToEnd =
|
|
4215
|
+
!this.isMouseDownInEditable &&
|
|
4216
|
+
this.props.value !== "" &&
|
|
4217
|
+
currentIsAtStart &&
|
|
4218
|
+
!this.rangeHasNonZeroPosition(savedRangeParams);
|
|
4011
4219
|
debugMathRichInput("handleFocus:start", {
|
|
4012
4220
|
propsValue: this.props.value,
|
|
4013
4221
|
oldRangeParams: this.oldRangeParams,
|
|
4014
4222
|
currentRangeParams,
|
|
4015
4223
|
shouldRestoreSavedRange,
|
|
4224
|
+
shouldMoveToEnd,
|
|
4016
4225
|
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
4017
4226
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
4018
4227
|
});
|
|
@@ -4024,6 +4233,25 @@ export default class MathRichInput extends React.Component {
|
|
|
4024
4233
|
if (shouldRestoreSavedRange) {
|
|
4025
4234
|
this._setRangeParams(savedRangeParams);
|
|
4026
4235
|
this.restoreRangeAfterBrowserWork(savedRangeParams, "focus-restore");
|
|
4236
|
+
} else if (shouldMoveToEnd) {
|
|
4237
|
+
// One task later, not here: the browser places the caret as part of the focus default
|
|
4238
|
+
// action, and for a keyboard focus (Tab) that happens AFTER this callback — moving now was
|
|
4239
|
+
// simply overwritten. A task boundary is after both the pointer and the keyboard default.
|
|
4240
|
+
// The guard keeps a selection placed meanwhile (a host restoring its own range) untouched.
|
|
4241
|
+
window.setTimeout(() => {
|
|
4242
|
+
if (this.state.hasFocus !== true) return;
|
|
4243
|
+
const liveRangeParams = this._getRangeParams();
|
|
4244
|
+
if (
|
|
4245
|
+
liveRangeParams !== null &&
|
|
4246
|
+
!this.rangeIsAtStart(liveRangeParams)
|
|
4247
|
+
) {
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
4250
|
+
const endOfField = document.createRange();
|
|
4251
|
+
endOfField.selectNodeContents(this.editableDiv);
|
|
4252
|
+
endOfField.collapse(false);
|
|
4253
|
+
this.setCaretToRange(endOfField);
|
|
4254
|
+
}, 0);
|
|
4027
4255
|
}
|
|
4028
4256
|
debugMathRichInput("handleFocus:after-setState", {
|
|
4029
4257
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
@@ -4039,7 +4267,7 @@ export default class MathRichInput extends React.Component {
|
|
|
4039
4267
|
let currentRangeParams = null;
|
|
4040
4268
|
try {
|
|
4041
4269
|
currentRangeParams = this._getRangeParams();
|
|
4042
|
-
if (
|
|
4270
|
+
if (currentRangeParams !== null) {
|
|
4043
4271
|
this.setOldRangeParams(currentRangeParams);
|
|
4044
4272
|
}
|
|
4045
4273
|
} catch (error) {
|
|
@@ -4052,6 +4280,10 @@ export default class MathRichInput extends React.Component {
|
|
|
4052
4280
|
innerHTML: this.editableDiv ? this.editableDiv.innerHTML : null,
|
|
4053
4281
|
selection: getSelectionSnapshot(this.editableDiv),
|
|
4054
4282
|
});
|
|
4283
|
+
// Keyboard focus on this editor's tools is still inside the editor. Keep both
|
|
4284
|
+
// the toolbar and the captured range until focus leaves the whole control.
|
|
4285
|
+
if (event.relatedTarget === this.editableDiv ||
|
|
4286
|
+
this.toolbar?.toolbar?.contains(event.relatedTarget)) return;
|
|
4055
4287
|
if (onBlur != null) {
|
|
4056
4288
|
onBlur();
|
|
4057
4289
|
}
|
|
@@ -4318,6 +4550,16 @@ export default class MathRichInput extends React.Component {
|
|
|
4318
4550
|
)}
|
|
4319
4551
|
{this.state.hasFocus && (
|
|
4320
4552
|
<Toolbar
|
|
4553
|
+
ref={(r) => (this.toolbar = r)}
|
|
4554
|
+
onBlur={this.handleBlur}
|
|
4555
|
+
beforeAction={() => {
|
|
4556
|
+
if (document.activeElement !== this.editableDiv) {
|
|
4557
|
+
const range = this.getOldRangeParams();
|
|
4558
|
+
this.editableDiv.focus({ preventScroll: true });
|
|
4559
|
+
if (range !== null) this._setRangeParams(range);
|
|
4560
|
+
}
|
|
4561
|
+
this.initialiseHistory();
|
|
4562
|
+
}}
|
|
4321
4563
|
className="Toolbar"
|
|
4322
4564
|
enableMath={this.enableMath()}
|
|
4323
4565
|
enableFontStyling={this.enableFontStyling()}
|
|
@@ -4352,9 +4594,7 @@ export default class MathRichInput extends React.Component {
|
|
|
4352
4594
|
onBlur={this.handleBlur}
|
|
4353
4595
|
onCompositionStart={this.handleCompositionStart}
|
|
4354
4596
|
onCompositionEnd={this.handleCompositionEnd}
|
|
4355
|
-
dangerouslySetInnerHTML={
|
|
4356
|
-
__html: editableHtml,
|
|
4357
|
-
}}
|
|
4597
|
+
dangerouslySetInnerHTML={this.editableHtmlProp(editableHtml)}
|
|
4358
4598
|
data-placeholder={this.props.placeholder || ""}
|
|
4359
4599
|
></span>
|
|
4360
4600
|
</div>
|
package/src/Toolbar.jsx
CHANGED
|
@@ -53,6 +53,7 @@ export default class Toolbar extends React.Component {
|
|
|
53
53
|
handleButtonClick = (event, buttonName) => {
|
|
54
54
|
event.preventDefault();
|
|
55
55
|
event.stopPropagation();
|
|
56
|
+
this.props.beforeAction?.();
|
|
56
57
|
this.suppliedClickHandler(event, buttonName);
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -147,70 +148,75 @@ export default class Toolbar extends React.Component {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
return (
|
|
150
|
-
<div className="Toolbar" style={style} ref={(r) => (this.toolbar = r)}>
|
|
151
|
+
<div className="Toolbar" style={style} ref={(r) => (this.toolbar = r)} onBlur={this.props.onBlur}>
|
|
151
152
|
{enableFontStyling && (
|
|
152
153
|
<>
|
|
153
154
|
<button
|
|
155
|
+
type="button"
|
|
154
156
|
name="Bold"
|
|
157
|
+
aria-label="Bold"
|
|
155
158
|
className={
|
|
156
159
|
"Toolbar-button TB-narrow" +
|
|
157
160
|
(this.props.activeButtons.bold ? " TB-selected" : "")
|
|
158
161
|
}
|
|
159
|
-
onClick={(e) =>
|
|
162
|
+
onClick={(e) => this.handleButtonClick(e, "Bold")}
|
|
160
163
|
onMouseDown={(e) => e.preventDefault()}
|
|
161
|
-
onMouseUp={(e) => this.handleButtonClick(e, "Bold")}
|
|
162
164
|
>
|
|
163
165
|
<b>B</b>
|
|
164
166
|
</button>
|
|
165
167
|
|
|
166
168
|
<button
|
|
169
|
+
type="button"
|
|
167
170
|
name="Italic"
|
|
171
|
+
aria-label="Italic"
|
|
168
172
|
className={
|
|
169
173
|
"Toolbar-button TB-narrow" +
|
|
170
174
|
(this.props.activeButtons.italic ? " TB-selected" : "")
|
|
171
175
|
}
|
|
172
|
-
onClick={(e) =>
|
|
176
|
+
onClick={(e) => this.handleButtonClick(e, "Italic")}
|
|
173
177
|
onMouseDown={(e) => e.preventDefault()}
|
|
174
|
-
onMouseUp={(e) => this.handleButtonClick(e, "Italic")}
|
|
175
178
|
>
|
|
176
179
|
<i>I</i>
|
|
177
180
|
</button>
|
|
178
181
|
|
|
179
182
|
<button
|
|
183
|
+
type="button"
|
|
180
184
|
name="Underline"
|
|
185
|
+
aria-label="Underline"
|
|
181
186
|
className={
|
|
182
187
|
"Toolbar-button TB-narrow" +
|
|
183
188
|
(this.props.activeButtons.underline ? " TB-selected" : "")
|
|
184
189
|
}
|
|
185
|
-
onClick={(e) =>
|
|
190
|
+
onClick={(e) => this.handleButtonClick(e, "Underline")}
|
|
186
191
|
onMouseDown={(e) => e.preventDefault()}
|
|
187
|
-
onMouseUp={(e) => this.handleButtonClick(e, "Underline")}
|
|
188
192
|
>
|
|
189
193
|
<u>U</u>
|
|
190
194
|
</button>
|
|
191
195
|
|
|
192
196
|
<button
|
|
197
|
+
type="button"
|
|
193
198
|
name="Superscript"
|
|
199
|
+
aria-label="Superscript"
|
|
194
200
|
className={
|
|
195
201
|
"Toolbar-button TB-narrow" +
|
|
196
202
|
(this.props.activeButtons.superscript ? " TB-selected" : "")
|
|
197
203
|
}
|
|
198
|
-
onClick={(e) =>
|
|
204
|
+
onClick={(e) => this.handleButtonClick(e, "Superscript")}
|
|
199
205
|
onMouseDown={(e) => e.preventDefault()}
|
|
200
|
-
onMouseUp={(e) => this.handleButtonClick(e, "Superscript")}
|
|
201
206
|
>
|
|
202
207
|
a<sup>x</sup>
|
|
203
208
|
</button>
|
|
204
209
|
|
|
205
210
|
<button
|
|
211
|
+
type="button"
|
|
206
212
|
name="Subscript"
|
|
213
|
+
aria-label="Subscript"
|
|
207
214
|
className={
|
|
208
215
|
"Toolbar-button TB-narrow" +
|
|
209
216
|
(this.props.activeButtons.subscript ? " TB-selected" : "")
|
|
210
217
|
}
|
|
211
|
-
onClick={(e) =>
|
|
218
|
+
onClick={(e) => this.handleButtonClick(e, "Subscript")}
|
|
212
219
|
onMouseDown={(e) => e.preventDefault()}
|
|
213
|
-
onMouseUp={(e) => this.handleButtonClick(e, "Subscript")}
|
|
214
220
|
>
|
|
215
221
|
a<sub>x</sub>
|
|
216
222
|
</button>
|
|
@@ -218,24 +224,32 @@ export default class Toolbar extends React.Component {
|
|
|
218
224
|
)}
|
|
219
225
|
|
|
220
226
|
<button
|
|
227
|
+
type="button"
|
|
221
228
|
name="Accents"
|
|
229
|
+
aria-label="Accents"
|
|
230
|
+
aria-expanded={this.state.showAccents}
|
|
222
231
|
className={
|
|
223
232
|
"Toolbar-button TB-narrow" +
|
|
224
233
|
(this.state.showAccents ? " TB-selected" : "")
|
|
225
234
|
}
|
|
226
235
|
onMouseDown={(e) => e.preventDefault()}
|
|
227
|
-
onClick={(e) =>
|
|
228
|
-
|
|
236
|
+
onClick={(e) => {
|
|
237
|
+
e.preventDefault(); e.stopPropagation();
|
|
238
|
+
this.props.beforeAction?.();
|
|
239
|
+
this.showOrHideAccentBar(e);
|
|
240
|
+
}}
|
|
229
241
|
>
|
|
230
242
|
á
|
|
231
243
|
</button>
|
|
232
244
|
|
|
233
245
|
{enableMath && (
|
|
234
246
|
<button
|
|
247
|
+
type="button"
|
|
235
248
|
name="Equation"
|
|
249
|
+
aria-label="Insert equation"
|
|
236
250
|
className="Toolbar-button TB-wide"
|
|
237
|
-
onClick={(e) =>
|
|
238
|
-
onMouseDown={(e) =>
|
|
251
|
+
onClick={(e) => this.handleButtonClick(e, "Equation")}
|
|
252
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
239
253
|
>
|
|
240
254
|
<div dangerouslySetInnerHTML={{ __html: katexHTML }} />
|
|
241
255
|
<span className="tooltiptext">
|