discourse-placeholder-theme.../javascripts/discourse/initializers/setup.js

272 lines
8.0 KiB
JavaScript

import { debounce, later } from "@ember/runloop";
import { withPluginApi } from "discourse/lib/plugin-api";
import DiscoursePlaceholderBuilder from "../components/modal/discourse-placeholder-builder";
const VALID_TAGS =
"h1, h2, h3, h4, h5, h6, p, code, blockquote, .md-table, li p";
const DELIMITER = "=";
const EXPIRE_AFTER_DAYS = 7;
const EXPIRE_AFTER_SECONDS = EXPIRE_AFTER_DAYS * 24 * 60 * 60;
const STORAGE_PREFIX = "d-placeholder-";
const originalContentMap = new WeakMap();
function buildInput(key, placeholder) {
const input = document.createElement("input");
input.classList.add("discourse-placeholder-value");
input.dataset.key = key;
input.dataset.delimiter = placeholder.delimiter;
if (placeholder.description) {
input.setAttribute("placeholder", placeholder.description);
}
if (placeholder.value) {
input.value = placeholder.value;
}
return input;
}
function addSelectOption(select, options = {}) {
const option = document.createElement("option");
option.classList.add("discourse-placeholder-option");
option.value = options.value;
option.text = options.description || options.value;
if (options.selected) {
option.setAttribute("selected", true);
}
select.appendChild(option);
}
function buildSelect(key, placeholder) {
const select = document.createElement("select");
select.classList.add("discourse-placeholder-select");
select.dataset.key = key;
select.dataset.delimiter = placeholder.delimiter;
if (placeholder.description) {
addSelectOption(select, {
value: "none",
description: placeholder.description,
});
}
placeholder.defaults.forEach((value) =>
addSelectOption(select, {
value,
selected: placeholder.value === value,
})
);
return select;
}
function replaceInText(text, placeholders) {
for (const [key, { delimiter, value }] of Object.entries(placeholders)) {
const placeholderWithDelimiter = `${delimiter}${key}${delimiter}`;
let substitution = value;
if (!substitution?.length || substitution === "none") {
substitution = placeholderWithDelimiter;
}
text = text.replaceAll(placeholderWithDelimiter, substitution);
}
return text;
}
function performReplacements(cooked, placeholders) {
cooked.querySelectorAll(VALID_TAGS).forEach((elem) => {
const textNodeWalker = document.createTreeWalker(
elem,
NodeFilter.SHOW_TEXT
);
// Handle text nodes
while (textNodeWalker.nextNode()) {
const node = textNodeWalker.currentNode;
if (!originalContentMap.has(node)) {
// Haven't seen this node before. Get the text, and store it for future transformations
originalContentMap.set(node, node.data);
}
const originalText = originalContentMap.get(node);
const text = replaceInText(originalText, placeholders);
if (node.data !== text) {
node.data = text;
}
}
// Handle a[href] attributes
cooked.querySelectorAll("a[href]").forEach((link) => {
const hrefAttr = link.attributes.getNamedItem("href");
if (!originalContentMap.has(hrefAttr)) {
// Haven't seen this attr before. Get the text, and store it for future transformations
originalContentMap.set(hrefAttr, hrefAttr.value);
}
const originalUrl = originalContentMap.get(hrefAttr);
const newUrl = replaceInText(originalUrl, placeholders);
if (hrefAttr.value !== newUrl) {
hrefAttr.value = newUrl;
}
});
});
}
export default {
name: "discourse-placeholder-theme-component",
expireOldValues() {
const now = Date.now();
this.keyValueStore.removeKeys?.((k, v) => {
if (!k.includes(STORAGE_PREFIX)) {
return false;
}
return !v?.expires || v.expires < now;
});
},
getValue(key) {
const data = this.keyValueStore.getObject(`${STORAGE_PREFIX}${key}`);
if (data) {
data.expires = Date.now() + EXPIRE_AFTER_SECONDS;
this.keyValueStore.setObject(`${STORAGE_PREFIX}${key}`, data);
return data.value;
}
},
setValue(key, value) {
this.keyValueStore.setObject({
key: `${STORAGE_PREFIX}${key}`,
value: {
expires: Date.now() + EXPIRE_AFTER_SECONDS,
value,
},
});
},
removeValue(key) {
this.keyValueStore.remove(`${STORAGE_PREFIX}${key}`);
},
initialize(container) {
this.keyValueStore = container.lookup("service:key-value-store");
this.expireOldValues();
withPluginApi("0.8.7", (api) => {
api.decorateCookedElement(
(cooked, postWidget) => {
if (!postWidget) {
return;
}
const postIdentifier = `${postWidget.widget.attrs.topicId}-${postWidget.widget.attrs.id}-`;
const placeholders = {};
const processChange = (inputEvent) => {
const value = inputEvent.target.value;
const key = inputEvent.target.dataset.key;
const placeholder = placeholders[inputEvent.target.dataset.key];
const placeholderIdentifier = `${postIdentifier}${key}`;
if (value && value !== placeholder.default) {
placeholder.value = value;
this.setValue(placeholderIdentifier, value);
} else {
placeholder.value = placeholder.default;
this.removeValue(placeholderIdentifier);
}
performReplacements(cooked, placeholders);
};
const placeholderNodes = cooked.querySelectorAll(
".d-wrap[data-wrap=placeholder]:not(.placeholdered)"
);
placeholderNodes.forEach((elem) => {
const dataKey = elem.dataset.key;
if (!dataKey) {
return;
}
const placeholderIdentifier = `${postIdentifier}${dataKey}`;
const valueFromStore = this.getValue(placeholderIdentifier);
const defaultValues = (elem.dataset.defaults || "")
.split(",")
.filter(Boolean);
placeholders[dataKey] = {
default: elem.dataset.default,
defaults: defaultValues,
delimiter: elem.dataset.delimiter || DELIMITER,
description: elem.dataset.description,
value: valueFromStore || elem.dataset.default,
};
const span = document.createElement("span");
span.classList.add("discourse-placeholder-name", "placeholdered");
span.innerText = dataKey;
// content has been set inside the [wrap][/wrap] block
if (elem.querySelector("p")) {
elem.querySelector("p").prepend(span);
} else {
elem.prepend(span);
}
if (defaultValues && defaultValues.length) {
const select = buildSelect(dataKey, placeholders[dataKey]);
elem.appendChild(select);
} else {
const input = buildInput(dataKey, placeholders[dataKey]);
elem.appendChild(input);
}
});
cooked
.querySelectorAll(".discourse-placeholder-value")
.forEach((el) => {
el.addEventListener("input", (inputEvent) =>
debounce(this, processChange, inputEvent, 150)
);
});
cooked
.querySelectorAll(".discourse-placeholder-select")
.forEach((el) => {
el.addEventListener("change", (inputEvent) =>
debounce(this, processChange, inputEvent, 150)
);
});
later(performReplacements, cooked, placeholders, 500);
},
{ onlyStream: true, id: "discourse-placeholder-theme-component" }
);
api.addComposerToolbarPopupMenuOption({
label: themePrefix("toolbar.builder"),
icon: "file",
action: (toolbarEvent) => {
const modal = container.lookup("service:modal");
modal.show(DiscoursePlaceholderBuilder, {
model: {
toolbarEvent,
},
});
},
});
});
},
};