integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js
import { getCsrfToken } from "../../utils/csrf-token";
(() => {
const tinymceConfig = document.getElementById("tinymce-config-options");
const getCompletions = async (query, id) => {
const url = tinymceConfig.getAttribute("data-link-ajax-url");
const response = await fetch(url, {
method: "POST",
headers: {
"X-CSRFToken": getCsrfToken(),
},
body: JSON.stringify({
query_string: query,
object_types: ["event", "page", "poi"],
archived: false,
}),
});
const HTTP_STATUS_OK = 200;
if (response.status !== HTTP_STATUS_OK) {
return [];
}
const data = await response.json();
return [data.data, id];
};
// Checks if the url is likely missing the https:// prefix and add it if that is the case
const checkUrlHttps = (url) => {
// This regex matches domains without protocol (strings which contain a dot without a preceding colon or slash)
const re = /^[^:/]+[.].+/;
if (re.test(url)) {
return `https://${url}`;
}
return url;
};
const updateLink = (editor, anchorElm, text, linkAttrs) => {
if (text !== null) {
/* eslint-disable-next-line no-param-reassign */
anchorElm.textContent = text;
}
editor.dom.setAttribs(anchorElm, linkAttrs);
editor.selection.select(anchorElm);
};
tinymce.PluginManager.add("custom_link_input", (editor, _url) => {
const isAnchor = (node) => node.nodeName.toLowerCase() === "a" && node.href;
const getAnchor = () => {
let node = editor.selection.getNode();
while (node !== null) {
if (isAnchor(node)) {
return node;
}
node = node.parentNode;
}
return null;
};
const openDialog = () => {
const anchor = getAnchor();
const initialText = anchor ? anchor.textContent : editor.selection.getContent({ format: "text" });
const initialUrl = anchor ? anchor.getAttribute("href") : "";
const initialAutoUpdateValue = anchor
? anchor.getAttribute("data-integreat-auto-update") === "true"
: initialText === "";
let prevAutoupdateValue = initialAutoUpdateValue;
const textDisabled = anchor ? anchor.children.length > 0 : false;
let prevSearchText = "";
let prevLinkUrl = initialUrl;
let prevSelectedCompletion = "";
// Store the custom user data separately, so that they can be restored when required
const userData = { url: "", text: "" };
// Stores the current request id, so that outdated requests get ignored
let ajaxRequestId = 0;
const defaultCompletionItem = {
text: tinymceConfig.getAttribute("data-link-no-results-text"),
title: "",
value: "",
};
const completionItems = [defaultCompletionItem];
let currentCompletionText = "";
const updateDialog = (api) => {
let data = api.getData();
if (data.autoupdate) {
api.disable("text");
if (!prevAutoupdateValue) {
api.setData({
url: "",
text: "",
});
}
} else {
api.enable("text");
}
prevAutoupdateValue = data.autoupdate;
let urlChangedBySearch = false;
// Check if the selected completion changed
if (prevSelectedCompletion !== data.completions) {
// find the correct text currently shown in the completion items box
if (completionItems.length > 0) {
const currentCompletion = completionItems.find(
(completion) => completion.value === data.completions
);
// Don't set the completion text to `- no results -`
if (currentCompletion.value !== "") {
currentCompletionText = currentCompletion.title;
} else {
currentCompletionText = "";
}
} else {
currentCompletionText = "";
}
// Set the url either to the selected internal link or to the user link
if (data.completions !== "") {
urlChangedBySearch = true;
api.setData({ url: data.completions });
// if the link should be automatically updated orthe text is not defined by the user,
// set it to the current completion item
if (data.autoupdate || !data.text || (userData.text !== data.text && !textDisabled)) {
api.setData({ text: currentCompletionText });
}
} else if (!data.autoupdate) {
// restore the original user data
api.setData({
url: userData.url,
text: textDisabled ? "" : userData.text,
});
}
}
prevSelectedCompletion = data.completions;
// Automatically update the text input to the url by default
data = api.getData();
if (!textDisabled && !urlChangedBySearch && data.text === prevLinkUrl) {
api.setData({ text: data.url });
}
prevLinkUrl = data.url;
// Update the user link
if (data.url !== data.completions) {
userData.url = data.url;
}
if (!textDisabled && data.text !== data.url && data.text !== currentCompletionText) {
userData.text = data.text;
}
// Disable the submit button if either one of the url or text are empty
data = api.getData();
if (data.url.trim() && (textDisabled || data.text.trim())) {
api.enable("submit");
} else {
api.disable("submit");
}
// make new ajax request on user input
if (data.search !== prevSearchText && data.search !== "") {
ajaxRequestId += 1;
getCompletions(data.search, ajaxRequestId).then(([newCompletions, requestId]) => {
if (requestId !== ajaxRequestId) {
return;
}
completionItems.length = 0;
for (const completion of newCompletions) {
completionItems.push({
text: completion.path,
title: completion.html_title,
value: completion.url,
});
}
let completionDisabled = false;
if (completionItems.length === 0) {
completionDisabled = true;
completionItems.push(defaultCompletionItem);
}
// It seems like there is no better way to update the completion list
/* eslint-disable-next-line @typescript-eslint/no-use-before-define */
api.redial(dialogConfig);
api.setData(data);
api.focus("search");
prevSearchText = data.search;
if (completionDisabled) {
api.disable("completions");
} else {
api.enable("completions");
}
updateDialog(api);
});
} else if (data.search === "" && prevSearchText !== "") {
// force an update so that the original user url can get restored
completionItems.length = 0;
completionItems.push(defaultCompletionItem);
/* eslint-disable-next-line @typescript-eslint/no-use-before-define */
api.redial(dialogConfig);
api.setData(data);
api.focus("search");
prevSearchText = data.search;
api.disable("completions");
updateDialog(api);
}
};
const dialogConfig = {
title: tinymceConfig.getAttribute("data-link-dialog-title-text"),
body: {
type: "panel",
items: [
{
type: "input",
name: "url",
label: tinymceConfig.getAttribute("data-link-dialog-url-text"),
},
{
type: "input",
name: "text",
label: tinymceConfig.getAttribute("data-link-dialog-text-text"),
disabled: textDisabled,
},
{
type: "label",
label: tinymceConfig.getAttribute("data-link-dialog-internal_link-text"),
items: [
{
type: "input",
name: "search",
},
{
type: "selectbox",
name: "completions",
items: completionItems,
disabled: true,
},
{
type: "checkbox",
name: "autoupdate",
label: tinymceConfig.getAttribute("data-link-dialog-autoupdate-text"),
},
],
},
],
},
buttons: [
{
type: "cancel",
text: tinymceConfig.getAttribute("data-dialog-cancel-text"),
},
{
type: "submit",
name: "submit",
text: tinymceConfig.getAttribute("data-dialog-submit-text"),
primary: true,
disabled: true,
},
],
initialData: {
text: initialText,
url: initialUrl,
autoupdate: initialAutoUpdateValue,
},
onSubmit: (api) => {
const data = api.getData();
const { url, autoupdate } = data;
const text = textDisabled ? null : data.text || url;
if (data.url.trim() === "") {
return;
}
api.close();
const realUrl = checkUrlHttps(url);
// Either insert a new link or update the existing one
const anchor = getAnchor();
if (!anchor) {
editor.insertContent(
`<a href=${realUrl}${autoupdate ? ' data-integreat-auto-update="true"' : ""}>${text}</a>`
);
} else {
updateLink(editor, anchor, text, {
"href": realUrl,
// If false, remove the attribute rather than writing it out to equal false
"data-integreat-auto-update": autoupdate ? true : null,
});
}
},
onChange: updateDialog,
};
return editor.windowManager.open(dialogConfig);
};
editor.addShortcut("Meta+K", tinymceConfig.getAttribute("data-link-menu-text"), openDialog);
editor.ui.registry.addMenuItem("add_link", {
text: tinymceConfig.getAttribute("data-link-menu-text"),
icon: "link",
shortcut: "Meta+K",
onAction: openDialog,
});
// This form opens when a link is current selected with the cursor
editor.ui.registry.addContextForm("link_context_form", {
predicate: isAnchor,
initValue: () => {
const elm = getAnchor();
return elm ? elm.href : "";
},
position: "node",
commands: [
{
type: "contextformbutton",
icon: "link",
tooltip: tinymceConfig.getAttribute("data-update-text"),
primary: true,
onSetup: (buttonApi) => {
const nodeChangeHandler = () => {
buttonApi.setDisabled(editor.readonly);
};
editor.on("nodechange", nodeChangeHandler);
return () => {
editor.off("nodechange", nodeChangeHandler);
};
},
onAction: (formApi) => {
const url = formApi.getValue();
if (url) {
const realUrl = checkUrlHttps(url);
const anchor = getAnchor();
updateLink(editor, anchor, null, {
href: realUrl,
});
}
formApi.hide();
},
},
{
type: "contextformbutton",
icon: "unlink",
tooltip: tinymceConfig.getAttribute("data-link-remove-text"),
active: false,
onAction: (formApi) => {
const elm = getAnchor();
if (elm) {
elm.insertAdjacentHTML("beforebegin", elm.innerHTML);
elm.remove();
}
formApi.hide();
},
},
{
type: "contextformbutton",
icon: "new-tab",
tooltip: tinymceConfig.getAttribute("data-link-open-text"),
active: false,
onAction: () => {
const elm = getAnchor();
if (elm) {
window.open(elm.getAttribute("href"), "_blank");
}
},
},
],
});
return {};
});
})();