mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-23 00:24:19 +08:00
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.
181 lines
5.1 KiB
JavaScript
Vendored
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", "​"); // 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 - ""
|
|
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));
|
|
},
|
|
};
|