2020-04-20 10:07:10 -04:00
|
|
|
import showModal from "discourse/lib/show-modal";
|
|
|
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
2022-06-18 15:27:31 -04:00
|
|
|
import { debounce, later } from "@ember/runloop";
|
2021-10-28 13:40:25 -04:00
|
|
|
import cookie, { removeCookie } from "discourse/lib/cookie";
|
2020-04-20 10:07:10 -04:00
|
|
|
|
2020-04-20 11:06:49 -04:00
|
|
|
const VALID_TAGS =
|
2020-04-24 05:19:36 -04:00
|
|
|
"h1, h2, h3, h4, h5, h6, p, code, blockquote, .md-table, li p";
|
2020-04-20 10:07:10 -04:00
|
|
|
const DELIMITER = "=";
|
2022-01-13 07:52:47 -05:00
|
|
|
const EXPIRE_AFTER_DAYS = 7;
|
|
|
|
const EXPIRE_AFTER_SECONDS = EXPIRE_AFTER_DAYS * 24 * 60 * 60;
|
|
|
|
const STORAGE_PREFIX = "d-placeholder-";
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
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",
|
2022-01-13 07:43:57 -05:00
|
|
|
description: placeholder.description,
|
2020-04-20 10:07:10 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-01-13 07:43:57 -05:00
|
|
|
placeholder.defaults.forEach((value) =>
|
2020-04-20 10:07:10 -04:00
|
|
|
addSelectOption(select, {
|
|
|
|
value,
|
2022-01-13 07:43:57 -05:00
|
|
|
selected: placeholder.default === value,
|
2020-04-20 10:07:10 -04:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
return select;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default {
|
|
|
|
name: "discourse-placeholder-theme-component",
|
|
|
|
|
2022-01-13 07:52:47 -05:00
|
|
|
// TODO: Remove once this change has been live for a few months
|
|
|
|
migrateCookiesToKeyValueStore() {
|
|
|
|
const cookies = document.cookie.split("; ");
|
|
|
|
const oldPlaceholderCookies = [];
|
|
|
|
|
|
|
|
for (let i = 0, l = cookies.length; i < l; i++) {
|
|
|
|
let parts = cookies[i].split("=");
|
|
|
|
if (parts[0].startsWith(STORAGE_PREFIX)) {
|
|
|
|
oldPlaceholderCookies.push(parts[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key of oldPlaceholderCookies) {
|
|
|
|
const value = cookie(key);
|
|
|
|
|
|
|
|
this.setValue(key, value);
|
|
|
|
removeCookie(key);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
expireOldValues() {
|
|
|
|
const now = Date.now();
|
|
|
|
Object.keys(window.localStorage)
|
|
|
|
.filter((k) => k.startsWith(STORAGE_PREFIX))
|
|
|
|
.forEach((k) => {
|
|
|
|
const data = this.keyValueStore.getObject(k);
|
|
|
|
if (!data?.expires || data.expires < now) {
|
|
|
|
this.removeValue(k);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
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,
|
2022-06-18 15:27:31 -04:00
|
|
|
value,
|
2022-01-13 07:52:47 -05:00
|
|
|
},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
removeValue(key) {
|
|
|
|
this.keyValueStore.remove(`${STORAGE_PREFIX}${key}`);
|
|
|
|
},
|
|
|
|
|
|
|
|
initialize(container) {
|
|
|
|
this.keyValueStore = container.lookup("key-value-store:main");
|
|
|
|
|
|
|
|
this.migrateCookiesToKeyValueStore();
|
|
|
|
this.expireOldValues();
|
|
|
|
|
2022-01-13 07:43:57 -05:00
|
|
|
withPluginApi("0.8.7", (api) => {
|
2022-01-13 07:42:57 -05:00
|
|
|
api.decorateCookedElement(
|
|
|
|
(cooked, postWidget) => {
|
2022-06-18 15:27:31 -04:00
|
|
|
if (!postWidget) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-20 10:07:10 -04:00
|
|
|
|
2022-01-13 07:52:47 -05:00
|
|
|
const postIdentifier = `${postWidget.widget.attrs.topicId}-${postWidget.widget.attrs.id}-`;
|
2020-04-20 10:07:10 -04:00
|
|
|
const mappings = [];
|
|
|
|
const placeholders = {};
|
|
|
|
|
2022-01-13 07:52:47 -05:00
|
|
|
const processChange = (inputEvent) => {
|
2020-04-20 10:07:10 -04:00
|
|
|
const value = inputEvent.target.value;
|
|
|
|
const key = inputEvent.target.dataset.key;
|
2020-04-20 14:39:54 -04:00
|
|
|
const placeholder = placeholders[inputEvent.target.dataset.key];
|
2020-04-20 10:07:10 -04:00
|
|
|
const placeholderIdentifier = `${postIdentifier}${key}`;
|
|
|
|
|
|
|
|
if (value) {
|
2020-04-20 14:39:54 -04:00
|
|
|
if (value !== placeholder.default) {
|
2022-01-13 07:52:47 -05:00
|
|
|
this.setValue(placeholderIdentifier, value);
|
2020-04-20 14:39:54 -04:00
|
|
|
}
|
2020-04-20 10:07:10 -04:00
|
|
|
} else {
|
2022-01-13 07:52:47 -05:00
|
|
|
this.removeValue(placeholderIdentifier);
|
2020-04-20 10:07:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
let newValue;
|
|
|
|
if (value && value.length && value !== "none") {
|
|
|
|
newValue = value;
|
|
|
|
} else {
|
2020-04-20 14:39:54 -04:00
|
|
|
newValue = `${placeholder.delimiter}${key}${placeholder.delimiter}`;
|
2020-04-20 10:07:10 -04:00
|
|
|
}
|
|
|
|
|
2022-01-13 07:42:57 -05:00
|
|
|
cooked.querySelectorAll(VALID_TAGS).forEach((elem, index) => {
|
2020-04-20 10:07:10 -04:00
|
|
|
const mapping = mappings[index];
|
|
|
|
|
2022-06-18 15:27:31 -04:00
|
|
|
if (!mapping) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
let diff = 0;
|
|
|
|
let replaced = false;
|
2022-08-13 16:39:58 -04:00
|
|
|
let newInnerHTML = elem.innerHTML;
|
2020-04-20 10:07:10 -04:00
|
|
|
|
2022-01-13 07:43:57 -05:00
|
|
|
mapping.forEach((m) => {
|
2020-04-20 14:39:54 -04:00
|
|
|
if (
|
|
|
|
m.pattern !==
|
|
|
|
`${placeholder.delimiter}${key}${placeholder.delimiter}`
|
|
|
|
) {
|
2020-04-20 10:07:10 -04:00
|
|
|
m.position = m.position + diff;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
replaced = true;
|
|
|
|
|
|
|
|
const previousLength = m.length;
|
2022-08-13 16:39:58 -04:00
|
|
|
const prefix = newInnerHTML.slice(0, m.position + diff);
|
|
|
|
const suffix = newInnerHTML.slice(
|
2020-04-20 10:07:10 -04:00
|
|
|
m.position + diff + m.length,
|
2022-08-13 16:39:58 -04:00
|
|
|
newInnerHTML.length
|
2020-04-20 10:07:10 -04:00
|
|
|
);
|
2022-08-13 16:39:58 -04:00
|
|
|
newInnerHTML = `${prefix}${newValue}${suffix}`;
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
m.length = newValue.length;
|
|
|
|
m.position = m.position + diff;
|
|
|
|
diff = diff + newValue.length - previousLength;
|
|
|
|
});
|
|
|
|
|
2022-06-18 15:27:31 -04:00
|
|
|
if (replaced) {
|
2022-08-13 16:39:58 -04:00
|
|
|
elem.innerHTML = newInnerHTML;
|
2022-06-18 15:27:31 -04:00
|
|
|
}
|
2020-04-20 10:07:10 -04:00
|
|
|
});
|
2022-01-13 07:52:47 -05:00
|
|
|
};
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
function processPlaceholders() {
|
|
|
|
mappings.length = 0;
|
|
|
|
|
|
|
|
const keys = Object.keys(placeholders);
|
|
|
|
const pattern = keys
|
2022-01-13 07:43:57 -05:00
|
|
|
.map((key) => {
|
2020-04-20 10:07:10 -04:00
|
|
|
const placeholder = placeholders[key];
|
|
|
|
return `(${placeholder.delimiter}${key}${placeholder.delimiter})`;
|
|
|
|
})
|
|
|
|
.join("|");
|
|
|
|
const regex = new RegExp(pattern, "g");
|
|
|
|
|
2022-01-13 07:42:57 -05:00
|
|
|
cooked.querySelectorAll(VALID_TAGS).forEach((elem, index) => {
|
2020-04-20 10:07:10 -04:00
|
|
|
let match;
|
|
|
|
|
|
|
|
mappings[index] = mappings[index] || [];
|
|
|
|
|
|
|
|
while ((match = regex.exec(elem.innerHTML)) != null) {
|
|
|
|
mappings[index].push({
|
|
|
|
pattern: match[0],
|
|
|
|
position: match.index,
|
2022-01-13 07:43:57 -05:00
|
|
|
length: match[0].length,
|
2020-04-20 10:07:10 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-01-13 07:52:47 -05:00
|
|
|
const _fillPlaceholders = () => {
|
2020-04-20 10:07:10 -04:00
|
|
|
if (Object.keys(placeholders).length > 0) {
|
2022-01-13 07:42:57 -05:00
|
|
|
processPlaceholders(placeholders, cooked, mappings);
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
// trigger fake event to setup initial state
|
2022-01-13 07:43:57 -05:00
|
|
|
Object.keys(placeholders).forEach((placeholderKey) => {
|
2020-04-20 10:07:10 -04:00
|
|
|
const placeholder = placeholders[placeholderKey];
|
|
|
|
const placeholderIdentifier = `${postIdentifier}${placeholderKey}`;
|
2020-04-20 14:39:54 -04:00
|
|
|
const value =
|
2022-01-13 07:52:47 -05:00
|
|
|
this.getValue(placeholderIdentifier) || placeholder.default;
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
processChange({
|
|
|
|
target: {
|
|
|
|
value,
|
|
|
|
dataset: {
|
|
|
|
key: placeholderKey,
|
2022-01-13 07:43:57 -05:00
|
|
|
delimiter: placeholder.delimiter,
|
|
|
|
},
|
|
|
|
},
|
2020-04-20 10:07:10 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2022-01-13 07:52:47 -05:00
|
|
|
};
|
2020-04-20 10:07:10 -04:00
|
|
|
|
2022-01-13 07:42:57 -05:00
|
|
|
const placeholderNodes = cooked.querySelectorAll(
|
2020-04-20 10:07:10 -04:00
|
|
|
".d-wrap[data-wrap=placeholder]:not(.placeholdered)"
|
|
|
|
);
|
|
|
|
|
2022-01-13 07:43:57 -05:00
|
|
|
placeholderNodes.forEach((elem) => {
|
2020-04-20 10:07:10 -04:00
|
|
|
const dataKey = elem.dataset.key;
|
|
|
|
|
2022-06-18 15:27:31 -04:00
|
|
|
if (!dataKey) {
|
|
|
|
return;
|
|
|
|
}
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
const placeholderIdentifier = `${postIdentifier}${dataKey}`;
|
2022-01-13 07:52:47 -05:00
|
|
|
const valueFromStore = this.getValue(placeholderIdentifier);
|
2020-04-20 10:07:10 -04:00
|
|
|
const defaultValues = (elem.dataset.defaults || "")
|
|
|
|
.split(",")
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
placeholders[dataKey] = {
|
2022-01-13 07:52:47 -05:00
|
|
|
default: valueFromStore || elem.dataset.default,
|
2020-04-20 10:07:10 -04:00
|
|
|
defaults: defaultValues,
|
|
|
|
delimiter: elem.dataset.delimiter || DELIMITER,
|
2022-01-13 07:43:57 -05:00
|
|
|
description: elem.dataset.description,
|
2020-04-20 10:07:10 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-01-13 07:42:57 -05:00
|
|
|
cooked
|
|
|
|
.querySelectorAll(".discourse-placeholder-value")
|
|
|
|
.forEach((el) => {
|
|
|
|
el.addEventListener("input", (inputEvent) =>
|
|
|
|
debounce(this, processChange, inputEvent, 150)
|
|
|
|
);
|
2022-01-13 19:48:07 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
cooked
|
|
|
|
.querySelectorAll(".discourse-placeholder-select")
|
|
|
|
.forEach((el) => {
|
2022-01-13 07:42:57 -05:00
|
|
|
el.addEventListener("change", (inputEvent) =>
|
|
|
|
debounce(this, processChange, inputEvent, 150)
|
|
|
|
);
|
|
|
|
});
|
2020-04-20 10:07:10 -04:00
|
|
|
|
|
|
|
later(_fillPlaceholders, 500);
|
|
|
|
},
|
|
|
|
{ onlyStream: true, id: "discourse-placeholder-theme-component" }
|
|
|
|
);
|
|
|
|
|
|
|
|
api.addToolbarPopupMenuOptionsCallback(() => {
|
|
|
|
return {
|
|
|
|
action: "insertPlaceholder",
|
|
|
|
icon: "file",
|
2022-01-13 07:43:57 -05:00
|
|
|
label: themePrefix("toolbar.builder"),
|
2020-04-20 10:07:10 -04:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
api.modifyClass("controller:composer", {
|
2021-09-03 13:43:45 -04:00
|
|
|
pluginId: "discourse-placeholder-theme-component",
|
|
|
|
|
2020-04-20 10:07:10 -04:00
|
|
|
actions: {
|
|
|
|
insertPlaceholder() {
|
|
|
|
showModal("discourse-placeholder-builder", {
|
|
|
|
model: {
|
2022-01-13 07:43:57 -05:00
|
|
|
toolbarEvent: this.toolbarEvent,
|
|
|
|
},
|
2020-04-20 10:07:10 -04:00
|
|
|
});
|
2022-01-13 07:43:57 -05:00
|
|
|
},
|
|
|
|
},
|
2020-04-20 10:07:10 -04:00
|
|
|
});
|
|
|
|
});
|
2022-01-13 07:43:57 -05:00
|
|
|
},
|
2020-04-20 10:07:10 -04:00
|
|
|
};
|