Skip to content

Commit 35b7ac9

Browse files
committed
Fix: render MathJax in doc blocks when they're being edited.
1 parent f0e4137 commit 35b7ac9

File tree

2 files changed

+74
-83
lines changed

2 files changed

+74
-83
lines changed

client/src/CodeChatEditor.mts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,7 @@ const save_lp = (is_dirty: boolean) => {
330330
mathJaxUnTypeset(codechat_body);
331331
// To save a document only, simply get the HTML from the only Tiny
332332
// MCE div.
333-
tinymce.activeEditor!.save();
334-
const html = tinymce.activeEditor!.getContent();
333+
const html = tinymce.activeEditor!.save();
335334
(
336335
code_mirror_diffable as {
337336
Plain: CodeMirror;

client/src/CodeMirror-integration.mts

Lines changed: 73 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ let tinymce_singleton: Editor | undefined;
104104
// When true, don't update on the next call to `on_dirty`. See that function for
105105
// more info.
106106
let ignore_next_dirty = false;
107+
// This indicates that a call to `on_dirty` is scheduled, but hasn't run yet.
108+
let on_dirty_scheduled = false;
107109
// True to ignore the next text selection change, since updates to the cursor or
108110
// scroll position from the Client trigged this change.
109111
let ignore_selection_change = false;
@@ -436,6 +438,7 @@ class DocBlockWidget extends WidgetType {
436438
`<div class="CodeChat-doc-contents" spellcheck contenteditable>` +
437439
this.contents +
438440
"</div>";
441+
// TODO: this is an async call. However, CodeMirror doesn't provide async support.
439442
mathJaxTypeset(wrap);
440443
return wrap;
441444
}
@@ -457,16 +460,17 @@ class DocBlockWidget extends WidgetType {
457460
// The contents div could be a TinyMCE instance, or just a plain div.
458461
// Handle both cases.
459462
const [contents_div, is_tinymce] = get_contents(dom);
463+
window.MathJax.typesetClear(contents_div);
460464
if (is_tinymce) {
461465
ignore_next_dirty = true;
462466
tinymce_singleton!.setContent(this.contents);
463467
tinymce_singleton!.save();
464468
} else {
465469
contents_div.innerHTML = this.contents;
466-
mathJaxTypeset(contents_div);
467470
}
471+
mathJaxTypeset(contents_div);
468472

469-
// Indicate the update was successful.
473+
// Indicate the update was successful. TODO: but, contents are still pending...
470474
return true;
471475
}
472476

@@ -501,17 +505,9 @@ class DocBlockWidget extends WidgetType {
501505
export const mathJaxTypeset = async (
502506
// The node to typeset.
503507
node: HTMLElement,
504-
// An optional function to run when the typeset finishes.
505-
afterTypesetFunc: () => void = () => {},
506508
) => {
507-
// Don't await this promise -- other MathJax processing may still be
508-
// running. See the
509-
// [release notes](https://github.com/mathjax/MathJax-src/releases/tag/4.0.0-rc.4#api).
510-
window.MathJax.typesetPromise([node]);
511509
try {
512-
// Instead, this function calls `afterTypesetFunc` after it awaits all
513-
// internal MathJax promises.
514-
window.MathJax.whenReady(afterTypesetFunc);
510+
await window.MathJax.typesetPromise([node]);
515511
} catch (err: any) {
516512
report_error(`Typeset failed: ${err.message}`);
517513
}
@@ -576,40 +572,57 @@ const on_dirty = (
576572
ignore_next_dirty = false;
577573
return;
578574
}
579-
// Find the doc block parent div.
580-
const target = (event_target as HTMLDivElement).closest(
581-
".CodeChat-doc",
582-
)! as HTMLDivElement;
583575

584-
// We can only get the position (the `from` value) for the doc block. Use
585-
// this to find the `to` value for the doc block.
586-
const from = current_view.posAtDOM(target);
587-
// Send an update to the state field associated with this DOM element.
588-
const indent_div = target.childNodes[0] as HTMLDivElement;
589-
const indent = indent_div.innerHTML;
590-
const delimiter = indent_div.getAttribute("data-delimiter")!;
591-
const [contents_div, is_tinymce] = get_contents(target);
592-
// Sorta ugly hack: TinyMCE stores its date in the DOM. CodeMirror stores
593-
// state in external structs. We need to update the CodeMirror state, but
594-
// not overwrite the DOM with this "new" state, since the DOM is already
595-
// updated. So, signal that this "update" is already done.
596-
(target as any).update_complete = true;
597-
tinymce_singleton!.save();
598-
const contents = is_tinymce
599-
? tinymce_singleton!.getContent()
600-
: contents_div.innerHTML;
601-
let effects: StateEffect<updateDocBlockType>[] = [
602-
updateDocBlock.of({
603-
from,
604-
indent,
605-
delimiter,
606-
contents,
607-
}),
608-
];
576+
if (on_dirty_scheduled) {
577+
return;
578+
}
609579

610-
current_view.dispatch({ effects });
580+
// Only run this after typesetting is done.
581+
window.MathJax.whenReady(() => {
582+
// Find the doc block parent div.
583+
const target = (event_target as HTMLDivElement).closest(
584+
".CodeChat-doc",
585+
)! as HTMLDivElement;
611586

612-
return false;
587+
// We can only get the position (the `from` value) for the doc block. Use
588+
// this to find the `to` value for the doc block.
589+
let from;
590+
try {
591+
from = current_view.posAtDOM(target);
592+
} catch (e) {
593+
console.error("Unable to get position from DOM.", target);
594+
return;
595+
}
596+
// Send an update to the state field associated with this DOM element.
597+
const indent_div = target.childNodes[0] as HTMLDivElement;
598+
const indent = indent_div.innerHTML;
599+
const delimiter = indent_div.getAttribute("data-delimiter")!;
600+
const [contents_div, is_tinymce] = get_contents(target);
601+
// I'd like to extract this string, then untypeset only that string, not the actual div. But I don't know how.
602+
mathJaxUnTypeset(contents_div);
603+
const contents = is_tinymce
604+
? tinymce_singleton!.save()
605+
: contents_div.innerHTML;
606+
// Although this is async, nothing following this call depends on its completion.
607+
mathJaxTypeset(contents_div);
608+
// Sorta ugly hack: TinyMCE stores its data in the DOM. CodeMirror stores
609+
// state in external structs. We need to update the CodeMirror state, but
610+
// not overwrite the DOM with this "new" state, since the DOM is already
611+
// updated. So, signal that this "update" is already done.
612+
(target as any).update_complete = true;
613+
let effects: StateEffect<updateDocBlockType>[] = [
614+
updateDocBlock.of({
615+
from,
616+
indent,
617+
delimiter,
618+
contents,
619+
}),
620+
];
621+
622+
current_view.dispatch({ effects });
623+
624+
on_dirty_scheduled = false;
625+
});
613626
};
614627

615628
export const DocBlockPlugin = ViewPlugin.fromClass(
@@ -714,17 +727,17 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
714727
// See if this is already a TinyMCE instance; if not, move it
715728
// here.
716729
if (is_tinymce) {
717-
ignore_next_dirty = true;
718-
mathJaxUnTypeset(contents_div);
719-
// If there was no math to untypeset, then `on_dirty` wasn't
720-
// called, but we should no longer ignore the next dirty
721-
// flag.
722-
ignore_next_dirty = false;
730+
// Nothing to do.
723731
} else {
724732
// Wait until the focus event completes; this causes the
725733
// cursor position (the selection) to be set in the
726734
// contenteditable div. Then, save that location.
727735
setTimeout(() => {
736+
// Untypeset math in the old doc block and the current doc block before moving its contents around.
737+
const tinymce_div =
738+
document.getElementById("TinyMCE-inst")!;
739+
mathJaxUnTypeset(tinymce_div);
740+
mathJaxUnTypeset(contents_div);
728741
// The code which moves TinyMCE into this div disturbs
729742
// all the nodes, which causes it to loose a selection
730743
// tied to a specific node. So, instead store the
@@ -779,8 +792,7 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
779792
// With the selection saved, it's safe to replace the
780793
// contenteditable div with the TinyMCE instance (which
781794
// would otherwise wipe the selection).
782-
const tinymce_div =
783-
document.getElementById("TinyMCE-inst")!;
795+
//
784796
// Copy the current TinyMCE instance contents into a
785797
// contenteditable div.
786798
const old_contents_div = document.createElement("div")!;
@@ -794,20 +806,20 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
794806
old_contents_div,
795807
null,
796808
);
809+
// The previous content edited by TinyMCE is now a div. Retypeset this after the transition.
810+
mathJaxTypeset(old_contents_div);
797811
// Move TinyMCE to the new location, then remove the old
798812
// div it will replace.
799813
target.insertBefore(tinymce_div, null);
800-
// TinyMCE edits booger MathJax. Also, the math is
801-
// uneditable. So, translate it back to its untypeset
802-
// form. When editing is done, it will be re-rendered.
803-
mathJaxUnTypeset(contents_div);
804814

805815
// Setting the content makes TinyMCE consider it dirty
806816
// -- ignore this "dirty" event.
807817
ignore_next_dirty = true;
808818
tinymce_singleton!.setContent(contents_div.innerHTML);
809819
tinymce_singleton!.save();
810820
contents_div.remove();
821+
// The new div is now a TinyMCE editor. Retypeset this.
822+
mathJaxTypeset(tinymce_div);
811823

812824
// This process causes TinyMCE to lose focus. Restore
813825
// that. However, this causes TinyMCE to lose the
@@ -824,7 +836,9 @@ export const DocBlockPlugin = ViewPlugin.fromClass(
824836
tinymce_singleton!.getContentAreaContainer();
825837
for (
826838
;
827-
selection_path.length;
839+
selection_path.length &&
840+
// If something goes wrong, bail out instead of producing exceptions.
841+
selection_node !== undefined;
828842
selection_node =
829843
// As before, use the more-consistent
830844
// `children` except for the last element,
@@ -1054,38 +1068,16 @@ export const CodeMirror_load = async (
10541068
await init({
10551069
selector: "#TinyMCE-inst",
10561070
setup: (editor: Editor) => {
1071+
// See the [docs](https://www.tiny.cloud/docs/tinymce/latest/events/#editor-core-events).
10571072
editor.on("Dirty", (event: any) => {
10581073
// Get the div TinyMCE stores edits in. TODO: find
1059-
// documentation for this.
1074+
// documentation for `event.target.bodyElement`.
10601075
const target_or_false = event.target?.bodyElement;
10611076
if (target_or_false == null) {
10621077
return false;
10631078
}
1064-
on_dirty(target_or_false);
1065-
});
1066-
// When leaving a TinyMCE block, retypeset the math. (It's
1067-
// untypeset when entering the block, to avoid editing
1068-
// problems.)
1069-
editor.on("focusout", (event: any) => {
1070-
const target_or_false = event.target;
1071-
if (target_or_false == null) {
1072-
return false;
1073-
}
1074-
// If the editor is dirty, save it first before we
1075-
// possibly modify it.
1076-
if (tinymce_singleton!.isDirty()) {
1077-
tinymce_singleton!.save();
1078-
}
1079-
// When switching from one doc block to another, the
1080-
// MathJax typeset finishes after the new doc block has
1081-
// been updated. To prevent saving the "dirty" content
1082-
// from typesetting, wait until this finishes to clear
1083-
// the `ignore_next_dirty` flag.
1084-
ignore_next_dirty = true;
1085-
mathJaxTypeset(target_or_false, () => {
1086-
tinymce_singleton!.save();
1087-
ignore_next_dirty = false;
1088-
});
1079+
// For some reason, calling this directly omits the most recent edit. Earlier versions of the code didn't have this problem. ???
1080+
setTimeout(() => on_dirty(target_or_false));
10891081
});
10901082
},
10911083
})

0 commit comments

Comments
 (0)