@richscripts/richtexteditor 2.14.0 → 2.15.0
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 +173 -10
- package/angular/build-manifest.json +23 -0
- package/angular/index.d.ts +71 -76
- package/angular/index.mjs +2 -0
- package/angular/package.json +4 -0
- package/headless/index.d.ts +3 -1
- package/headless/index.js +72 -16
- package/index.d.ts +33 -54
- package/integrations/initialization-scope.js +27 -0
- package/integrations/native-mount.js +30 -0
- package/integrations/shared.d.ts +23 -1
- package/integrations/shared.js +58 -22
- package/package.json +25 -3
- package/react/index.js +118 -37
- package/react/index.mjs +29 -0
- package/richtexteditor/all_plugins.js +7113 -1111
- package/richtexteditor/all_plugins.min.js +62 -59
- package/richtexteditor/lang/emoji/LICENSE-CLDR.txt +41 -0
- package/richtexteditor/lang/emoji/rte-emoji-de.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-es.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-fr.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-it.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-ja.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-ko.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-nl.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-pt-pt.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-pt.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-ru.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-zh-Hans.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-zh-Hant.js +3 -0
- package/richtexteditor/lang/rte-lang-af.js +1 -0
- package/richtexteditor/lang/rte-lang-ar.js +1 -0
- package/richtexteditor/lang/rte-lang-bg.js +1 -0
- package/richtexteditor/lang/rte-lang-bn.js +1 -0
- package/richtexteditor/lang/rte-lang-bs.js +1 -0
- package/richtexteditor/lang/rte-lang-ca.js +1 -0
- package/richtexteditor/lang/rte-lang-cs.js +1 -0
- package/richtexteditor/lang/rte-lang-cy.js +1 -0
- package/richtexteditor/lang/rte-lang-da.js +1 -0
- package/richtexteditor/lang/rte-lang-de.js +1017 -2
- package/richtexteditor/lang/rte-lang-el.js +1 -0
- package/richtexteditor/lang/rte-lang-en.js +1 -0
- package/richtexteditor/lang/rte-lang-es.js +1023 -2
- package/richtexteditor/lang/rte-lang-et.js +1 -0
- package/richtexteditor/lang/rte-lang-fa.js +1 -0
- package/richtexteditor/lang/rte-lang-fi.js +1 -0
- package/richtexteditor/lang/rte-lang-fil.js +1 -0
- package/richtexteditor/lang/rte-lang-fj.js +1 -0
- package/richtexteditor/lang/rte-lang-fr.js +1023 -2
- package/richtexteditor/lang/rte-lang-ga.js +1 -0
- package/richtexteditor/lang/rte-lang-gu.js +1 -0
- package/richtexteditor/lang/rte-lang-he.js +1 -0
- package/richtexteditor/lang/rte-lang-hi.js +1 -0
- package/richtexteditor/lang/rte-lang-hr.js +1 -0
- package/richtexteditor/lang/rte-lang-ht.js +1 -0
- package/richtexteditor/lang/rte-lang-hu.js +1 -0
- package/richtexteditor/lang/rte-lang-id.js +1 -0
- package/richtexteditor/lang/rte-lang-is.js +1 -0
- package/richtexteditor/lang/rte-lang-it.js +1023 -2
- package/richtexteditor/lang/rte-lang-ja.js +1023 -2
- package/richtexteditor/lang/rte-lang-kn.js +1 -0
- package/richtexteditor/lang/rte-lang-ko.js +1023 -2
- package/richtexteditor/lang/rte-lang-lt.js +1 -0
- package/richtexteditor/lang/rte-lang-lv.js +1 -0
- package/richtexteditor/lang/rte-lang-mg.js +1 -0
- package/richtexteditor/lang/rte-lang-mi.js +1 -0
- package/richtexteditor/lang/rte-lang-ml.js +1 -0
- package/richtexteditor/lang/rte-lang-mr.js +1 -0
- package/richtexteditor/lang/rte-lang-ms.js +1 -0
- package/richtexteditor/lang/rte-lang-mt.js +1 -0
- package/richtexteditor/lang/rte-lang-mww.js +1 -0
- package/richtexteditor/lang/rte-lang-nb.js +1 -0
- package/richtexteditor/lang/rte-lang-nl.js +1023 -2
- package/richtexteditor/lang/rte-lang-otq.js +1 -0
- package/richtexteditor/lang/rte-lang-pa.js +1 -0
- package/richtexteditor/lang/rte-lang-pl.js +1 -0
- package/richtexteditor/lang/rte-lang-pt-pt.js +1023 -2
- package/richtexteditor/lang/rte-lang-pt.js +1023 -2
- package/richtexteditor/lang/rte-lang-ro.js +1 -0
- package/richtexteditor/lang/rte-lang-ru.js +1023 -2
- package/richtexteditor/lang/rte-lang-sk.js +1 -0
- package/richtexteditor/lang/rte-lang-sl.js +1 -0
- package/richtexteditor/lang/rte-lang-sm.js +1 -0
- package/richtexteditor/lang/rte-lang-sr-Cyrl.js +1 -0
- package/richtexteditor/lang/rte-lang-sr-Latn.js +1 -0
- package/richtexteditor/lang/rte-lang-sv.js +1 -0
- package/richtexteditor/lang/rte-lang-sw.js +1 -0
- package/richtexteditor/lang/rte-lang-ta.js +1 -0
- package/richtexteditor/lang/rte-lang-te.js +1 -0
- package/richtexteditor/lang/rte-lang-th.js +1 -0
- package/richtexteditor/lang/rte-lang-tlh-Latn.js +1 -0
- package/richtexteditor/lang/rte-lang-tlh-Piqd.js +1 -0
- package/richtexteditor/lang/rte-lang-to.js +1 -0
- package/richtexteditor/lang/rte-lang-tr.js +1 -0
- package/richtexteditor/lang/rte-lang-ty.js +1 -0
- package/richtexteditor/lang/rte-lang-uk.js +1 -0
- package/richtexteditor/lang/rte-lang-ur.js +1 -0
- package/richtexteditor/lang/rte-lang-vi.js +1 -0
- package/richtexteditor/lang/rte-lang-yua.js +1 -0
- package/richtexteditor/lang/rte-lang-zh-Hans.js +1017 -2
- package/richtexteditor/lang/rte-lang-zh-Hant.js +1017 -2
- package/richtexteditor/plugins/a11yenhance.js +2 -1
- package/richtexteditor/plugins/a11yenhance.min.js +1 -1
- package/richtexteditor/plugins/accessibilitychecker.js +252 -60
- package/richtexteditor/plugins/accessibilitychecker.min.js +1 -1
- package/richtexteditor/plugins/aitoolkit.js +966 -303
- package/richtexteditor/plugins/aitoolkit.min.js +7 -7
- package/richtexteditor/plugins/all_plugins.js +7113 -1111
- package/richtexteditor/plugins/all_plugins.min.js +62 -59
- package/richtexteditor/plugins/autocorrect.js +66 -31
- package/richtexteditor/plugins/autocorrect.min.js +1 -1
- package/richtexteditor/plugins/autoembed.js +17 -0
- package/richtexteditor/plugins/autoembed.min.js +1 -1
- package/richtexteditor/plugins/blocktypes.js +18 -9
- package/richtexteditor/plugins/blocktypes.min.js +1 -1
- package/richtexteditor/plugins/bookmarkcard.js +12 -5
- package/richtexteditor/plugins/bookmarkcard.min.js +2 -2
- package/richtexteditor/plugins/changecase.js +72 -19
- package/richtexteditor/plugins/changecase.min.js +1 -1
- package/richtexteditor/plugins/charlimit.js +62 -2
- package/richtexteditor/plugins/charlimit.min.js +2 -2
- package/richtexteditor/plugins/comments.js +458 -59
- package/richtexteditor/plugins/comments.min.js +4 -3
- package/richtexteditor/plugins/contentminimap.js +25 -6
- package/richtexteditor/plugins/contentminimap.min.js +1 -1
- package/richtexteditor/plugins/crossreference.js +1 -1
- package/richtexteditor/plugins/crossreference.min.js +1 -1
- package/richtexteditor/plugins/dialoga11y.js +26 -4
- package/richtexteditor/plugins/dialoga11y.min.js +1 -1
- package/richtexteditor/plugins/dictation.js +1 -1
- package/richtexteditor/plugins/documentimport.js +220 -51
- package/richtexteditor/plugins/documentimport.min.js +4 -4
- package/richtexteditor/plugins/documentoutline.js +88 -38
- package/richtexteditor/plugins/documentoutline.min.js +1 -1
- package/richtexteditor/plugins/docxexport.js +204 -32
- package/richtexteditor/plugins/docxexport.min.js +2 -2
- package/richtexteditor/plugins/draghandle.js +267 -3
- package/richtexteditor/plugins/draghandle.min.js +1 -1
- package/richtexteditor/plugins/emailtoolkit.js +28 -14
- package/richtexteditor/plugins/emailtoolkit.min.js +1 -1
- package/richtexteditor/plugins/emojiautocomplete.js +110 -56
- package/richtexteditor/plugins/emojiautocomplete.min.js +1 -1
- package/richtexteditor/plugins/footnotes.js +65 -11
- package/richtexteditor/plugins/footnotes.min.js +1 -1
- package/richtexteditor/plugins/formatpainter.js +10 -0
- package/richtexteditor/plugins/formatpainter.min.js +1 -1
- package/richtexteditor/plugins/formattingmarks.js +53 -8
- package/richtexteditor/plugins/formattingmarks.min.js +1 -1
- package/richtexteditor/plugins/gapcursor.js +60 -0
- package/richtexteditor/plugins/gapcursor.min.js +1 -1
- package/richtexteditor/plugins/ghostcomplete.js +1 -1
- package/richtexteditor/plugins/html2pdf.js +1 -1
- package/richtexteditor/plugins/html2pdf.min.js +1 -1
- package/richtexteditor/plugins/imageeditor.js +12 -6
- package/richtexteditor/plugins/imageeditor.min.js +1 -1
- package/richtexteditor/plugins/importstyles.js +1 -1
- package/richtexteditor/plugins/inlinecode.js +25 -0
- package/richtexteditor/plugins/inlinecode.min.js +1 -1
- package/richtexteditor/plugins/insertcode.js +21 -3
- package/richtexteditor/plugins/insertcode.min.js +4 -4
- package/richtexteditor/plugins/insertemoji.js +112 -15
- package/richtexteditor/plugins/insertemoji.min.js +1 -1
- package/richtexteditor/plugins/insertgallery.js +42 -36
- package/richtexteditor/plugins/insertgallery.min.js +1 -1
- package/richtexteditor/plugins/inserttemplate.js +33 -17
- package/richtexteditor/plugins/inserttemplate.min.js +1 -1
- package/richtexteditor/plugins/keyboarda11y.js +66 -3
- package/richtexteditor/plugins/keyboarda11y.min.js +1 -1
- package/richtexteditor/plugins/layouttable.js +20 -0
- package/richtexteditor/plugins/layouttable.min.js +1 -1
- package/richtexteditor/plugins/linkchecker.js +29 -9
- package/richtexteditor/plugins/linkchecker.min.js +1 -1
- package/richtexteditor/plugins/localdraft.js +51 -7
- package/richtexteditor/plugins/localdraft.min.js +1 -1
- package/richtexteditor/plugins/markdownexport.js +159 -0
- package/richtexteditor/plugins/markdownexport.min.js +2 -2
- package/richtexteditor/plugins/mathdialog.js +11 -5
- package/richtexteditor/plugins/mathdialog.min.js +1 -1
- package/richtexteditor/plugins/mentionplugin.js +88 -25
- package/richtexteditor/plugins/mentionplugin.min.js +1 -1
- package/richtexteditor/plugins/mermaiddiagram.js +9 -3
- package/richtexteditor/plugins/mermaiddiagram.min.js +2 -2
- package/richtexteditor/plugins/multilevellist.js +4 -0
- package/richtexteditor/plugins/multilevellist.min.js +1 -1
- package/richtexteditor/plugins/pdfexport.js +235 -35
- package/richtexteditor/plugins/pdfexport.min.js +12 -12
- package/richtexteditor/plugins/permanentpen.js +3 -3
- package/richtexteditor/plugins/permanentpen.min.js +1 -1
- package/richtexteditor/plugins/readabilitystats.js +25 -11
- package/richtexteditor/plugins/readabilitystats.min.js +1 -1
- package/richtexteditor/plugins/readaloud.js +29 -15
- package/richtexteditor/plugins/readaloud.min.js +1 -1
- package/richtexteditor/plugins/restrictedediting.js +166 -0
- package/richtexteditor/plugins/restrictedediting.min.js +2 -2
- package/richtexteditor/plugins/revisionhistory.js +169 -37
- package/richtexteditor/plugins/revisionhistory.min.js +3 -3
- package/richtexteditor/plugins/rubyannotation.js +19 -9
- package/richtexteditor/plugins/rubyannotation.min.js +1 -1
- package/richtexteditor/plugins/sanitizer.js +86 -4
- package/richtexteditor/plugins/sanitizer.min.js +1 -1
- package/richtexteditor/plugins/slashcommand.js +30 -16
- package/richtexteditor/plugins/slashcommand.min.js +2 -2
- package/richtexteditor/plugins/smartchips.js +36 -22
- package/richtexteditor/plugins/smartchips.min.js +2 -2
- package/richtexteditor/plugins/spellcheck.js +17 -8
- package/richtexteditor/plugins/spellcheck.min.js +2 -2
- package/richtexteditor/plugins/stateinspector.js +19 -9
- package/richtexteditor/plugins/stateinspector.min.js +2 -2
- package/richtexteditor/plugins/structured-content-bridge.js +56 -24
- package/richtexteditor/plugins/structured-content-bridge.min.js +7 -7
- package/richtexteditor/plugins/tableofcontents.js +5 -2
- package/richtexteditor/plugins/tableofcontents.min.js +1 -1
- package/richtexteditor/plugins/tabletools.js +388 -3
- package/richtexteditor/plugins/tabletools.min.js +1 -1
- package/richtexteditor/plugins/textdirection.js +18 -5
- package/richtexteditor/plugins/textdirection.min.js +1 -1
- package/richtexteditor/plugins/textpartlanguage.js +161 -32
- package/richtexteditor/plugins/textpartlanguage.min.js +1 -1
- package/richtexteditor/plugins/todolist.js +90 -6
- package/richtexteditor/plugins/todolist.min.js +1 -1
- package/richtexteditor/plugins/trackedchanges.js +1812 -25
- package/richtexteditor/plugins/trackedchanges.min.js +5 -3
- package/richtexteditor/plugins/wordexport.js +2 -2
- package/richtexteditor/plugins/yjscollab.js +9 -4
- package/richtexteditor/plugins/yjscollab.min.js +1 -1
- package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +22 -13
- package/richtexteditor/plugins/zz_richtextbox_dialog_style.min.js +1 -1
- package/richtexteditor/rte-config.js +131 -50
- package/richtexteditor/rte-headless.js +19 -4
- package/richtexteditor/rte-webcomponent.js +89 -16
- package/richtexteditor/rte.js +133 -52
- package/richtexteditor/runtime/colorpicker_more_ns.htm +93 -16
- package/scripts/build-angular.mjs +97 -0
- package/svelte/index.d.ts +2 -1
- package/svelte/index.js +80 -9
- package/vue/index.js +130 -34
- package/angular/component.ts +0 -297
- package/angular/index.ts +0 -24
|
@@ -44,7 +44,10 @@ function RTE_Plugin_Comments() {
|
|
|
44
44
|
openSidebar: function () { openSidebar(); },
|
|
45
45
|
closeSidebar: function () { closeSidebar(); },
|
|
46
46
|
toggleSidebar: function () { if (sidebar && sidebar.isConnected) closeSidebar(); else openSidebar(); },
|
|
47
|
-
focusComment: function (id) { focusComment(id); }
|
|
47
|
+
focusComment: function (id) { focusComment(id); },
|
|
48
|
+
react: function (commentId, emoji) { return reactToComment(commentId, emoji); },
|
|
49
|
+
next: function () { return stepComment(1); },
|
|
50
|
+
previous: function () { return stepComment(-1); }
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
injectStyles();
|
|
@@ -54,13 +57,46 @@ function RTE_Plugin_Comments() {
|
|
|
54
57
|
return editor.createToolbarButton(cmd);
|
|
55
58
|
};
|
|
56
59
|
|
|
60
|
+
// 2026-10-17 Ctrl/Cmd+Alt+M adds a comment on the selection (Google Docs, CKEditor and TinyMCE bind it). A key that
|
|
61
|
+
// reports a character other than "m" is AltGr typing (German AltGr+M is a micro sign) and is left alone.
|
|
62
|
+
// tools/keyboard/comment-shortcut.mjs
|
|
63
|
+
(editor.bindDocumentEvent ? function (fn) { editor.bindDocumentEvent("keydown", fn, true); } : function (fn) { editor.getDocument().addEventListener("keydown", fn, true); })(function (e) {
|
|
64
|
+
if (!(e.ctrlKey || e.metaKey) || !e.altKey || e.shiftKey) return;
|
|
65
|
+
if (e.key !== "m" && e.key !== "M") return;
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
openComposerForSelection();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 2026-10-17 Google Docs' chords: Ctrl/Cmd+Alt+N then C = next comment, Ctrl/Cmd+Alt+P then C = previous. A key
|
|
71
|
+
// that reports a character other than n / p is AltGr typing and is left alone. tools/comments/comment-navigation.mjs
|
|
72
|
+
var chordDir = 0, chordAt = 0;
|
|
73
|
+
(editor.bindDocumentEvent ? function (fn) { editor.bindDocumentEvent("keydown", fn, true); } : function (fn) { editor.getDocument().addEventListener("keydown", fn, true); })(function (e) {
|
|
74
|
+
if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === "n" || e.key === "N" || e.key === "p" || e.key === "P")) {
|
|
75
|
+
chordDir = (e.key === "n" || e.key === "N") ? 1 : -1;
|
|
76
|
+
chordAt = Date.now();
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (chordDir && !e.ctrlKey && !e.metaKey && !e.altKey && (e.key === "c" || e.key === "C") && Date.now() - chordAt < 2000) {
|
|
81
|
+
var dir = chordDir;
|
|
82
|
+
chordDir = 0;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
stepComment(dir);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (e.key !== "Control" && e.key !== "Alt" && e.key !== "Meta" && e.key !== "Shift") chordDir = 0;
|
|
88
|
+
});
|
|
89
|
+
|
|
57
90
|
editor.attachEvent("exec_command_commentadd", function (state) {
|
|
58
91
|
state.returnValue = true;
|
|
59
92
|
state.stopBubble = true;
|
|
60
93
|
openComposerForSelection();
|
|
61
94
|
});
|
|
62
95
|
|
|
63
|
-
|
|
96
|
+
// 2026-10-17 bound through the core so it follows the live editing document: plugins that wrap the content area move
|
|
97
|
+
// the iframe during init, and a listener on the old document died (a click on a comment's highlight did nothing).
|
|
98
|
+
// tools/keyboard/plugin-listeners-live.mjs
|
|
99
|
+
(editor.bindEditableEvent ? function (fn) { editor.bindEditableEvent("click", fn); } : function (fn) { editor.getEditable().addEventListener("click", fn); })(function (e) {
|
|
64
100
|
var span = e.target && e.target.closest ? e.target.closest(".rte-comment") : null;
|
|
65
101
|
if (!span) return;
|
|
66
102
|
var id = span.getAttribute("data-comment-id");
|
|
@@ -120,6 +156,24 @@ function RTE_Plugin_Comments() {
|
|
|
120
156
|
|
|
121
157
|
// --- creating comments ---
|
|
122
158
|
|
|
159
|
+
// 2026-10-18 a comments button runs its action on click, so Enter and Space work (they fire click, not mousedown;
|
|
160
|
+
// these ran on mousedown only and did nothing from the keyboard). mousedown still prevents default so a mouse press
|
|
161
|
+
// keeps the editor's selection. If a keyboard press re-rendered the sidebar and took focus with it, focus goes to
|
|
162
|
+
// the sidebar's first control. tools/a11y/comment-buttons-keyboard.mjs
|
|
163
|
+
function onActivate(el, fn) {
|
|
164
|
+
el.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
|
165
|
+
el.addEventListener("click", function (e) {
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
var fromKeyboard = e.detail === 0;
|
|
168
|
+
fn();
|
|
169
|
+
if (!fromKeyboard || !sidebar || !sidebar.isConnected) return;
|
|
170
|
+
var a = sidebar.ownerDocument.activeElement;
|
|
171
|
+
if (a && a !== sidebar.ownerDocument.body && a.isConnected) return;
|
|
172
|
+
var first = sidebar.querySelector("button, [tabindex]:not([tabindex='-1'])");
|
|
173
|
+
if (first) { try { first.focus(); } catch (x) { /* ignore */ } }
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
123
177
|
function openComposerForSelection() {
|
|
124
178
|
var sel = editor.getSelection();
|
|
125
179
|
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
|
|
@@ -150,35 +204,117 @@ function RTE_Plugin_Comments() {
|
|
|
150
204
|
composer = host.createElement("div");
|
|
151
205
|
composer.className = "rte-comment-composer" + (isEditorDark() ? " rte-comment-dark" : "");
|
|
152
206
|
composer.setAttribute("role", "dialog");
|
|
153
|
-
composer.setAttribute("aria-label", "Add comment");
|
|
207
|
+
composer.setAttribute("aria-label", cmText("cm_addcomment", "Add comment"));
|
|
154
208
|
|
|
155
209
|
var header = host.createElement("div");
|
|
156
210
|
header.className = "rte-comment-composer-header";
|
|
157
|
-
header.textContent = "Add comment";
|
|
211
|
+
header.textContent = cmText("cm_addcomment", "Add comment");
|
|
158
212
|
composer.appendChild(header);
|
|
159
213
|
|
|
160
214
|
var textarea = host.createElement("textarea");
|
|
161
215
|
textarea.className = "rte-comment-composer-textarea";
|
|
162
|
-
textarea.placeholder = "Type your comment...";
|
|
216
|
+
textarea.placeholder = cmText("cm_typecomment", "Type your comment...");
|
|
163
217
|
textarea.rows = 3;
|
|
164
218
|
composer.appendChild(textarea);
|
|
165
219
|
|
|
220
|
+
// Mentions: "@" starts a list of the users in config.commentMentions. Choosing one inserts "@Name" and keeps
|
|
221
|
+
// the user id with the comment. tools/comments/comment-mentions.mjs
|
|
222
|
+
var mentionBox = host.createElement("div");
|
|
223
|
+
mentionBox.className = "rte-comment-mentions";
|
|
224
|
+
mentionBox.setAttribute("role", "listbox");
|
|
225
|
+
mentionBox.style.display = "none";
|
|
226
|
+
composer.appendChild(mentionBox);
|
|
227
|
+
var picked = [];
|
|
228
|
+
var mentionItems = [];
|
|
229
|
+
var mentionActive = 0;
|
|
230
|
+
var mentionStart = -1;
|
|
231
|
+
function mentionQuery() {
|
|
232
|
+
var before = textarea.value.slice(0, textarea.selectionStart);
|
|
233
|
+
var m = /(^|\s)@([^\s@]*)$/.exec(before);
|
|
234
|
+
if (!m) return null;
|
|
235
|
+
mentionStart = before.length - m[2].length - 1;
|
|
236
|
+
return m[2];
|
|
237
|
+
}
|
|
238
|
+
function hideMentions() {
|
|
239
|
+
while (mentionBox.firstChild) mentionBox.removeChild(mentionBox.firstChild);
|
|
240
|
+
mentionBox.style.display = "none";
|
|
241
|
+
mentionItems = [];
|
|
242
|
+
mentionStart = -1;
|
|
243
|
+
}
|
|
244
|
+
function paintMentions() {
|
|
245
|
+
while (mentionBox.firstChild) mentionBox.removeChild(mentionBox.firstChild);
|
|
246
|
+
for (var i = 0; i < mentionItems.length; i++) {
|
|
247
|
+
var opt = host.createElement("div");
|
|
248
|
+
opt.className = "rte-comment-mention-option" + (i === mentionActive ? " rte-comment-mention-active" : "");
|
|
249
|
+
opt.setAttribute("role", "option");
|
|
250
|
+
opt.setAttribute("aria-selected", i === mentionActive ? "true" : "false");
|
|
251
|
+
opt.textContent = mentionItems[i].name;
|
|
252
|
+
opt.addEventListener("mousedown", (function (u) { return function (e) { e.preventDefault(); pickMention(u); }; })(mentionItems[i]));
|
|
253
|
+
mentionBox.appendChild(opt);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function refreshMentions() {
|
|
257
|
+
var q = mentionQuery();
|
|
258
|
+
var users = Array.isArray(config.commentMentions) ? config.commentMentions : [];
|
|
259
|
+
mentionItems = [];
|
|
260
|
+
if (q !== null) {
|
|
261
|
+
var ql = q.toLowerCase();
|
|
262
|
+
for (var i = 0; i < users.length; i++) {
|
|
263
|
+
var u = users[i];
|
|
264
|
+
if (!u || !u.name || u.id == null) continue;
|
|
265
|
+
var words = String(u.name).toLowerCase().split(/\s+/);
|
|
266
|
+
for (var w = 0; w < words.length; w++) {
|
|
267
|
+
if (words[w].indexOf(ql) === 0) { mentionItems.push(u); break; }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!mentionItems.length) { hideMentions(); return; }
|
|
272
|
+
mentionActive = 0;
|
|
273
|
+
mentionBox.style.display = "";
|
|
274
|
+
paintMentions();
|
|
275
|
+
}
|
|
276
|
+
function pickMention(user) {
|
|
277
|
+
if (!user || mentionStart < 0) return;
|
|
278
|
+
var insert = "@" + user.name + " ";
|
|
279
|
+
var caret = textarea.selectionStart;
|
|
280
|
+
textarea.value = textarea.value.slice(0, mentionStart) + insert + textarea.value.slice(caret);
|
|
281
|
+
var pos = mentionStart + insert.length;
|
|
282
|
+
textarea.setSelectionRange(pos, pos);
|
|
283
|
+
if (!picked.some(function (m) { return m.id === user.id; })) picked.push({ id: user.id, name: user.name });
|
|
284
|
+
hideMentions();
|
|
285
|
+
}
|
|
286
|
+
textarea.addEventListener("input", refreshMentions);
|
|
287
|
+
textarea.addEventListener("keydown", function (e) {
|
|
288
|
+
if (mentionBox.style.display === "none") return;
|
|
289
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
290
|
+
var step = e.key === "ArrowDown" ? 1 : -1;
|
|
291
|
+
mentionActive = (mentionActive + step + mentionItems.length) % mentionItems.length;
|
|
292
|
+
paintMentions();
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
} else if (e.key === "Enter" || e.key === "Tab") {
|
|
295
|
+
pickMention(mentionItems[mentionActive]);
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
} else if (e.key === "Escape" || e.key === "Esc") {
|
|
298
|
+
hideMentions();
|
|
299
|
+
e.stopPropagation();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
166
303
|
var actions = host.createElement("div");
|
|
167
304
|
actions.className = "rte-comment-composer-actions";
|
|
168
305
|
var cancel = host.createElement("button");
|
|
169
306
|
cancel.type = "button";
|
|
170
307
|
cancel.className = "rte-comment-btn rte-comment-btn-ghost";
|
|
171
|
-
cancel.textContent = "Cancel";
|
|
172
|
-
cancel
|
|
308
|
+
cancel.textContent = cmText("cancel", "Cancel");
|
|
309
|
+
onActivate(cancel, closeComposer);
|
|
173
310
|
var submit = host.createElement("button");
|
|
174
311
|
submit.type = "button";
|
|
175
312
|
submit.className = "rte-comment-btn rte-comment-btn-primary";
|
|
176
|
-
submit.textContent = "Comment";
|
|
177
|
-
submit
|
|
178
|
-
e.preventDefault();
|
|
313
|
+
submit.textContent = cmText("cm_submit", "Comment");
|
|
314
|
+
onActivate(submit, function () {
|
|
179
315
|
var text = textarea.value.replace(/^\s+|\s+$/g, "");
|
|
180
316
|
if (!text) return;
|
|
181
|
-
addComment({ anchor: options.anchor, text: text });
|
|
317
|
+
addComment({ anchor: options.anchor, text: text, mentions: picked.filter(function (m) { return text.indexOf("@" + m.name) >= 0; }) });
|
|
182
318
|
closeComposer();
|
|
183
319
|
});
|
|
184
320
|
actions.appendChild(cancel);
|
|
@@ -195,6 +331,35 @@ function RTE_Plugin_Comments() {
|
|
|
195
331
|
composer.addEventListener("keydown", escListener);
|
|
196
332
|
}
|
|
197
333
|
|
|
334
|
+
// Users a comment mentions: { id, name } pairs, with anything else dropped.
|
|
335
|
+
function sanitizeMentions(list) {
|
|
336
|
+
var out = [];
|
|
337
|
+
if (!Array.isArray(list)) return out;
|
|
338
|
+
for (var i = 0; i < list.length; i++) {
|
|
339
|
+
var m = list[i];
|
|
340
|
+
if (m && m.id != null && m.name) out.push({ id: String(m.id), name: String(m.name) });
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Comment text with each mention ("@Name") in its own highlighted span. Built from text nodes, never innerHTML.
|
|
346
|
+
function appendTextWithMentions(host, parent, text, mentions) {
|
|
347
|
+
var names = sanitizeMentions(mentions).sort(function (a, b) { return b.name.length - a.name.length; });
|
|
348
|
+
if (!names.length || !text) { parent.textContent = text || ""; return; }
|
|
349
|
+
var pattern = names.map(function (m) { return m.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }).join("|");
|
|
350
|
+
var re = new RegExp("@(" + pattern + ")", "g");
|
|
351
|
+
var last = 0, hit;
|
|
352
|
+
while ((hit = re.exec(text)) !== null) {
|
|
353
|
+
if (hit.index > last) parent.appendChild(host.createTextNode(text.slice(last, hit.index)));
|
|
354
|
+
var span = host.createElement("span");
|
|
355
|
+
span.className = "rte-comment-mention";
|
|
356
|
+
span.textContent = hit[0];
|
|
357
|
+
parent.appendChild(span);
|
|
358
|
+
last = hit.index + hit[0].length;
|
|
359
|
+
}
|
|
360
|
+
if (last < text.length) parent.appendChild(host.createTextNode(text.slice(last)));
|
|
361
|
+
}
|
|
362
|
+
|
|
198
363
|
function positionComposer(anchor) {
|
|
199
364
|
if (!composer) return;
|
|
200
365
|
composer.style.position = "absolute";
|
|
@@ -209,8 +374,10 @@ function RTE_Plugin_Comments() {
|
|
|
209
374
|
r.setEnd(anchor.endContainer, anchor.endOffset);
|
|
210
375
|
var rects = r.getClientRects();
|
|
211
376
|
var rect = rects && rects.length ? rects[rects.length - 1] : r.getBoundingClientRect();
|
|
212
|
-
|
|
213
|
-
|
|
377
|
+
// the rect is in iframe pixels, scaled on screen inside a transform:scale() ancestor
|
|
378
|
+
var sk = iframe.offsetWidth && ir.width > 0 ? ir.width / iframe.offsetWidth : 1;
|
|
379
|
+
left = ir.left + (rect.left || 0) * sk + window.pageXOffset;
|
|
380
|
+
top = ir.top + (rect.bottom || rect.top || 0) * sk + window.pageYOffset + 6;
|
|
214
381
|
} catch (ignore) { }
|
|
215
382
|
}
|
|
216
383
|
composer.style.left = left + "px";
|
|
@@ -249,21 +416,26 @@ function RTE_Plugin_Comments() {
|
|
|
249
416
|
var wrapRange = editdoc.createRange();
|
|
250
417
|
wrapRange.setStart(anchor.startContainer, anchor.startOffset);
|
|
251
418
|
wrapRange.setEnd(anchor.endContainer, anchor.endOffset);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
var
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
419
|
+
// 2026-10-02 one highlight per block the selection crosses (wrapEachBlock)
|
|
420
|
+
var spans = wrapEachBlock(editdoc, wrapRange, function () {
|
|
421
|
+
var span = editdoc.createElement("span");
|
|
422
|
+
span.className = "rte-comment";
|
|
423
|
+
span.setAttribute("data-comment-id", id);
|
|
424
|
+
span.setAttribute("data-comment-author", user.id);
|
|
425
|
+
return span;
|
|
426
|
+
});
|
|
427
|
+
var parts = [];
|
|
428
|
+
for (var si = 0; si < spans.length; si++) parts.push(spans[si].textContent);
|
|
429
|
+
wrappedText = parts.join("\n");
|
|
430
|
+
if (spans.length) {
|
|
431
|
+
// Collapse caret after the last highlight.
|
|
432
|
+
var after = editdoc.createRange();
|
|
433
|
+
after.setStartAfter(spans[spans.length - 1]);
|
|
434
|
+
after.collapse(true);
|
|
435
|
+
var sel = editor.getSelection();
|
|
436
|
+
sel.removeAllRanges();
|
|
437
|
+
sel.addRange(after);
|
|
438
|
+
}
|
|
267
439
|
} catch (err) {
|
|
268
440
|
console.warn("comments: range wrap failed:", err);
|
|
269
441
|
}
|
|
@@ -274,6 +446,7 @@ function RTE_Plugin_Comments() {
|
|
|
274
446
|
changeType: "comment",
|
|
275
447
|
author: user,
|
|
276
448
|
text: text,
|
|
449
|
+
mentions: sanitizeMentions(options.mentions),
|
|
277
450
|
originalText: wrappedText || (anchor && anchor.text) || "",
|
|
278
451
|
status: "pending",
|
|
279
452
|
sourceLabel: "Comment",
|
|
@@ -304,10 +477,91 @@ function RTE_Plugin_Comments() {
|
|
|
304
477
|
return true;
|
|
305
478
|
}
|
|
306
479
|
|
|
480
|
+
// 2026-10-15 reactions (BlockNote has them): a person adds an emoji to a comment and clicks it again to take it
|
|
481
|
+
// back. Kept with the comment in the shared review store as [{ emoji, users: [id] }]; only this set is accepted.
|
|
482
|
+
// tools/comments/comment-reactions.mjs
|
|
483
|
+
var REACTIONS = [
|
|
484
|
+
{ emoji: "\uD83D\uDC4D", label: "Thumbs up", key: "cm_thumbsup" },
|
|
485
|
+
{ emoji: "\u2705", label: "Done", key: "cm_done" },
|
|
486
|
+
{ emoji: "\uD83C\uDF89", label: "Celebrate", key: "cm_celebrate" },
|
|
487
|
+
{ emoji: "\uD83D\uDC40", label: "Looking", key: "cm_looking" },
|
|
488
|
+
{ emoji: "\u2753", label: "Question", key: "cm_question" }
|
|
489
|
+
];
|
|
490
|
+
// 2026-10-18 the comments UI reads config.text_ keys (language packs), English as the fallback; {0} / {1} fill a
|
|
491
|
+
// template. tools/i18n/comments-language.mjs
|
|
492
|
+
function cmText(key, fallback) {
|
|
493
|
+
var s = (config && config["text_" + key]) || fallback;
|
|
494
|
+
for (var i = 2; i < arguments.length; i++) s = s.split("{" + (i - 2) + "}").join(String(arguments[i]));
|
|
495
|
+
return s;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function reactionIndex(emoji) {
|
|
499
|
+
for (var i = 0; i < REACTIONS.length; i++) if (REACTIONS[i].emoji === emoji) return i;
|
|
500
|
+
return -1;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function reactToComment(commentId, emoji) {
|
|
504
|
+
if (!editor.reviewLedger || reactionIndex(emoji) < 0) return false;
|
|
505
|
+
var entry = editor.reviewLedger.get(commentId);
|
|
506
|
+
if (!entry || entry.changeType !== "comment") return false;
|
|
507
|
+
var me = String(getCurrentUser().id);
|
|
508
|
+
var list = Array.isArray(entry.reactions) ? entry.reactions : [];
|
|
509
|
+
var out = [], found = false;
|
|
510
|
+
for (var i = 0; i < list.length; i++) {
|
|
511
|
+
if (!list[i] || !Array.isArray(list[i].users)) continue;
|
|
512
|
+
var users = list[i].users.slice();
|
|
513
|
+
if (list[i].emoji === emoji) {
|
|
514
|
+
found = true;
|
|
515
|
+
var at = users.indexOf(me);
|
|
516
|
+
if (at >= 0) users.splice(at, 1); else users.push(me);
|
|
517
|
+
}
|
|
518
|
+
if (users.length) out.push({ emoji: list[i].emoji, users: users });
|
|
519
|
+
}
|
|
520
|
+
if (!found) out.push({ emoji: emoji, users: [me] });
|
|
521
|
+
editor.reviewLedger.update(commentId, { reactions: out });
|
|
522
|
+
if (sidebar && sidebar.isConnected) renderSidebar();
|
|
523
|
+
try { if (typeof editor.fireEvent === "function") editor.fireEvent("comment_reacted", { id: commentId, emoji: emoji, user: me }); } catch (e) { }
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function renderReactions(entry, host) {
|
|
528
|
+
var row = host.createElement("div");
|
|
529
|
+
row.className = "rte-comment-reactions";
|
|
530
|
+
row.setAttribute("role", "group");
|
|
531
|
+
row.setAttribute("aria-label", cmText("cm_reactions", "Reactions"));
|
|
532
|
+
var me = String(getCurrentUser().id);
|
|
533
|
+
var list = Array.isArray(entry.reactions) ? entry.reactions : [];
|
|
534
|
+
REACTIONS.forEach(function (r, index) {
|
|
535
|
+
var users = [];
|
|
536
|
+
for (var i = 0; i < list.length; i++) if (list[i] && list[i].emoji === r.emoji && Array.isArray(list[i].users)) users = list[i].users;
|
|
537
|
+
var mine = users.indexOf(me) >= 0;
|
|
538
|
+
var b = host.createElement("button");
|
|
539
|
+
b.type = "button";
|
|
540
|
+
b.className = "rte-comment-reaction";
|
|
541
|
+
b.setAttribute("data-reaction", String(index));
|
|
542
|
+
b.setAttribute("aria-pressed", mine ? "true" : "false");
|
|
543
|
+
var rLabel = cmText(r.key, r.label);
|
|
544
|
+
b.setAttribute("aria-label", users.length === 1 ? cmText("cm_reactioncount", "{0}, {1} reaction", rLabel, 1) : cmText("cm_reactionscount", "{0}, {1} reactions", rLabel, users.length));
|
|
545
|
+
b.title = rLabel;
|
|
546
|
+
b.textContent = r.emoji + (users.length ? " " + users.length : "");
|
|
547
|
+
b.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
|
548
|
+
b.addEventListener("click", function (e) {
|
|
549
|
+
e.preventDefault();
|
|
550
|
+
reactToComment(entry.id, r.emoji);
|
|
551
|
+
// the sidebar was rebuilt; keep keyboard focus on the same button
|
|
552
|
+
try {
|
|
553
|
+
var again = sidebar && sidebar.querySelector('[data-comment-ref="' + cssEscape(entry.id) + '"] .rte-comment-reaction[data-reaction="' + index + '"]');
|
|
554
|
+
if (again) again.focus();
|
|
555
|
+
} catch (err) { }
|
|
556
|
+
});
|
|
557
|
+
row.appendChild(b);
|
|
558
|
+
});
|
|
559
|
+
return row;
|
|
560
|
+
}
|
|
561
|
+
|
|
307
562
|
function resolveComment(commentId) {
|
|
308
563
|
if (!editor.reviewLedger) return false;
|
|
309
|
-
|
|
310
|
-
if (span) unwrapKeepChildren(span);
|
|
564
|
+
unwrapCommentSpans(commentId);
|
|
311
565
|
editor.reviewLedger.update(commentId, { status: "resolved", decidedAt: Date.now() });
|
|
312
566
|
if (sidebar && sidebar.isConnected) renderSidebar();
|
|
313
567
|
try { if (typeof editor.fireEvent === "function") editor.fireEvent("comment_resolved", { id: commentId }); } catch (e) { }
|
|
@@ -316,14 +570,62 @@ function RTE_Plugin_Comments() {
|
|
|
316
570
|
|
|
317
571
|
function deleteComment(commentId) {
|
|
318
572
|
if (!editor.reviewLedger) return false;
|
|
319
|
-
|
|
320
|
-
if (span) unwrapKeepChildren(span);
|
|
573
|
+
unwrapCommentSpans(commentId);
|
|
321
574
|
editor.reviewLedger.remove(commentId);
|
|
322
575
|
if (sidebar && sidebar.isConnected) renderSidebar();
|
|
323
576
|
return true;
|
|
324
577
|
}
|
|
325
578
|
|
|
579
|
+
// 2026-10-17 select the next / previous commented text after / before the caret (wrapping) and show it in the
|
|
580
|
+
// sidebar. tools/comments/comment-navigation.mjs
|
|
581
|
+
function stepComment(dir) {
|
|
582
|
+
var doc = editor.getDocument(), ed = editor.getEditable();
|
|
583
|
+
if (!doc || !ed) return null;
|
|
584
|
+
var spans = ed.querySelectorAll("[data-comment-id]"), firsts = [], seen = {};
|
|
585
|
+
for (var i = 0; i < spans.length; i++) {
|
|
586
|
+
var cid = spans[i].getAttribute("data-comment-id");
|
|
587
|
+
if (!cid || seen[cid]) continue;
|
|
588
|
+
seen[cid] = true;
|
|
589
|
+
firsts.push(spans[i]);
|
|
590
|
+
}
|
|
591
|
+
if (!firsts.length) return null;
|
|
592
|
+
var sel = doc.getSelection(), cur = sel && sel.rangeCount ? sel.getRangeAt(0) : null;
|
|
593
|
+
var pick = null;
|
|
594
|
+
if (cur) {
|
|
595
|
+
if (dir > 0) {
|
|
596
|
+
for (var a = 0; a < firsts.length && !pick; a++) {
|
|
597
|
+
var ra = doc.createRange(); ra.selectNodeContents(firsts[a]);
|
|
598
|
+
if (ra.compareBoundaryPoints(Range.START_TO_END, cur) >= 0 && ra.compareBoundaryPoints(Range.START_TO_START, cur) > 0) pick = firsts[a];
|
|
599
|
+
else if (ra.compareBoundaryPoints(Range.START_TO_START, cur) >= 0 && !cur.collapsed && ra.compareBoundaryPoints(Range.START_TO_START, cur) !== 0) pick = firsts[a];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
for (var b = firsts.length - 1; b >= 0 && !pick; b--) {
|
|
604
|
+
var rb = doc.createRange(); rb.selectNodeContents(firsts[b]);
|
|
605
|
+
if (rb.compareBoundaryPoints(Range.START_TO_START, cur) < 0) pick = firsts[b];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!pick) pick = dir > 0 ? firsts[0] : firsts[firsts.length - 1];
|
|
610
|
+
var id = pick.getAttribute("data-comment-id");
|
|
611
|
+
var all = ed.querySelectorAll('[data-comment-id="' + cssEscape(id) + '"]');
|
|
612
|
+
var r = doc.createRange();
|
|
613
|
+
r.setStart(all[0], 0);
|
|
614
|
+
r.setEnd(all[all.length - 1], all[all.length - 1].childNodes.length);
|
|
615
|
+
try { editor.focus(); } catch (x) { /* best effort */ }
|
|
616
|
+
sel.removeAllRanges();
|
|
617
|
+
sel.addRange(r);
|
|
618
|
+
try { pick.scrollIntoView({ block: "nearest" }); } catch (x) { /* best effort */ }
|
|
619
|
+
focusComment(id);
|
|
620
|
+
return id;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 2026-10-17 the highlighted comment survives a re-render of the sidebar (selecting its text re-renders it, which
|
|
624
|
+
// dropped the highlight at once). tools/comments/comment-navigation.mjs
|
|
625
|
+
var activeCommentId = null, activeCommentUntil = 0;
|
|
326
626
|
function focusComment(commentId) {
|
|
627
|
+
activeCommentId = commentId;
|
|
628
|
+
activeCommentUntil = Date.now() + 1400;
|
|
327
629
|
if (!sidebar || !sidebar.isConnected) openSidebar();
|
|
328
630
|
setTimeout(function () {
|
|
329
631
|
if (!sidebar) return;
|
|
@@ -331,11 +633,84 @@ function RTE_Plugin_Comments() {
|
|
|
331
633
|
if (item) {
|
|
332
634
|
item.scrollIntoView({ block: "nearest" });
|
|
333
635
|
item.classList.add("rte-comment-item-active");
|
|
334
|
-
|
|
636
|
+
// a timer from an earlier highlight of the same comment must not clear a newer one
|
|
637
|
+
setTimeout(function () { if (activeCommentId !== commentId || Date.now() >= activeCommentUntil - 20) item.classList.remove("rte-comment-item-active"); }, 1400);
|
|
335
638
|
}
|
|
336
639
|
}, 30);
|
|
337
640
|
}
|
|
338
641
|
|
|
642
|
+
// 2026-10-02 A comment on a selection that crosses blocks highlights the selected text of EACH
|
|
643
|
+
// block. One span went AROUND the blocks: two paragraphs became four
|
|
644
|
+
// (<p>al</p><span><p>pha</p><p>be</p></span><p>ta</p>), list items and table cells got a span
|
|
645
|
+
// inside <ul> / <tr>, and resolving the comment left the four paragraphs. The selection is cut
|
|
646
|
+
// into runs of text that share a block, and each run is wrapped in its own make() element; all
|
|
647
|
+
// carry the comment's id, so resolve and delete remove every one. A run starting or ending at
|
|
648
|
+
// the edge of an inline element (<b>) takes the whole element, so no empty <b></b> is left
|
|
649
|
+
// behind. Text inside a non-editable element (a chip) moves with its run.
|
|
650
|
+
// tools/comments/comments-across-blocks.mjs
|
|
651
|
+
var BLOCK_RE = /^(P|DIV|LI|UL|OL|DL|DT|DD|TABLE|TBODY|THEAD|TFOOT|TR|TD|TH|CAPTION|H[1-6]|PRE|BLOCKQUOTE|ADDRESS|FIGURE|FIGCAPTION|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|NAV|MAIN|DETAILS|SUMMARY|FORM|FIELDSET|LEGEND|HR|BODY)$/;
|
|
652
|
+
function blockOf(node) {
|
|
653
|
+
for (var n = node; n; n = n.parentNode)
|
|
654
|
+
if (n.nodeType === 1 && BLOCK_RE.test(n.nodeName)) return n;
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
function wrapEachBlock(doc, range, make) {
|
|
658
|
+
var root = range.commonAncestorContainer;
|
|
659
|
+
if (root.nodeType !== 1) root = root.parentNode;
|
|
660
|
+
var runs = [], cur = null;
|
|
661
|
+
var w = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
|
|
662
|
+
acceptNode: function (n) { return n.nodeType === 1 && n.getAttribute("contenteditable") === "false" ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; }
|
|
663
|
+
});
|
|
664
|
+
for (var n = w.nextNode(); n; n = w.nextNode()) {
|
|
665
|
+
if (n.nodeType === 1) {
|
|
666
|
+
if (BLOCK_RE.test(n.nodeName)) cur = null;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (!range.intersectsNode(n)) continue;
|
|
670
|
+
var a = n === range.startContainer ? range.startOffset : 0, b = n === range.endContainer ? range.endOffset : n.data.length;
|
|
671
|
+
if (b <= a) continue;
|
|
672
|
+
var blk = blockOf(n);
|
|
673
|
+
if (!cur || cur.block !== blk) {
|
|
674
|
+
cur = { block: blk, sn: n, so: a, text: "" };
|
|
675
|
+
runs.push(cur);
|
|
676
|
+
}
|
|
677
|
+
cur.en = n;
|
|
678
|
+
cur.eo = b;
|
|
679
|
+
cur.text += n.data.substring(a, b);
|
|
680
|
+
}
|
|
681
|
+
var out = [];
|
|
682
|
+
// from the last run back: wrapping one never moves the text of a run still to come
|
|
683
|
+
for (var i = runs.length - 1; i >= 0; i--) {
|
|
684
|
+
var run = runs[i];
|
|
685
|
+
if (!/\S/.test(run.text)) continue; // the line break between two blocks
|
|
686
|
+
var sub = doc.createRange();
|
|
687
|
+
sub.setStart(run.sn, run.so);
|
|
688
|
+
sub.setEnd(run.en, run.eo);
|
|
689
|
+
widenToInlineEdges(sub, run.block);
|
|
690
|
+
var el = make();
|
|
691
|
+
el.appendChild(sub.extractContents());
|
|
692
|
+
sub.insertNode(el);
|
|
693
|
+
out.unshift(el);
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
function widenToInlineEdges(range, block) {
|
|
698
|
+
var n = range.startContainer;
|
|
699
|
+
if (n.nodeType === 3 && range.startOffset === 0) {
|
|
700
|
+
while (n.parentNode && n.parentNode !== block && !n.previousSibling && !BLOCK_RE.test(n.parentNode.nodeName)) n = n.parentNode;
|
|
701
|
+
range.setStartBefore(n);
|
|
702
|
+
}
|
|
703
|
+
n = range.endContainer;
|
|
704
|
+
if (n.nodeType === 3 && range.endOffset === n.data.length) {
|
|
705
|
+
while (n.parentNode && n.parentNode !== block && !n.nextSibling && !BLOCK_RE.test(n.parentNode.nodeName)) n = n.parentNode;
|
|
706
|
+
range.setEndAfter(n);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function unwrapCommentSpans(commentId) {
|
|
710
|
+
var spans = editor.getEditable().querySelectorAll('[data-comment-id="' + cssEscape(commentId) + '"]');
|
|
711
|
+
for (var i = 0; i < spans.length; i++) unwrapKeepChildren(spans[i]);
|
|
712
|
+
}
|
|
713
|
+
|
|
339
714
|
function unwrapKeepChildren(el) {
|
|
340
715
|
var parent = el.parentNode;
|
|
341
716
|
if (!parent) return;
|
|
@@ -367,7 +742,9 @@ function RTE_Plugin_Comments() {
|
|
|
367
742
|
sidebar = host.createElement("div");
|
|
368
743
|
sidebar.className = "rte-comment-sidebar" + (isEditorDark() ? " rte-comment-dark" : "");
|
|
369
744
|
sidebar.setAttribute("role", "complementary");
|
|
370
|
-
sidebar.setAttribute("aria-label", "Comments");
|
|
745
|
+
sidebar.setAttribute("aria-label", cmText("cm_comments", "Comments"));
|
|
746
|
+
// 2026-10-17 drag / arrow-key resizable (tools/ui/panel-resize.mjs)
|
|
747
|
+
setTimeout(function () { if (editor && sidebar && typeof editor.makePanelResizable === "function") editor.makePanelResizable(sidebar, { label: cmText("cm_resizepanel", "Resize comments panel") }); }, 0);
|
|
371
748
|
var shell = getEditorShell();
|
|
372
749
|
if (shell) {
|
|
373
750
|
shell.parentNode.insertBefore(sidebar, shell.nextSibling);
|
|
@@ -403,14 +780,14 @@ function RTE_Plugin_Comments() {
|
|
|
403
780
|
header.className = "rte-comment-sidebar-header";
|
|
404
781
|
var title = host.createElement("div");
|
|
405
782
|
title.className = "rte-comment-sidebar-title";
|
|
406
|
-
title.textContent = "Comments";
|
|
783
|
+
title.textContent = cmText("cm_comments", "Comments");
|
|
407
784
|
header.appendChild(title);
|
|
408
785
|
var close = host.createElement("button");
|
|
409
786
|
close.type = "button";
|
|
410
787
|
close.className = "rte-comment-sidebar-close";
|
|
411
|
-
close.setAttribute("aria-label", "Close");
|
|
788
|
+
close.setAttribute("aria-label", cmText("close", "Close"));
|
|
412
789
|
close.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12"/><path d="M18 6L6 18"/></svg>';
|
|
413
|
-
close
|
|
790
|
+
onActivate(close, closeSidebar);
|
|
414
791
|
header.appendChild(close);
|
|
415
792
|
sidebar.appendChild(header);
|
|
416
793
|
|
|
@@ -422,7 +799,7 @@ function RTE_Plugin_Comments() {
|
|
|
422
799
|
if (!entries.length) {
|
|
423
800
|
var empty = host.createElement("div");
|
|
424
801
|
empty.className = "rte-comment-empty";
|
|
425
|
-
empty.textContent = "No open comments. Select text and click the Comment button to start.";
|
|
802
|
+
empty.textContent = cmText("cm_empty", "No open comments. Select text and click the Comment button to start.");
|
|
426
803
|
list.appendChild(empty);
|
|
427
804
|
return;
|
|
428
805
|
}
|
|
@@ -437,7 +814,11 @@ function RTE_Plugin_Comments() {
|
|
|
437
814
|
function renderEntry(entry, host) {
|
|
438
815
|
var wrap = host.createElement("div");
|
|
439
816
|
wrap.className = "rte-comment-item";
|
|
440
|
-
wrap.setAttribute("data-comment-ref", entry.id);
|
|
817
|
+
wrap.setAttribute("data-comment-ref", entry.id);
|
|
818
|
+
if (entry.id === activeCommentId && Date.now() < activeCommentUntil) {
|
|
819
|
+
wrap.classList.add("rte-comment-item-active");
|
|
820
|
+
setTimeout(function () { if (activeCommentId !== entry.id || Date.now() >= activeCommentUntil - 20) wrap.classList.remove("rte-comment-item-active"); }, Math.max(0, activeCommentUntil - Date.now()));
|
|
821
|
+
}
|
|
441
822
|
wrap.setAttribute("role", "article");
|
|
442
823
|
|
|
443
824
|
var topRow = host.createElement("div");
|
|
@@ -464,15 +845,15 @@ function RTE_Plugin_Comments() {
|
|
|
464
845
|
var resolveBtn = host.createElement("button");
|
|
465
846
|
resolveBtn.type = "button";
|
|
466
847
|
resolveBtn.className = "rte-comment-action rte-comment-resolve";
|
|
467
|
-
resolveBtn.textContent = "Resolve";
|
|
468
|
-
resolveBtn.setAttribute("aria-label", "Resolve comment");
|
|
469
|
-
resolveBtn
|
|
848
|
+
resolveBtn.textContent = cmText("cm_resolve", "Resolve");
|
|
849
|
+
resolveBtn.setAttribute("aria-label", cmText("cm_resolvecomment", "Resolve comment"));
|
|
850
|
+
onActivate(resolveBtn, function () { resolveComment(entry.id); });
|
|
470
851
|
var delBtn = host.createElement("button");
|
|
471
852
|
delBtn.type = "button";
|
|
472
853
|
delBtn.className = "rte-comment-action rte-comment-delete";
|
|
473
|
-
delBtn.textContent = "Delete";
|
|
474
|
-
delBtn.setAttribute("aria-label", "Delete comment");
|
|
475
|
-
delBtn
|
|
854
|
+
delBtn.textContent = cmText("delete", "Delete");
|
|
855
|
+
delBtn.setAttribute("aria-label", cmText("cm_deletecomment", "Delete comment"));
|
|
856
|
+
onActivate(delBtn, function () { deleteComment(entry.id); });
|
|
476
857
|
actions.appendChild(resolveBtn);
|
|
477
858
|
actions.appendChild(delBtn);
|
|
478
859
|
topRow.appendChild(actions);
|
|
@@ -489,8 +870,9 @@ function RTE_Plugin_Comments() {
|
|
|
489
870
|
|
|
490
871
|
var body = host.createElement("div");
|
|
491
872
|
body.className = "rte-comment-body";
|
|
492
|
-
body.
|
|
873
|
+
appendTextWithMentions(host, body, entry.text, entry.mentions);
|
|
493
874
|
wrap.appendChild(body);
|
|
875
|
+
wrap.appendChild(renderReactions(entry, host));
|
|
494
876
|
|
|
495
877
|
var replies = Array.isArray(entry.replies) ? entry.replies : [];
|
|
496
878
|
for (var i = 0; i < replies.length; i++) {
|
|
@@ -505,7 +887,7 @@ function RTE_Plugin_Comments() {
|
|
|
505
887
|
repBody.className = "rte-comment-reply-body";
|
|
506
888
|
var repName = host.createElement("div");
|
|
507
889
|
repName.className = "rte-comment-reply-name";
|
|
508
|
-
repName.textContent = ((rep.author && rep.author.name) || "User") + " \u00B7 " + relativeTime(rep.createdAt);
|
|
890
|
+
repName.textContent = ((rep.author && rep.author.name) || cmText("cm_user", "User")) + " \u00B7 " + relativeTime(rep.createdAt);
|
|
509
891
|
var repText = host.createElement("div");
|
|
510
892
|
repText.className = "rte-comment-reply-text";
|
|
511
893
|
repText.textContent = rep.text;
|
|
@@ -522,8 +904,8 @@ function RTE_Plugin_Comments() {
|
|
|
522
904
|
var replyInput = host.createElement("input");
|
|
523
905
|
replyInput.type = "text";
|
|
524
906
|
replyInput.className = "rte-comment-reply-input";
|
|
525
|
-
replyInput.placeholder = "Reply...";
|
|
526
|
-
replyInput.setAttribute("aria-label", "Reply to comment");
|
|
907
|
+
replyInput.placeholder = cmText("cm_reply", "Reply...");
|
|
908
|
+
replyInput.setAttribute("aria-label", cmText("cm_replytocomment", "Reply to comment"));
|
|
527
909
|
replyInput.addEventListener("keydown", function (e) {
|
|
528
910
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
529
911
|
e.preventDefault();
|
|
@@ -539,11 +921,13 @@ function RTE_Plugin_Comments() {
|
|
|
539
921
|
}
|
|
540
922
|
|
|
541
923
|
function scrollToSpan(commentId) {
|
|
542
|
-
var
|
|
543
|
-
if (!
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
setTimeout(function () {
|
|
924
|
+
var spans = editor.getEditable().querySelectorAll('[data-comment-id="' + cssEscape(commentId) + '"]');
|
|
925
|
+
if (!spans.length) return;
|
|
926
|
+
spans[0].scrollIntoView({ block: "center", behavior: "smooth" });
|
|
927
|
+
for (var i = 0; i < spans.length; i++) spans[i].classList.add("rte-comment-flash");
|
|
928
|
+
setTimeout(function () {
|
|
929
|
+
for (var j = 0; j < spans.length; j++) spans[j].classList.remove("rte-comment-flash");
|
|
930
|
+
}, 1200);
|
|
547
931
|
}
|
|
548
932
|
|
|
549
933
|
function initialsOf(label) {
|
|
@@ -556,10 +940,10 @@ function RTE_Plugin_Comments() {
|
|
|
556
940
|
function relativeTime(ts) {
|
|
557
941
|
if (!ts) return "";
|
|
558
942
|
var diff = Math.floor((Date.now() - ts) / 1000);
|
|
559
|
-
if (diff < 60) return "just now";
|
|
560
|
-
if (diff < 3600) return Math.floor(diff / 60)
|
|
561
|
-
if (diff < 86400) return Math.floor(diff / 3600)
|
|
562
|
-
return Math.floor(diff / 86400)
|
|
943
|
+
if (diff < 60) return cmText("cm_justnow", "just now");
|
|
944
|
+
if (diff < 3600) return cmText("cm_minutesago", "{0}m ago", Math.floor(diff / 60));
|
|
945
|
+
if (diff < 86400) return cmText("cm_hoursago", "{0}h ago", Math.floor(diff / 3600));
|
|
946
|
+
return cmText("cm_daysago", "{0}d ago", Math.floor(diff / 86400));
|
|
563
947
|
}
|
|
564
948
|
|
|
565
949
|
function truncate(s, n) {
|
|
@@ -604,6 +988,11 @@ function RTE_Plugin_Comments() {
|
|
|
604
988
|
".rte-comment-reply-name{font-size:11px;color:#64748b}",
|
|
605
989
|
".rte-comment-reply-text{font-size:12px;color:#172033;line-height:1.45;white-space:pre-wrap;margin-top:2px}",
|
|
606
990
|
".rte-comment-reply-composer{margin-top:8px;display:flex}",
|
|
991
|
+
".rte-comment-reactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}",
|
|
992
|
+
".rte-comment-reaction{display:inline-flex;align-items:center;gap:3px;min-height:24px;padding:1px 7px;border-radius:12px;border:1px solid rgba(148,163,184,.28);background:#fff;font:inherit;font-size:12px;line-height:1;color:#334155;cursor:pointer;opacity:.6}",
|
|
993
|
+
".rte-comment-reaction:hover,.rte-comment-reaction:focus-visible{opacity:1;border-color:rgba(37,99,235,.38)}",
|
|
994
|
+
".rte-comment-reaction:focus-visible{outline:2px solid rgba(37,99,235,.45);outline-offset:1px}",
|
|
995
|
+
".rte-comment-reaction[aria-pressed=true]{opacity:1;background:#eef4ff;border-color:rgba(37,99,235,.45);color:#0f3f9f}",
|
|
607
996
|
".rte-comment-reply-input{flex:1;min-width:0;padding:7px 10px;border-radius:7px;border:1px solid rgba(148,163,184,.24);font:inherit;font-size:12px;background:#fff}",
|
|
608
997
|
".rte-comment-reply-input:focus,.rte-comment-composer-textarea:focus{outline:2px solid rgba(37,99,235,.18);border-color:rgba(37,99,235,.38)}",
|
|
609
998
|
".rte-comment-composer{position:absolute;z-index:2147483000;width:min(320px,calc(100vw - 24px));background:linear-gradient(180deg,#fff,#f8fbff);border:1px solid rgba(148,163,184,.22);box-shadow:0 14px 32px rgba(29,78,216,.12),0 2px 8px rgba(15,23,42,.08);border-radius:8px;padding:12px;font-family:Aptos,'Segoe UI',sans-serif;color:#172033}",
|
|
@@ -624,7 +1013,17 @@ function RTE_Plugin_Comments() {
|
|
|
624
1013
|
".rte-comment-dark .rte-comment-body,.rte-comment-dark .rte-comment-reply-text,.rte-comment-dark .rte-comment-composer-header{color:#e2e8f0}",
|
|
625
1014
|
".rte-comment-dark .rte-comment-reply-input,.rte-comment-dark .rte-comment-composer-textarea{background:#0f172a;color:#e2e8f0;border-color:#334155}",
|
|
626
1015
|
".rte-comment-dark .rte-comment-action,.rte-comment-dark .rte-comment-btn-ghost,.rte-comment-dark .rte-comment-sidebar-close{background:#0f172a;color:#cbd5e1;border-color:#334155}",
|
|
627
|
-
".rte-comment-dark .rte-comment-quote{background:rgba(245,158,11,.12);color:#fcd34d}"
|
|
1016
|
+
".rte-comment-dark .rte-comment-quote{background:rgba(245,158,11,.12);color:#fcd34d}",
|
|
1017
|
+
".rte-comment-dark .rte-comment-reaction{background:#0f172a;color:#cbd5e1;border-color:#334155}",
|
|
1018
|
+
".rte-comment-dark .rte-comment-reaction[aria-pressed=true]{background:#1e3a8a;color:#e2e8f0;border-color:#3b82f6}",
|
|
1019
|
+
".rte-comment-mentions{margin-top:4px;max-height:160px;overflow:auto;border:1px solid rgba(148,163,184,.3);border-radius:7px;background:#fff;box-shadow:0 6px 18px rgba(15,23,42,.12)}",
|
|
1020
|
+
".rte-comment-mention-option{padding:6px 10px;font-size:13px;color:#172033;cursor:pointer}",
|
|
1021
|
+
".rte-comment-mention-option.rte-comment-mention-active{background:#dbeafe}",
|
|
1022
|
+
".rte-comment-mention{font-weight:600;color:#1d4ed8;background:rgba(37,99,235,.1);border-radius:4px;padding:0 2px}",
|
|
1023
|
+
".rte-comment-dark .rte-comment-mentions{background:#0f172a;border-color:#334155}",
|
|
1024
|
+
".rte-comment-dark .rte-comment-mention-option{color:#e2e8f0}",
|
|
1025
|
+
".rte-comment-dark .rte-comment-mention-option.rte-comment-mention-active{background:#1e3a8a}",
|
|
1026
|
+
".rte-comment-dark .rte-comment-mention{color:#93c5fd;background:rgba(147,197,253,.14)}"
|
|
628
1027
|
].join("\n");
|
|
629
1028
|
host.head.appendChild(style);
|
|
630
1029
|
// A <style> ELEMENT is governed by style-src: under a strict policy the browser
|