@zzish/math-rich-input 0.1.57 → 0.1.58

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.
@@ -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
- debugMathRichInput("componentDidUpdate:preserve-restore-before", {
2960
- rangeParams,
2961
- selection: getSelectionSnapshot(this.editableDiv),
2962
- });
2963
- this._setRangeParams(rangeParams);
2964
- debugMathRichInput("componentDidUpdate:preserve-restore-after", {
2965
- selection: getSelectionSnapshot(this.editableDiv),
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
- if (rangeParams.startNodeIndex < 0) {
2985
- console.error("Could not find position of cursor in node list");
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 (node === null)
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
- this.rangeIsAtStart(currentRangeParams) &&
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 (this.rangeHasNonZeroPosition(currentRangeParams)) {
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) => e.preventDefault()}
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) => e.preventDefault()}
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) => e.preventDefault()}
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) => e.preventDefault()}
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) => e.preventDefault()}
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) => e.preventDefault()}
228
- onMouseUp={(e) => this.showOrHideAccentBar(e)}
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) => e.preventDefault()}
238
- onMouseDown={(e) => this.handleButtonClick(e, "Equation")}
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">
@@ -52,6 +52,8 @@ export function getMark() {
52
52
  }
53
53
 
54
54
  export function isKatexNode(node) {
55
+ // Callers probe siblings and parents that may not exist (the first node has no previousSibling).
56
+ if (node === null || node === undefined) return false;
55
57
  return node.className === "katex" || node.className === "katex-error";
56
58
  }
57
59
 
@@ -969,9 +971,11 @@ export function getRangeParams(editableDiv) {
969
971
 
970
972
  const selection = window.getSelection();
971
973
  if (selection.rangeCount === 0) {
972
- console.error(
973
- "Can't get range params, window.getSelection did not return any ranges"
974
- );
974
+ // No selection yet is ordinary, not an error. A mousedown reaches this before the browser has
975
+ // placed a caret, and a toolbar button takes the selection out of the field; every click used to
976
+ // print an error to the console for a state the callers handle. Return null, and say so only when
977
+ // debug logging is on.
978
+ debugMathRichInput("getRangeParams:no-selection");
975
979
  return null;
976
980
  }
977
981