2023-08-02 10:17:24 +01:00
|
|
|
import { ajax } from "discourse/lib/ajax";
|
2023-10-10 19:38:59 +01:00
|
|
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
2023-08-02 10:17:24 +01:00
|
|
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
|
|
|
|
import I18n from "I18n";
|
|
|
|
|
|
|
|
|
|
function initializePlugin(api) {
|
|
|
|
|
const siteSettings = api.container.lookup("site-settings:main");
|
|
|
|
|
|
|
|
|
|
if (siteSettings.checklist_enabled) {
|
2023-09-12 16:32:04 +01:00
|
|
|
api.decorateCookedElement(checklistSyntax);
|
2023-08-02 10:17:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeReadonlyClass(boxes) {
|
|
|
|
|
boxes.forEach((e) => e.classList.remove("readonly"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2023-08-02 23:24:20 +02:00
|
|
|
|
|
|
|
|
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
|
2023-08-02 10:17:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function checklistSyntax(elem, postDecorator) {
|
|
|
|
|
const boxes = [...elem.getElementsByClassName("chcklst-box")];
|
|
|
|
|
addUlClasses(boxes);
|
|
|
|
|
|
|
|
|
|
if (!postDecorator) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const postWidget = postDecorator.widget;
|
|
|
|
|
const postModel = postDecorator.getModel();
|
|
|
|
|
|
|
|
|
|
if (!postModel.can_edit) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boxes.forEach((val, idx) => {
|
2023-08-02 23:24:20 +02:00
|
|
|
val.onclick = async (event) => {
|
|
|
|
|
const box = event.currentTarget;
|
2023-08-02 10:17:24 +01:00
|
|
|
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" });
|
|
|
|
|
box.insertAdjacentElement("afterend", template.content.firstChild);
|
|
|
|
|
box.classList.add("hidden");
|
|
|
|
|
boxes.forEach((e) => e.classList.add("readonly"));
|
|
|
|
|
|
2023-08-02 23:24:20 +02:00
|
|
|
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(
|
|
|
|
|
/\[(\s|\_|\-|\x|\\?\*)?\]/gi,
|
|
|
|
|
(match, ignored, off) => {
|
|
|
|
|
if (found) {
|
|
|
|
|
return match;
|
2023-08-02 10:17:24 +01:00
|
|
|
}
|
|
|
|
|
|
2023-08-02 23:24:20 +02:00
|
|
|
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;
|
2023-08-02 10:17:24 +01:00
|
|
|
}
|
2023-08-02 23:24:20 +02:00
|
|
|
|
|
|
|
|
return match;
|
2023-08-02 10:17:24 +01:00
|
|
|
}
|
2023-08-02 23:24:20 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await postModel.save({
|
|
|
|
|
raw: newRaw,
|
|
|
|
|
edit_reason: I18n.t("checklist.edit_reason"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
postWidget.attrs.isSaving = false;
|
|
|
|
|
postWidget.scheduleRerender();
|
|
|
|
|
} finally {
|
|
|
|
|
removeReadonlyClass(boxes);
|
|
|
|
|
}
|
2023-08-02 10:17:24 +01:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: "checklist",
|
2023-08-02 23:24:20 +02:00
|
|
|
|
|
|
|
|
initialize() {
|
2023-08-02 10:17:24 +01:00
|
|
|
withPluginApi("0.1", (api) => initializePlugin(api));
|
|
|
|
|
},
|
|
|
|
|
};
|