mirror of
https://github.com/discourse/discourse-placeholder-theme-component.git
synced 2026-09-18 16:11:36 -04:00
This selector was too specific and would end up catching a directlyt wich would result in innerHTML being the text of the link and not the full link.
261 lines
8.0 KiB
JavaScript
261 lines
8.0 KiB
JavaScript
import showModal from "discourse/lib/show-modal";
|
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
|
import { later, debounce } from "@ember/runloop";
|
|
|
|
const VALID_TAGS =
|
|
"h1, h2, h3, h4, h5, h6, p, code, blockquote, .md-table, li p";
|
|
const DELIMITER = "=";
|
|
|
|
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.default) {
|
|
input.value = placeholder.default;
|
|
}
|
|
|
|
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.default === value
|
|
})
|
|
);
|
|
|
|
return select;
|
|
}
|
|
|
|
export default {
|
|
name: "discourse-placeholder-theme-component",
|
|
|
|
initialize() {
|
|
withPluginApi("0.8.7", api => {
|
|
api.decorateCooked(
|
|
($cooked, postWidget) => {
|
|
if (!postWidget) return;
|
|
|
|
const postIdentifier = `d-placeholder-${postWidget.widget.attrs.topicId}-${postWidget.widget.attrs.id}-`;
|
|
const mappings = [];
|
|
const placeholders = {};
|
|
|
|
function 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) {
|
|
if (value !== placeholder.default) {
|
|
$.cookie(placeholderIdentifier, value);
|
|
}
|
|
} else {
|
|
$.removeCookie(placeholderIdentifier);
|
|
}
|
|
|
|
let newValue;
|
|
if (value && value.length && value !== "none") {
|
|
newValue = value;
|
|
} else {
|
|
newValue = `${placeholder.delimiter}${key}${placeholder.delimiter}`;
|
|
}
|
|
|
|
$cooked.find(VALID_TAGS).each((index, elem) => {
|
|
const mapping = mappings[index];
|
|
|
|
if (!mapping) return;
|
|
|
|
let diff = 0;
|
|
let replaced = false;
|
|
let newInnnerHTML = elem.innerHTML;
|
|
|
|
mapping.forEach(m => {
|
|
if (
|
|
m.pattern !==
|
|
`${placeholder.delimiter}${key}${placeholder.delimiter}`
|
|
) {
|
|
m.position = m.position + diff;
|
|
return;
|
|
}
|
|
|
|
replaced = true;
|
|
|
|
const previousLength = m.length;
|
|
const prefix = newInnnerHTML.slice(0, m.position + diff);
|
|
const suffix = newInnnerHTML.slice(
|
|
m.position + diff + m.length,
|
|
newInnnerHTML.length
|
|
);
|
|
newInnnerHTML = `${prefix}${newValue}${suffix}`;
|
|
|
|
m.length = newValue.length;
|
|
m.position = m.position + diff;
|
|
diff = diff + newValue.length - previousLength;
|
|
});
|
|
|
|
if (replaced) elem.innerHTML = newInnnerHTML;
|
|
});
|
|
}
|
|
|
|
function processPlaceholders() {
|
|
mappings.length = 0;
|
|
|
|
const keys = Object.keys(placeholders);
|
|
const pattern = keys
|
|
.map(key => {
|
|
const placeholder = placeholders[key];
|
|
return `(${placeholder.delimiter}${key}${placeholder.delimiter})`;
|
|
})
|
|
.join("|");
|
|
const regex = new RegExp(pattern, "g");
|
|
|
|
$cooked.find(VALID_TAGS).each((index, elem) => {
|
|
let match;
|
|
|
|
mappings[index] = mappings[index] || [];
|
|
|
|
while ((match = regex.exec(elem.innerHTML)) != null) {
|
|
mappings[index].push({
|
|
pattern: match[0],
|
|
position: match.index,
|
|
length: match[0].length
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function _fillPlaceholders() {
|
|
if (Object.keys(placeholders).length > 0) {
|
|
processPlaceholders(placeholders, $cooked, mappings);
|
|
|
|
// trigger fake event to setup initial state
|
|
Object.keys(placeholders).forEach(placeholderKey => {
|
|
const placeholder = placeholders[placeholderKey];
|
|
const placeholderIdentifier = `${postIdentifier}${placeholderKey}`;
|
|
const value =
|
|
$.cookie(placeholderIdentifier) || placeholder.default;
|
|
|
|
processChange({
|
|
target: {
|
|
value,
|
|
dataset: {
|
|
key: placeholderKey,
|
|
delimiter: placeholder.delimiter
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
const placeholderNodes = $cooked[0].querySelectorAll(
|
|
".d-wrap[data-wrap=placeholder]:not(.placeholdered)"
|
|
);
|
|
|
|
placeholderNodes.forEach(elem => {
|
|
const dataKey = elem.dataset.key;
|
|
|
|
if (!dataKey) return;
|
|
|
|
const placeholderIdentifier = `${postIdentifier}${dataKey}`;
|
|
const valueFromCookie = $.cookie(placeholderIdentifier);
|
|
const defaultValues = (elem.dataset.defaults || "")
|
|
.split(",")
|
|
.filter(Boolean);
|
|
|
|
placeholders[dataKey] = {
|
|
default: valueFromCookie || elem.dataset.default,
|
|
defaults: defaultValues,
|
|
delimiter: elem.dataset.delimiter || DELIMITER,
|
|
description: elem.dataset.description
|
|
};
|
|
|
|
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
|
|
.on("input", ".discourse-placeholder-value", inputEvent =>
|
|
debounce(this, processChange, inputEvent, 150)
|
|
)
|
|
.on("change", ".discourse-placeholder-select", inputEvent =>
|
|
debounce(this, processChange, inputEvent, 150)
|
|
);
|
|
|
|
later(_fillPlaceholders, 500);
|
|
},
|
|
{ onlyStream: true, id: "discourse-placeholder-theme-component" }
|
|
);
|
|
|
|
api.addToolbarPopupMenuOptionsCallback(() => {
|
|
return {
|
|
action: "insertPlaceholder",
|
|
icon: "file",
|
|
label: themePrefix("toolbar.builder")
|
|
};
|
|
});
|
|
|
|
api.modifyClass("controller:composer", {
|
|
actions: {
|
|
insertPlaceholder() {
|
|
showModal("discourse-placeholder-builder", {
|
|
model: {
|
|
toolbarEvent: this.toolbarEvent
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
};
|