discourse/plugins/checklist/assets/javascripts/discourse/initializers/checklist.js
Sérgio Saquetim a80017ff6a
DEV: add fallback for calling helper.widget decorating cooked content when using the Glimmer Post Stream (#32585)
This commit adds a basic fallback to prevent errors for extensions accessing
`helper.widget` when using `api.decorateCookedElement`.

The purpose of the fallback is provide access to `.widget.attrs`and
`scheduleRerender()` which are common scenarios.

Since the call to the callback happens only when rendering the auto mode
of the Glimmer Post Stream can't detect the existence of these
customizations. The PR also adds a deprecation notice in the console.
2025-05-06 14:13:45 -03:00

181 lines
5.1 KiB
JavaScript
Vendored

import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { getOwnerWithFallback } from "discourse/lib/get-owner";
import { iconHTML } from "discourse/lib/icon-library";
import { withPluginApi } from "discourse/lib/plugin-api";
import { i18n } from "discourse-i18n";
import richEditorExtension from "../../lib/rich-editor-extension";
function initializePlugin(api) {
const siteSettings = api.container.lookup("service:site-settings");
if (siteSettings.checklist_enabled) {
api.decorateCookedElement(checklistSyntax);
api.registerRichEditorExtension(richEditorExtension);
}
}
function isWhitespaceNode(node) {
return node.nodeType === 3 && node.nodeValue.match(/^\s*$/);
}
function hasPrecedingContent(node) {
let sibling = node.previousSibling;
while (sibling) {
if (!isWhitespaceNode(sibling)) {
return true;
}
sibling = sibling.previousSibling;
}
return false;
}
function addUlClasses(boxes) {
boxes.forEach((val) => {
let parent = val.parentElement;
if (
parent.nodeName === "P" &&
parent.parentElement.firstElementChild === parent
) {
parent = parent.parentElement;
}
if (
parent.nodeName === "LI" &&
parent.parentElement.nodeName === "UL" &&
!hasPrecedingContent(val)
) {
parent.classList.add("has-checkbox");
val.classList.add("list-item-checkbox");
if (!val.nextSibling) {
val.insertAdjacentHTML("afterend", "&#8203;"); // Ensure otherwise empty <li> does not collapse height
}
}
});
}
export function checklistSyntax(elem, postDecorator) {
const boxes = [...elem.getElementsByClassName("chcklst-box")];
addUlClasses(boxes);
// TODO (glimmer-post-stream): remove this when we remove the legacy post stream code
const postWidget = getOwnerWithFallback(this).lookup("service:site")
.useGlimmerPostStream
? null
: postDecorator?.widget;
const postModel = postDecorator?.getModel();
if (!postModel?.can_edit) {
return;
}
boxes.forEach((val, idx) => {
val.onclick = async (event) => {
const box = event.currentTarget;
const classList = box.classList;
if (classList.contains("permanent") || classList.contains("readonly")) {
return;
}
const newValue = classList.contains("checked") ? "[ ]" : "[x]";
const template = document.createElement("template");
template.innerHTML = iconHTML("spinner", {
class: "fa-spin list-item-checkbox",
});
box.insertAdjacentElement("afterend", template.content.firstChild);
box.classList.add("hidden");
boxes.forEach((e) => e.classList.add("readonly"));
try {
const post = await ajax(`/posts/${postModel.id}`);
const blocks = [];
// Computing offsets where checkbox are not evaluated (i.e. inside
// code blocks).
[
// inline code
/`[^`\n]*\n?[^`\n]*`/gm,
// multi-line code
/^```[^]*?^```/gm,
// bbcode
/\[code\][^]*?\[\/code\]/gm,
// italic/bold
/_(?=\S).*?\S_/gm,
// strikethrough
/~~(?=\S).*?\S~~/gm,
].forEach((regex) => {
let match;
while ((match = regex.exec(post.raw)) != null) {
blocks.push([match.index, match.index + match[0].length]);
}
});
[
// italic/bold
/([^\[\n]|^)\*\S.+?\S\*(?=[^\]\n]|$)/gm,
].forEach((regex) => {
let match;
while ((match = regex.exec(post.raw)) != null) {
// Simulate lookbehind - skip the first character
blocks.push([match.index + 1, match.index + match[0].length]);
}
});
// make the first run go to index = 0
let nth = -1;
let found = false;
const newRaw = post.raw.replace(
/\[( |x)?\]/gi,
(match, ignored, off) => {
if (found) {
return match;
}
// skip empty image URLs - "![](https://example.com/image.jpg)"
if (off > 0 && post.raw[off - 1] === "!") {
return match;
}
nth += blocks.every(
(b) => b[0] >= off + match.length || off > b[1]
);
if (nth === idx) {
found = true; // Do not replace any further matches
return newValue;
}
return match;
}
);
await postModel.save({
raw: newRaw,
edit_reason: i18n("checklist.edit_reason"),
});
// TODO (glimmer-post-stream): remove the following code when removing the legacy post stream code
if (postWidget) {
postWidget.attrs.isSaving = false;
postWidget.scheduleRerender();
}
} catch (e) {
popupAjaxError(e);
} finally {
boxes.forEach((e) => e.classList.remove("readonly"));
box.classList.remove("hidden");
box.parentElement.querySelector(".fa-spin").remove();
}
};
});
}
export default {
name: "checklist",
initialize() {
withPluginApi((api) => initializePlugin(api));
},
};