diff --git a/integreat_cms/api/v3/pages.py b/integreat_cms/api/v3/pages.py
index 7fa803bb5e..fd2318f6e9 100644
--- a/integreat_cms/api/v3/pages.py
+++ b/integreat_cms/api/v3/pages.py
@@ -90,6 +90,7 @@ def transform_page(
expand_shortcodes(page_translation.combined_text, context=context)
),
"content": expand_shortcodes(page_translation.combined_text, context=context),
+ "page_id": page_translation.page.id,
"parent": parent,
"order": order,
"available_languages": page_translation.available_languages_dict,
diff --git a/integreat_cms/cms/models/utils.py b/integreat_cms/cms/models/utils.py
index c660f9e26e..ab8fee6b32 100644
--- a/integreat_cms/cms/models/utils.py
+++ b/integreat_cms/cms/models/utils.py
@@ -50,6 +50,8 @@ def format_object_translation(
+ object_translation.link_title.tail
)
return {
+ "id": object_translation.id,
+ "foreign_object_id": object_translation.foreign_object.id,
"path": object_translation.path(),
"title": text_title,
"html_title": html_title,
diff --git a/integreat_cms/cms/templates/_tinymce_config.html b/integreat_cms/cms/templates/_tinymce_config.html
index 6eccad192a..5137095278 100644
--- a/integreat_cms/cms/templates/_tinymce_config.html
+++ b/integreat_cms/cms/templates/_tinymce_config.html
@@ -19,8 +19,11 @@
{% comment %} Styling for text diff taken from style.scss {% endcomment %}
{% firstof font_style|add:"del { background-color: rgb(252 165 165); } ins { background-color: rgb(134 239 172); text-decoration-line: none; }" as content_style %}
{
},
insert: {
title: "Insert",
- items: "openmediacenter add_link add_contact media | charmap hr",
+ items: "add_shortcode_page | add_shortcode_contact | openmediacenter add_link add_contact media | charmap hr",
},
},
link_title: false,
@@ -110,6 +110,7 @@ window.addEventListener("load", () => {
mediacenter: tinymceConfig.getAttribute("data-custom-plugins"),
custom_link_input: tinymceConfig.getAttribute("data-custom-plugins"),
custom_contact_input: tinymceConfig.getAttribute("data-custom-plugins"),
+ shortcodes: tinymceConfig.getAttribute("data-custom-plugins"),
},
link_default_protocol: "https",
link_target_list: false,
diff --git a/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js b/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js
index 0d00577df6..14cfd2e84f 100644
--- a/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js
+++ b/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js
@@ -1,4 +1,5 @@
import { getCsrfToken } from "../../utils/csrf-token";
+import { stripProtocol } from "../../utils/url-tools";
(() => {
const tinymceConfig = document.getElementById("tinymce-config-options");
@@ -47,7 +48,24 @@ import { getCsrfToken } from "../../utils/csrf-token";
};
tinymce.PluginManager.add("custom_link_input", (editor, _url) => {
- const isAnchor = (node) => node.nodeName.toLowerCase() === "a" && node.href && node.isContentEditable;
+ const internalPageURLRegex = new RegExp(String.raw`
+ ^[^:/]*://
+ ${stripProtocol(tinymceConfig.getAttribute("data-webapp-url")).replace(/\/$/, "")}
+ (
+ /
+ ([^/]+)
+ /
+ ([^/]{2,8})
+ /
+ (
+ ([^?#]+)
+ /
+ )?
+ ([^/?#]+)
+ )
+ `.replace(/\s+/g, ""));
+
+ const isAnchor = (node) => node.nodeName.toLowerCase() === "a" && node.href && node.isContentEditable && !internalPageURLRegex.exec(node.href);
const getAnchor = () => {
let node = editor.selection.getNode();
while (node !== null) {
diff --git a/integreat_cms/static/src/js/tinymce-plugins/shortcodes/contact.ts b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/contact.ts
new file mode 100644
index 0000000000..ec8764dcd6
--- /dev/null
+++ b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/contact.ts
@@ -0,0 +1,74 @@
+import { ShortcodeHandle, AcceptArbitraryArguments, PargsDescriptor, KWargsDescriptor } from "./utils";
+import type { ToolbarButtonInstanceApi, ContextFormInstanceApi, ContextFormButtonInstanceApi, ContextFormToggleButtonInstanceApi, DialogInstanceApi, DialogData, MenuItemInstanceApi, DialogSpec, BodyComponentSpec } from "../tinymce.d.ts";
+import { Editor } from "tinymce";
+import TomSelect from "tom-select";
+import { getCsrfToken } from "../../utils/csrf-token";
+import { stripProtocol } from "../../utils/url-tools";
+import { evaluateOnceDecorator } from "../../utils/caching-functions";
+
+class ContactHandle extends ShortcodeHandle {
+ keyword = "contact";
+ addIcon = "contact";
+ editIcon = "contact";
+ removeIcon = "remove";
+
+ pargs: PargsDescriptor = [
+ [["Contact ID", "The ID of the Contact whose details should be displayed"]],
+ [
+ ["address", "Whether the address should be shown and other, not explicitly wanted details should be hidden"],
+ ["email", "Whether the email should be shown and other, not explicitly wanted details should be hidden"],
+ ["phone_number", "Whether the phone number should be shown and other, not explicitly wanted details should be hidden"],
+ ["mobile_phone_number", "Whether the mobile phone number should be shown and other, not explicitly wanted details should be hidden"],
+ ["website", "Whether the website should be shown and other, not explicitly wanted details should be hidden"],
+ ],
+ ];
+ kwargs: KWargsDescriptor = [];
+
+ domainAndPrefix: () => string = evaluateOnceDecorator(() => stripProtocol(this.tinymceConfig.getAttribute("data-webapp-url")));
+ // Regular expression to check íf a link could be a page
+ // Capture groups: Path, Region slug, language slug, page infix, page slug
+ internalPageURLRegex: () => RegExp = evaluateOnceDecorator(() => new RegExp(String.raw`
+ ^[^:/]*://
+ ${this.domainAndPrefix().replace(/\/$/, "")}
+ (
+ /
+ ([^/]+)
+ /
+ ([^/]{2,8})
+ /
+ (
+ ([^?#]+)
+ /
+ )?
+ ([^/?#]+)
+ )
+ `.replace(/\s+/g, "")));
+
+ //contactCache: ContactCache = null;
+
+ predicate(node: Element): boolean {
+ // We also consider old contact cards as instances of the contact shortcode.
+ // This way shortcodes will work on the old contact cards and we will naturally slowly convert content from the old style directly embedded HTML.
+ if ("contactId" in (node as HTMLElement).dataset) {
+ return true;
+ }
+ return super.predicate(node);
+ }
+
+ setup(editor: Editor): boolean {
+ super.setup(editor);
+
+ this.addText = this.tinymceConfig.getAttribute("data-contact-menu-text");
+ this.editText = this.tinymceConfig.getAttribute("data-contact-change-text");
+ this.removeText = this.tinymceConfig.getAttribute("data-contact-remove-text");
+
+ const isContactsEnabled = this.tinymceConfig.getAttribute("data-contact-module-activated") !== "False";
+ if (!isContactsEnabled) {
+ return false;
+ }
+ return true;
+ }
+}
+
+
+export default ContactHandle;
diff --git a/integreat_cms/static/src/js/tinymce-plugins/shortcodes/page.ts b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/page.ts
new file mode 100644
index 0000000000..6b35c2ade7
--- /dev/null
+++ b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/page.ts
@@ -0,0 +1,510 @@
+import { ShortcodeHandle, AcceptArbitraryArguments, PargsDescriptor, KWargsDescriptor } from "./utils";
+import type { ToolbarButtonInstanceApi, ContextFormInstanceApi, ContextFormButtonInstanceApi, ContextFormToggleButtonInstanceApi, DialogInstanceApi, DialogData, MenuItemInstanceApi, DialogSpec, BodyComponentSpec } from "../tinymce.d.ts";
+import { Editor } from "tinymce";
+import { getCsrfToken } from "../../utils/csrf-token";
+import { stripProtocol } from "../../utils/url-tools";
+import { evaluateOnceDecorator } from "../../utils/caching-functions";
+
+import { debounce } from "../../utils/debounce";
+
+
+class PageHandle extends ShortcodeHandle {
+ keyword = "page";
+
+ pargs: PargsDescriptor = [
+ [["id", "The ID of the page to link to"]],
+ [["text", "The text to display (if not specified, show page title)"]],
+ ];
+ kwargs: KWargsDescriptor = [
+ // TODO: implement lang kwarg
+ // [["lang", false, "The language to which to link (instead of preferring the language of the document containing this link)", "Language"]],
+ ];
+
+ addText = "Add Page Link"
+ editText = "Edit Page Link"
+ removeText = "Remove Page Link"
+
+ domainAndPrefix: () => string = evaluateOnceDecorator(() => stripProtocol(this.tinymceConfig.getAttribute("data-webapp-url")));
+ // Regular expression to check íf a link could be a page
+ // Capture groups: Path, Region slug, language slug, page infix, page slug
+ internalPageURLRegex: () => RegExp = evaluateOnceDecorator(() => new RegExp(String.raw`
+ ^[^:/]*://
+ ${this.domainAndPrefix().replace(/\/$/, "")}
+ (
+ /
+ ([^/]+)
+ /
+ ([^/]{2,8})
+ /
+ (
+ ([^?#]+)
+ /
+ )?
+ ([^/?#]+)
+ )
+ `.replace(/\s+/g, "")));
+
+ pageCache: PageCache = null;
+
+ predicate(node: Element): boolean {
+ // We also consider old links that look like they point to pages as instances of the page shortcode.
+ // This way shortcodes will work on the old links and we will naturally slowly convert content from the old style direct links.
+ if (node.nodeName.toLowerCase() === "a") {
+ const href = (node as HTMLLinkElement).href;
+ if (href && (node as HTMLElement).isContentEditable && this.internalPageURLRegex().exec(href)) {
+ return true;
+ }
+ }
+ return super.predicate(node);
+ }
+
+ argsFromNode(node: HTMLElement | null): [string[], Map
] {
+ // If we are operating on an old style direct link, we need to recover what that would be as the arguments for the new shortcode.
+ if (node && node.nodeName.toLowerCase() === "a") {
+ const [fullURL, path, regionSlug, languageSlug, infix, pageSlug] = node.getAttribute("href").match(this.internalPageURLRegex());
+ let text = node.textContent !== node.getAttribute("href") ? node.textContent : "";
+ // Get page id for slug
+ const translation = this.pageCache.byPath.get(path);
+ const id = translation.page.id;
+ return [[`${id}`, text], new Map()];
+ }
+ return super.argsFromNode(node);
+ }
+
+ renderPreviewNode(pargs: string[], kwargs: Map): string {
+ // The html string representation of the shortcode in the TinyMCE editor
+ // By default this is a span marked with mceNonEditable and the shortcode keyword and parameters
+ // and defers the visual presented to the user to be rendered to renderPreview()
+
+ // Ensure the data for this is loaded in the cache,
+ // including ancestors, so the edit dialog can render the title path
+ this.pageCache.requestId(parseInt(pargs[0]), true).then();
+ return super.renderPreviewNode(pargs, kwargs)
+ }
+
+ renderPreview(pargs: string[], kwargs: Map): string {
+ // The html string representation of the shortcode preview in the TinyMCE editor
+ // By default this is just the canonical text representation. This function will be overwritten by most subclasses.
+ const id = parseInt(pargs[0]);
+ const languageSlug = this.tinymceConfig.getAttribute("data-language-slug");
+ const page = this.pageCache.byId.get(id);
+ // TODO: If page not in cache, re-render after request resolved
+ const translation = page?.translations.get(languageSlug);
+ const text = pargs[1] || translation?.title;
+
+ const TEXT_MISSING = "MISSING LINK"; // TODO: translations (#4044)
+ let element;
+ if (!translation) {
+ element = document.createElement("i");
+ element.classList.add("error");
+ element.innerText = `[${text || TEXT_MISSING}]`;
+ } else {
+ element = document.createElement("a");
+ element.innerText = text;
+ // No href, the link is non-interactible anyway
+ element.href = "#"
+ }
+ return element.outerHTML;
+ }
+
+ async getCompletions(query: string, id: number) {
+ const url = this.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,
+ is_link_suggestion: true,
+ }),
+ });
+ const HTTP_STATUS_OK = 200;
+ if (response.status !== HTTP_STATUS_OK) {
+ return [];
+ }
+
+ const data = await response.json();
+ return [data.data, id];
+ }
+
+ displayEditDialog(initialPargs: string[], initialKWargs: [string, string][]) {
+ const ID_ARG = "parg0";
+ const TEXT_ARG = "parg1";
+
+ const node = this.getNode();
+ const initialText = node ? initialPargs[1] : this.editor.selection.getContent({ format: "text" });
+
+ let prevSearchText = "";
+ let prevSelectedCompletion = initialPargs[0] ? initialPargs[0] : "";
+
+ // Stores the current request id, so that outdated requests get ignored
+ let ajaxRequestId = 0;
+ const defaultCompletionItem = {
+ text: this.tinymceConfig.getAttribute("data-link-no-results-text"),
+ title: "",
+ value: "",
+ };
+ const languageSlug = this.tinymceConfig.getAttribute("data-language-slug");
+ const cachedPageData = this.pageCache.byId.get(parseInt(initialPargs[0]))?.translations.get(languageSlug);
+ const initialCompletionItem = {
+ text: cachedPageData?.titlePath || "",
+ title: cachedPageData?.title || "",
+ value: `${initialPargs[0]}`,
+ };
+ const completionItems = cachedPageData ? [initialCompletionItem] : [defaultCompletionItem];
+ let currentCompletionText = "";
+
+ const that = this;
+ const updateDialog = (api: DialogInstanceApi) => {
+ super.defaultOnChange(api);
+
+ let data = api.getData();
+
+ let urlChangedBySearch = false;
+ // Check if the selected completion changed
+ if (prevSelectedCompletion !== data[ID_ARG]) {
+ // find the correct text currently shown in the completion items box
+ if (completionItems.length > 0) {
+ const currentCompletion = completionItems.find(
+ (completion) => completion.value === data[ID_ARG]
+ );
+ // Don't set the completion text to `- no results -`
+ if (currentCompletion && currentCompletion.value !== "") {
+ currentCompletionText = currentCompletion.title;
+ } else {
+ currentCompletionText = "";
+ }
+ } else {
+ currentCompletionText = "";
+ }
+ }
+ prevSelectedCompletion = data[ID_ARG];
+
+ // Disable the submit button if no valid page found
+ api.setEnabled("submit", data[ID_ARG]);
+
+ // make new ajax request on user input
+ if (data.search !== prevSearchText && data.search !== "") {
+ ajaxRequestId += 1;
+ this.getCompletions(data.search, ajaxRequestId).then(([newCompletions, requestId]) => {
+ if (requestId !== ajaxRequestId) {
+ return;
+ }
+
+ completionItems.length = 0;
+ for (const completion of newCompletions) {
+ const [fullURL, path, regionSlug, languageSlug, infix, pageSlug] = completion.url.match(that.internalPageURLRegex());
+ completionItems.push({
+ text: completion.path,
+ title: completion.html_title,
+ value: `${completion.foreign_object_id}`,
+ //value: `${that.pageCache.bySlug.get(languageSlug).get(path).page.id}`,
+ });
+ }
+
+ 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;
+
+ api.setEnabled(ID_ARG, !completionDisabled);
+
+ 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(ID_ARG);
+ updateDialog(api);
+ }
+ };
+
+ const completion: any = {};
+ completion[ID_ARG] = prevSelectedCompletion
+ const dialogConfig: DialogSpec = {
+ title: this.text(this.editText),
+ body: {
+ type: "panel",
+ items: [
+ {
+ type: "input",
+ name: TEXT_ARG,
+ label: this.tinymceConfig.getAttribute("data-link-dialog-text-text"),
+ //disabled: textDisabled,
+ },
+ {
+ type: "label",
+ label: this.tinymceConfig.getAttribute("data-link-dialog-internal_link-text"),
+ items: [
+ {
+ type: "input",
+ name: "search",
+ },
+ {
+ type: "selectbox",
+ name: ID_ARG,
+ items: completionItems,
+ //disabled: true,
+ },
+ ],
+ },
+ ],
+ },
+ buttons: [
+ {
+ type: "cancel",
+ text: this.tinymceConfig.getAttribute("data-dialog-cancel-text"),
+ },
+ {
+ type: "submit",
+ name: "submit",
+ text: this.tinymceConfig.getAttribute("data-dialog-submit-text"),
+ primary: true,
+ enabled: false,
+ },
+ ],
+ initialData: {
+ ...this.defaultInitialData(initialPargs, initialKWargs),
+ ...completion,
+ },
+ onSubmit: this.defaultOnSubmit.bind(this),
+ onChange: updateDialog.bind(this),
+ };
+
+ return this.editor.windowManager.open(dialogConfig);
+ }
+
+ populatePageCache() {
+ const baseUrl = this.tinymceConfig.getAttribute("data-base-url");
+ const regionSlug = this.tinymceConfig.getAttribute("data-region-slug");
+ const languageSlug = this.tinymceConfig.getAttribute("data-language-slug");
+ const url = `${baseUrl}/api/v3/${regionSlug}/${languageSlug}/pages/`;
+
+ fetch(url, {
+ method: "GET",
+ headers: {
+ "X-CSRFToken": getCsrfToken(),
+ },
+ }).then((response): any => {
+ const HTTP_STATUS_OK = 200;
+ if (response.status !== HTTP_STATUS_OK) {
+ return [];
+ }
+ return response.json();
+ }).then(async (data) => {
+ for (let mainTranslation of data) {
+ await this.pageCache.requestId(mainTranslation.page_id);
+ };
+ });
+ }
+
+ setup(editor: Editor) {
+ super.setup(editor);
+
+ this.addText = this.tinymceConfig.getAttribute("data-link-dialog-title-text");
+ this.editText = this.tinymceConfig.getAttribute("data-link-dialog-title-text");
+ this.removeText = this.tinymceConfig.getAttribute("data-link-remove-text");
+
+ const baseUrl = this.tinymceConfig.getAttribute("data-base-url");
+ const regionSlug = this.tinymceConfig.getAttribute("data-region-slug");
+ const languageSlug = this.tinymceConfig.getAttribute("data-language-slug");
+
+ const debouncedRefreshPreview = debounce((pageMetadata: PageMetadata) => {
+ this.refreshPreview((pargs, kwargs) => parseInt(pargs[0]) == pageMetadata.id);
+ }, 100);
+ this.pageCache = new PageCache(baseUrl, regionSlug, languageSlug, debouncedRefreshPreview);
+
+ //this.populatePageCache();
+ return true;
+ }
+}
+
+
+class PageCache {
+ byId = new Map();
+ byPath = new Map();
+
+ pending = new Map>();
+
+ afterFetch: ((metadata: PageMetadata) => void) | null = null;
+
+ baseUrl: string;
+ regionSlug: string;
+ defaultLanguageSlug: string;
+
+ _pathRegex = new RegExp(String.raw`
+ /
+ ([^/]+)
+ /
+ ([^/]{2,8})
+ /
+ (
+ ([^?#]+)
+ /
+ )?
+ ([^/?#]+)
+ `.replace(/\s+/g, ""));
+
+ constructor(baseUrl: string, regionSlug: string, defaultLanguageSlug: string, afterFetch: ((metadata: PageMetadata) => void) | null = null) {
+ this.baseUrl = baseUrl;
+ this.regionSlug = regionSlug;
+ this.defaultLanguageSlug = defaultLanguageSlug;
+ this.afterFetch = afterFetch;
+ }
+
+ cacheTranslationMetadata(translation: any) {
+ const [fullURL, regionSlug, languageSlug, infix, _, slug] = translation.path.match(this._pathRegex);
+ console.assert(translation.page_id, `translation without page id:`, translation);
+
+ const pageMetadata = this.byId.get(translation.page_id) || new PageMetadata({
+ id: translation.page_id,
+ parentId: translation.parent?.id,
+ regionSlug: regionSlug,
+ }, this);
+ if (!this.byId.has(translation.page_id)) {
+ this.byId.set(translation.page_id, pageMetadata);
+ }
+
+ const translationMetadata = pageMetadata.translations.get(languageSlug) || new TranslationMetadata({
+ id: translation.id,
+ languageSlug: languageSlug,
+ title: translation.title,
+ slug: slug,
+ path: translation.path,
+ page: pageMetadata,
+ }, this);
+ pageMetadata.translations.set(languageSlug, translationMetadata);
+
+ this.byPath.set(translation.path, translationMetadata);
+
+ if (this.afterFetch) this.afterFetch(pageMetadata);
+ return pageMetadata;
+ }
+
+ async _getPage(id: number, languageSlug: string = undefined) {
+ languageSlug = languageSlug || this.defaultLanguageSlug;
+
+ const url = `${this.baseUrl}/api/v3/${this.regionSlug}/${languageSlug}/page/?id=${id}`;
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ "X-CSRFToken": getCsrfToken(),
+ },
+ })
+ const translation = response.status === 200 ? await response.json() : null;
+ return {
+ id: id,
+ languageSlug: languageSlug,
+ translation: translation,
+ };
+ }
+
+ requestId(id: number, ancestors: boolean = false): Promise {
+ const that = this;
+ async function inner(id: number) {
+ const {languageSlug, translation} = await that._getPage(id);
+ if (!translation) return null;
+ const pageMetadata = that.cacheTranslationMetadata(translation);
+
+ for (const [lang, tr] of Object.entries(translation.available_languages)) {
+ const {translation} = await that._getPage(id, lang);
+ if (translation) {
+ that.cacheTranslationMetadata(translation);
+ }
+ }
+ if (ancestors && pageMetadata.parentId) {
+ await that.requestId(pageMetadata.parentId, ancestors);
+ }
+ return pageMetadata;
+ }
+
+ if (this.byId.has(id)) {
+ // Return a Promise that immediately resolves
+ return new Promise((res, rej) => res(this.byId.get(id)));
+ } else if (!this.pending.has(id)) {
+ // Start a new query
+ this.pending.set(id, inner(id));
+ }
+ // Return the Promise of the ongoing query
+ return this.pending.get(id);
+ }
+}
+class PageMetadata {
+ _cache: PageCache;
+
+ id: number;
+ parentId: number;
+ regionSlug: string;
+ translations = new Map();
+
+ get parent() {
+ return this._cache?.byId.get(this.parentId);
+ }
+
+ constructor(data: {id: number, parentId: number, regionSlug: string}, cache: PageCache = null) {
+ this._cache = cache;
+ this.id = data.id;
+ this.parentId = data.parentId;
+ this.regionSlug = data.regionSlug;
+ }
+}
+
+class TranslationMetadata {
+ _cache: PageCache;
+
+ id: number;
+ languageSlug: string;
+ title: string;
+ slug: string;
+ path: string;
+ page: PageMetadata;
+
+ get parent() {
+ return this.page.parent?.translations.get(this.languageSlug);
+ }
+ get titlePath() {
+ const reverseTitles = [this.title];
+ let translation: TranslationMetadata = this;
+ while (translation.page.parentId) {
+ translation = translation.parent;
+ if (translation) {
+ reverseTitles.push(translation.title);
+ } else {
+ reverseTitles.push("[?]");
+ break;
+ }
+ }
+ return reverseTitles.reverse().join(" → ");
+ }
+
+ constructor(data: {id: number, languageSlug: string, title: string, slug: string, path: string, page: PageMetadata}, cache: PageCache = null) {
+ this._cache = cache;
+ this.id = data.id;
+ this.languageSlug = data.languageSlug;
+ this.title = data.title;
+ this.slug = data.slug;
+ this.path = data.path;
+ this.page = data.page;
+ }
+}
+
+export default PageHandle;
diff --git a/integreat_cms/static/src/js/tinymce-plugins/shortcodes/plugin.js b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/plugin.js
new file mode 100644
index 0000000000..d5698fcdce
--- /dev/null
+++ b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/plugin.js
@@ -0,0 +1,72 @@
+import { Parser } from "./shortcodes";
+import ContactHandle from "./contact";
+import PageHandle from "./page";
+import { Registry, ShortcodeHandle } from "./utils";
+
+Registry.register(new PageHandle());
+Registry.register(new ContactHandle());
+
+function DummyHandleFactory(keyword) {
+ const handle = new ShortcodeHandle();
+ handle.keyword = keyword;
+ return handle;
+}
+Registry.setUnknownHandleFactory(DummyHandleFactory);
+
+
+(() => {
+ const tinymceConfig = document.getElementById("tinymce-config-options");
+ const parser = new Parser("[", "]", "\\", true, true);
+ const context = {
+ language: tinymceConfig.getAttribute("data-language"),
+ directionality: tinymceConfig.getAttribute("data-directionality"),
+ };
+
+ tinymce.PluginManager.add("shortcodes", editor => {
+ /*
+ function insertShortcode() {
+ let html = `[shortcode 2]`;
+ editor.insertContent(html);
+ }
+ */
+
+ editor.on('BeforeSetContent', function(e) {
+ /*
+ // Ensure all shortcodes are represented by a marker node in tinyMCE
+ e.content = e.content.replace(/(\[shortcode (\d+)\]<\/span>|\[shortcode (\d+)\])/g, (match, _, a, b) => {
+ return `[shortcode ${a || b}]`;
+ });
+ */
+ console.log("Parsing registered handles:", Registry.instance.handles);
+ try {
+ e.content = parser.parse(e.content, context);
+ } catch (e) {
+ console.error("Failed to expand shortcodes:", e);
+ }
+ });
+
+ editor.on('PreProcess', function(e) {
+ // Strip the mce marker out when extracting the content for saving or the source code view
+ console.log(`PreProcess – restoring canonical form`, e);
+ const shortcodes = Array.from(e.node.querySelectorAll('span.mceNonEditable[data-shortcode]'));
+ shortcodes.forEach(node => {
+ const keyword = node.dataset.shortcode;
+ const handle = Registry.get(keyword);
+ node.outerText = handle.renderShortcode(...handle.argsFromNode(node));
+ });
+ });
+
+ /*
+ editor.ui.registry.addMenuItem("add_shortcode", {
+ text: "Shortcode",
+ icon: "link",
+ //shortcut: "Meta+L",
+ onAction: insertShortcode,
+ });
+ */
+
+ Registry.setupAll(editor, parser);
+
+ return {};
+ });
+})();
diff --git a/integreat_cms/static/src/js/tinymce-plugins/shortcodes/shortcodes.ts b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/shortcodes.ts
new file mode 100644
index 0000000000..43f697ca82
--- /dev/null
+++ b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/shortcodes.ts
@@ -0,0 +1,411 @@
+/*******************************************
+ * JS version of pythons shortcode package *
+ * which is licensed under MIT. *
+ * This ensures that editor and backend *
+ * behave exactly the same. *
+ *******************************************/
+
+
+// Globally-registered handler functions indexed by keyword.
+const global_keywords = new Map, context: any, content?: string) => string, string]>();
+
+
+// The set of all end-words for globally-registered block-scoped shortcodes.
+const global_endwords = new Set();
+
+
+// Decorator function for globally registering shortcode handlers.
+function register(keyword: string, endword: string) {
+
+ function register_function(func: (pargs: string[], kwargs: Map, context: any, content?: string) => string) {
+ global_keywords.set(keyword, [func, endword]);
+ if (endword) {
+ global_endwords.add(endword);
+ }
+ return func;
+ }
+
+ return register_function;
+}
+
+
+/***********************
+ * Exception Classes *
+ ***********************/
+
+
+// Base class for all exceptions raised by the library.
+class ShortcodeError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "ShortcodeError";
+ }
+}
+
+
+// Raised if the parser detects invalid shortcode syntax.
+class ShortcodeSyntaxError extends ShortcodeError {
+ constructor(message: string) {
+ super(message);
+ this.name = "ShortcodeSyntaxError";
+ }
+}
+
+
+// Raised if a handler function throws an error.
+class ShortcodeRenderingError extends ShortcodeError {
+ constructor(message: string) {
+ super(message);
+ this.name = "ShortcodeRenderingError";
+ }
+}
+
+
+/***************
+ * AST Nodes *
+ ***************/
+
+
+// Input text is parsed into a tree of ASTNode instances.
+class ASTNode {
+ children: ASTNode[] = [];
+ token: Token;
+
+ constructor() {
+ }
+
+ render(context: any): string {
+ return this.children.map(c => c.render(context)).join("");
+ }
+}
+
+
+// Represents ordinary text not enclosed in tag delimiters.
+class Text extends ASTNode {
+ text: string;
+
+ constructor(text: string) {
+ super();
+ this.text = text;
+ }
+
+ render(context: any): string {
+ return this.text;
+ }
+}
+
+
+// Base class for atomic and block-scoped shortcodes.
+class Shortcode extends ASTNode {
+ // Regex for parsing the shortcode's arguments.
+ re_args = new RegExp(String.raw`
+ (?:([^\s'"=]+)=)?
+ (
+ "((?:[^\\"]|\\.)*)"
+ |
+ '((?:[^\\']|\\.)*)'
+ )
+ |
+ ([^\s'"=]+)=(\S+)
+ |
+ (\S+)
+ `.replace(/\s+/g, ""), "g");
+
+ handler: (pargs: string[], kwargs: Map, context: any, content?: string) => string;
+ pargs: string[];
+ kwargs: Map;
+ children: ASTNode[];
+
+ constructor(token: Token, handler_function: (pargs: string[], kwargs: Map, context: any, content?: string) => string) {
+ super();
+ this.token = token;
+ this.handler = handler_function;
+ [this.pargs, this.kwargs] = this.parse_args(token.text.slice(token.keyword.length));
+ this.children = [];
+ }
+
+ parse_args(argstring: string): [string[], Map] {
+ const pargs: string[] = [];
+ const kwargs = new Map();
+ for (const match of argstring.matchAll(this.re_args)) {
+ if (match[2] || match[5]) {
+ const key = match[1] || match[5];
+ const value = match[3] || match[4] || match[6];
+ if (key) {
+ kwargs.set(key, value);
+ } else {
+ pargs.push(value);
+ }
+ } else {
+ pargs.push(match[7]);
+ }
+ }
+ console.log(`parsed from argstring: »${argstring}«`, pargs, kwargs);
+ return [pargs, kwargs];
+ }
+}
+
+
+// An atomic shortcode is a shortcode with no closing tag.
+class AtomicShortcode extends Shortcode {
+ /* If the shortcode handler raises an exception we intercept it and wrap it
+ * in a ShortcodeRenderingError.
+ */
+ render(context: any) {
+ try {
+ return this.handler(this.pargs, this.kwargs, context).toString();
+ } catch (ex: unknown) {
+ const msg = `An exception was raised while rendering the '${this.token.keyword}' shortcode in line ${this.token.line_number}.`;
+ const error = new ShortcodeRenderingError(msg);
+ if (ex instanceof Error) {
+ error.stack = ex.stack;
+ }
+ throw error;
+ }
+ }
+}
+
+
+// A block-scoped shortcode is a shortcode with a closing tag.
+class BlockShortcode extends Shortcode {
+ /* If the shortcode handler raises an exception we intercept it and wrap it
+ * in a ShortcodeRenderingError. The original exception will still be
+ * available via the exception's __cause__ attribute.
+ */
+ render(context: any) {
+ const content = this.children.map(c => c.render(context)).join("");
+ try {
+ return this.handler(this.pargs, this.kwargs, context, content).toString();
+ } catch (ex: unknown) {
+ const msg = `An exception was raised while rendering the '${this.token.keyword}' shortcode in line ${this.token.line_number}.`
+ const error = new ShortcodeRenderingError(msg);
+ if (ex instanceof Error) {
+ error.stack = ex.stack;
+ }
+ throw error;
+ }
+ }
+}
+
+
+/************
+ * Parser *
+ ************/
+
+
+/* A Parser instance parses input text and renders shortcodes. A single Parser
+ * instance can parse an unlimited number of input strings. Note that the parse()
+ * method accepts an optional arbitrary context object which it passes on to each
+ * shortcode's handler function.
+ *
+ * If the `inherit_globals` parameter is true, the parser will inherit a copy of
+ * the set of globally-registered shortcodes at the moment of instantiation.
+ *
+ * If `ignore_unknown` is true, unknown shortcodes are ignored. If this parameter
+ * is false (the default), unknown shortcodes cause an error.
+ */
+class Parser {
+ start: string;
+ end: string;
+ esc_start: string;
+ keywords: Map, context: any, content?: string) => string, string]>;
+ endwords: Set;
+ ignore_unknown: boolean;
+ unknownHandlerFactory: (keyword: string) => (pargs: string[], kwargs: Map, context: any, content?: string) => string; // patched in
+
+ constructor(start: string = '[%', end: string = '%]', esc: string = '\\', inherit_globals: boolean = true, ignore_unknown: boolean = false) {
+ this.start = start;
+ this.end = end;
+ this.esc_start = esc + start;
+ this.keywords = new Map, context: any, content?: string) => string, string]>(inherit_globals ? global_keywords : null);
+ this.endwords = new Set(inherit_globals ? global_endwords : null);
+ this.ignore_unknown = ignore_unknown;
+ this.unknownHandlerFactory = null; // patched in
+ }
+
+ register(func: (pargs: string[], kwargs: Map, context: any, content?: string) => string, keyword: string, endword: string = null) {
+ this.keywords.set(keyword, [func, endword]);
+ if (endword) {
+ this.endwords.add(endword);
+ }
+ }
+
+ // patched in
+ setUnknownHandlerFactory(func: (keyword: string) => (pargs: string[], kwargs: Map, context: any, content?: string) => string) {
+ this.unknownHandlerFactory = func;
+ }
+
+ parse(text: string, context: any = null) {
+ if (!text.includes(this.start)) {
+ return text;
+ }
+
+ const stack = [new ASTNode()];
+ const expecting = [];
+
+ const lexer = new Lexer(text, this.start, this.end, this.esc_start);
+ for (const token of lexer.tokenize()) {
+ if (token.type == "TEXT") {
+ stack[stack.length-1].children.push(new Text(token.text));
+ } else if (this.keywords.has(token.keyword)) {
+ const [handler, endword] = this.keywords.get(token.keyword);
+ if (endword) {
+ const node = new BlockShortcode(token, handler);
+ stack[stack.length-1].children.push(node);
+ stack.push(node);
+ expecting.push(endword);
+ } else {
+ const node = new AtomicShortcode(token, handler);
+ stack[stack.length-1].children.push(node);
+ }
+ } else if (this.endwords.has(token.keyword)) {
+ if (expecting.length == 0) {
+ const msg = `Unexpected '${token.keyword}' tag in line ${token.line_number}.`;
+ throw new ShortcodeSyntaxError(msg);
+ } else if (token.keyword == expecting[expecting.length-1]) {
+ stack.pop();
+ expecting.pop();
+ } else {
+ const msg = `Unexpected '${token.keyword}' tag in line ${token.line_number}. The shortcode parser was expecting a closing '${expecting[-1]}' tag.`;
+ throw new ShortcodeSyntaxError(msg);
+ }
+ } else if (token.keyword == '') {
+ const msg = `Empty shortcode tag in line ${token.line_number}.`;
+ throw new ShortcodeSyntaxError(msg);
+ } else if (this.ignore_unknown) {
+ if (this.unknownHandlerFactory !== null) { // START patched in
+ // Instead of treating the unknown shortcode as text, parse it as a dummy one
+ const node = new AtomicShortcode(token, this.unknownHandlerFactory(token.keyword));
+ stack[stack.length-1].children.push(node);
+ continue;
+ } // END patched in
+ stack[stack.length-1].children.push(new Text(token.raw_text));
+ } else {
+ const msg = `Unrecognised shortcode tag '${token.keyword}' in line ${token.line_number}.`
+ throw new ShortcodeSyntaxError(msg);
+ }
+ }
+
+ if (expecting.length) {
+ const token = stack[stack.length-1].token;
+ const msg = `Unexpected end of document. The shortcode parser was expecting a closing '${expecting[-1]}' tag to close the '${token.keyword}' tag opened in line ${token.line_number}.`;
+ throw new ShortcodeSyntaxError(msg);
+ }
+
+ return stack.pop().render(context);
+ }
+}
+
+
+/***********
+ * Lexer *
+ ***********/
+
+
+class Token {
+ keyword: string;
+ type: string;
+ text: string;
+ raw_text: string;
+ line_number: number;
+
+ constructor(token_type: string, token_text: string, raw_text: string, line_number: number) {
+ const words = token_text.split(/\s+/);
+ this.keyword = words ? words[0] : '';
+ this.type = token_type;
+ this.text = token_text;
+ this.raw_text = raw_text;
+ this.line_number = line_number;
+ }
+
+ toString(): string {
+ return `(${this.type}, ${this.text.toString()}, ${this.line_number})`;
+ }
+}
+
+
+class Lexer {
+ text: string;
+ start: string;
+ end: string;
+ esc_start: string;
+ tokens: Token[];
+ index: number;
+ line_number: number;
+
+ constructor(text: string, start: string, end: string, esc_start: string) {
+ this.text = text;
+ this.start = start;
+ this.end = end;
+ this.esc_start = esc_start;
+ this.tokens = [];
+ this.index = 0;
+ this.line_number = 1;
+ }
+
+ match(target: string): boolean {
+ if (this.text.startsWith(target, this.index)) {
+ return true;
+ }
+ return false;
+ }
+
+ advance() {
+ if (this.text[this.index] == '\n') {
+ this.line_number += 1;
+ }
+ this.index += 1;
+ }
+
+ tokenize(): Token[] {
+ while (this.index < this.text.length) {
+ if (this.match(this.esc_start)) {
+ this.read_escaped_tag_delimiter();
+ } else if (this.match(this.start)) {
+ this.read_tag();
+ } else {
+ this.read_text();
+ }
+ }
+ return this.tokens;
+ }
+
+ read_escaped_tag_delimiter() {
+ this.index += this.esc_start.length;
+ this.tokens.push(new Token("TEXT", this.start, this.esc_start, this.line_number));
+ }
+
+ read_tag() {
+ this.index += this.start.length;
+ const start_index = this.index;
+ const start_line_number = this.line_number;
+ while (this.index < this.text.length) {
+ if (this.match(this.end)) {
+ const text = this.text.slice(start_index, this.index).trim();
+ const raw_text = this.text.slice(start_index-this.start.length, this.index+this.end.length);
+ this.tokens.push(new Token("TAG", text, raw_text, start_line_number));
+ this.index += this.end.length;
+ return;
+ }
+ this.advance();
+ }
+ const msg = `Unclosed shortcode tag. The tag was opened in line ${start_line_number}.`;
+ throw new ShortcodeSyntaxError(msg);
+ }
+
+ read_text() {
+ const start_index = this.index;
+ const start_line_number = this.line_number;
+ while (this.index < this.text.length) {
+ if (this.match(this.esc_start) || this.match(this.start)) {
+ break;
+ }
+ this.advance();
+ }
+ const text = this.text.slice(start_index, this.index);
+ this.tokens.push(new Token("TEXT", text, text, start_line_number));
+ }
+}
+
+
+export { register, ShortcodeError, ShortcodeSyntaxError, ShortcodeRenderingError, ASTNode, Text, Shortcode, AtomicShortcode, BlockShortcode, Parser, Token, Lexer };
diff --git a/integreat_cms/static/src/js/tinymce-plugins/shortcodes/utils.ts b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/utils.ts
new file mode 100644
index 0000000000..92bef89cf1
--- /dev/null
+++ b/integreat_cms/static/src/js/tinymce-plugins/shortcodes/utils.ts
@@ -0,0 +1,870 @@
+///
+import type { ToolbarButtonInstanceApi, ContextFormInstanceApi, ContextFormButtonInstanceApi, ContextFormToggleButtonInstanceApi, DialogInstanceApi, DialogData, MenuItemInstanceApi, DialogSpec, BodyComponentSpec } from "../tinymce.d.ts";
+import { Editor } from "tinymce";
+
+import type { Parser } from "./shortcodes";
+import { Shortcode as parser } from "./shortcodes";
+
+/*
+
+- enable easily implementing behaviour to edit/manage different kinds of shortcodes
+- register handle() function to parse shortcode arguments into tinymce readable marker
+- ??? provide function to get canocical shortcode ??? (to be used when turning markers back into just the shortcode)
+- provide easily overrideable methods for UI handling
+
+- one ShortcodeHandle object per shortcode type, not per shortcode in content
+
+*/
+
+
+const AcceptArbitraryArguments = Symbol("AcceptArbitraryArguments");
+type ARBITRARY = typeof AcceptArbitraryArguments;
+
+type TextDescriptor = string | ((self: ShortcodeHandle) => string);
+
+type SingleParg = null // Just to occupy this index.
+ | string // Name for the positional argument.
+ | [string, string?]; // Name and description of the positional argument.
+type PargsDescriptor = ARBITRARY // Allow any number of positional arguments.
+ | number // How many positional arguments have to be given (exactly, not more and not less).
+ | [number, number | ARBITRARY] // How many positional arguments are required, and up to how many CAN be given (Infinity or null means unbounded).
+ | [SingleParg[], SingleParg[] | [...SingleParg[], ARBITRARY] | ARBITRARY]; // The list of the required and the list of optional positional arguments.
+type SingleKWarg = string // Keyword (required)
+ | [string, boolean?, string?, string?]; // Keyword, whether it is required (default: not required), a description of the argument and a human readable name to use instead of the default conversion of the keyword.
+type KWargsDescriptor = ARBITRARY // Allow any keyword argument.
+ | SingleKWarg[] // List of all keyword arguments being accepted. Also serves as a canonical order normalizing the shortcode.
+ | [...SingleKWarg[], ARBITRARY]; // If the last element is AcceptArbitraryArguments, anything is accepted as optional argument.
+type ExplicitArg = {
+ name: string;
+ specifiedName?: string;
+ genericName?: string;
+ required: boolean;
+ description: string;
+ unifiedDescription: string;
+};
+
+
+class ShortcodeHandle {
+ readonly keyword: string;
+ readonly endword: string | null = null;
+ editor: Editor;
+ tinymceConfig: HTMLElement;
+ addText: TextDescriptor = (self: ShortcodeHandle) => `Insert ${self.keyword}`;
+ addIcon: string = "link";
+ editText: TextDescriptor = (self: ShortcodeHandle) => `Edit ${self.keyword}`;
+ editIcon: string = "link";
+ removeText: TextDescriptor = (self: ShortcodeHandle) => `Remove ${self.keyword}`;
+ removeIcon: string = "unlink";
+
+ static escape(str: string | undefined): string {
+ if (!str) return '""';
+ return str.includes(" ") ? `"${str.replace(/"/g, '\\"')}"` : str;
+ }
+
+ readonly pargs: PargsDescriptor = AcceptArbitraryArguments;
+ readonly kwargs: KWargsDescriptor = AcceptArbitraryArguments;
+ get maxPargs(): number {
+ if (this.pargs === AcceptArbitraryArguments)
+ return Infinity;
+ if (typeof this.pargs === "number")
+ return this.pargs;
+ else if (this.pargs.length > 0 && typeof this.pargs[0] === "number") {
+ // [min, max] style
+ if (this.pargs[1] === AcceptArbitraryArguments)
+ return Infinity;
+ else
+ return this.pargs[1] as number;
+ } else {
+ // Listing of required and list of optional positional arguments style
+ const required = this.pargs[0] as SingleParg[];
+ const optional = this.pargs[1] as SingleParg[] | [...SingleParg[], ARBITRARY];
+ if (optional[optional.length] === AcceptArbitraryArguments)
+ return Infinity;
+ else
+ return required.length + optional.length;
+ }
+ }
+ get minPargs(): number {
+ if (this.pargs === AcceptArbitraryArguments)
+ return 0;
+ if (typeof this.pargs === "number")
+ return this.pargs;
+ else if (this.pargs.length > 0 && typeof this.pargs[0] === "number") {
+ // [min, max] style
+ return this.pargs[0];
+ } else {
+ // Listing of required and list of optional positional arguments style
+ return (this.pargs[0] as SingleParg[]).length;
+ }
+ }
+ getExplicitParg(index: number): ExplicitArg {
+ // Get an explicit descriptor of the positional argument at this index
+ if (index < 0 || index >= this.maxPargs)
+ throw RangeError;
+ const parg: ExplicitArg = {
+ name: null,
+ specifiedName: null,
+ genericName: `Argument ${index}`,
+ required: null,
+ description: null,
+ unifiedDescription: null,
+ };
+ if (this.pargs === AcceptArbitraryArguments)
+ parg.required = false;
+ else if (typeof this.pargs === "number")
+ parg.required = true;
+ else if (this.pargs.length > 0 && typeof this.pargs[0] === "number") {
+ let [min, max] = this.pargs;
+ parg.required = index < min;
+ } else {
+ // Listing of required and list of optional positional arguments style
+ const required = this.pargs[0] as SingleParg[];
+ const optional = this.pargs[1] === AcceptArbitraryArguments ? [] : this.pargs[1] as SingleParg[] | [...SingleParg[], ARBITRARY];
+ // Create a joined list that only contains argument descriptions, not the Symbol
+ const pargs = required.concat((
+ optional[optional.length-1] === AcceptArbitraryArguments ?
+ optional.slice(0, optional.length-1)
+ : optional
+ ) as SingleParg[]);
+ let length = pargs.length;
+ if (index < length) {
+ if (typeof pargs[index] === "string")
+ parg.specifiedName = pargs[index];
+ else if (pargs[index] !== null) {
+ const arg = pargs[index] as [string, string?];
+ parg.specifiedName = arg[0];
+ if (arg.length > 1)
+ parg.description = arg[1];
+ }
+ } else
+ parg.required = false;
+ }
+ // Fill in missing details
+ parg.name = parg.specifiedName !== null ? parg.specifiedName : parg.genericName;
+ if (parg.required === null)
+ parg.required = index < this.minPargs;
+ if (parg.description === null)
+ parg.description = ``; // Description stays empty
+ if (parg.specifiedName !== null) {
+ parg.unifiedDescription = parg.specifiedName;
+ if (parg.description)
+ parg.unifiedDescription += ` – ${parg.description}`;
+ } else
+ parg.unifiedDescription = parg.description || parg.genericName;
+ return parg;
+ }
+
+ get requiredKWargs(): Set {
+ if (this.kwargs === AcceptArbitraryArguments) return new Set();
+ const known = this.kwargs.filter(kw => kw !== AcceptArbitraryArguments) as SingleKWarg[];
+ const required = known.reduce((acc: string[], key: SingleKWarg) => {
+ if (typeof key === "string") {
+ acc.push(key);
+ } else if (key.length > 1 && key[1]) {
+ acc.push(key[0]);
+ }
+ return acc;
+ }, []);
+ return new Set(required);
+ }
+ get optionalKWargs(): Set {
+ if (this.kwargs === AcceptArbitraryArguments) return new Set();
+ const known = this.kwargs.filter(kw => kw !== AcceptArbitraryArguments) as SingleKWarg[];
+ const optional = known.reduce((acc, key: SingleKWarg) => {
+ if (!(typeof key === "string") && !(key.length > 1 && key[1])) {
+ acc.push(key[0]);
+ }
+ return acc;
+ }, []);
+ return new Set(optional);
+ }
+ get acceptingArbitraryKWargs(): boolean {
+ if (this.kwargs === AcceptArbitraryArguments)
+ return true;
+ return (this.kwargs as [...SingleKWarg[], ARBITRARY]) // Wrong type, it might also not contain AcceptArbitraryArguments, but this way typescript doesn't complain
+ .includes(AcceptArbitraryArguments); // This would make perfect sense to me even if kwargs is regarded as SingleKWarg[] | [...SingleKWarg[], ARBITRARY]
+ }
+ get kwargsOrder(): string[] {
+ if (this.kwargs === AcceptArbitraryArguments)
+ return [];
+ const kwargs = this.kwargs.filter(kw => kw !== AcceptArbitraryArguments) as SingleKWarg[]
+ const keywords = kwargs.map((kw: SingleKWarg) => typeof kw === "string" ? kw : kw[0]);
+ return keywords;
+ }
+ getExplicitKWarg(name: string): ExplicitArg {
+ // Get an explicit descriptor of the keyword argument at this index
+ /*if (!this.acceptingArbitraryKWargs && !this.requiredKWargs.has(name) && !this.optionalKWargs.has(name))
+ throw RangeError;*/
+
+ const kwarg: ExplicitArg = {
+ name: null,
+ specifiedName: null,
+ genericName: `${name.slice(0,1).toUpperCase()}${name.slice(1).replace("-", " ")}`,
+ required: null,
+ description: null,
+ unifiedDescription: null,
+ };
+ if (this.kwargs === AcceptArbitraryArguments) {
+ kwarg.required = false;
+ } else if (name) {
+ // Listing all individual arguments style
+ // Iterate over them until we find the argument we are looking for
+ for (const descriptor of this.kwargs) {
+ if (typeof descriptor === "string") {
+ if (descriptor == name) {
+ kwarg.required = true;
+ break;
+ }
+ } else if (typeof descriptor === "object" && "length" in descriptor && descriptor[0] == name) {
+ if (descriptor.length > 1)
+ kwarg.required = descriptor[1];
+ if (descriptor.length > 2)
+ kwarg.description = descriptor[2];
+ if (descriptor.length > 3)
+ kwarg.specifiedName = descriptor[3];
+ break;
+ }
+ }
+ }
+ // Fill in missing details
+ kwarg.name = kwarg.specifiedName !== null ? kwarg.specifiedName : kwarg.genericName;
+ if (kwarg.required === null)
+ kwarg.required = this.requiredKWargs.has(name);
+ if (kwarg.description === null)
+ kwarg.description = ``; // Description stays empty
+ kwarg.unifiedDescription = kwarg.name;
+ if (kwarg.description)
+ kwarg.unifiedDescription += ` – ${kwarg.description}`;
+ return kwarg;
+ }
+
+ lastUnsavedPargs: string[] | null = null;
+ lastUnsavedKWargs: [string, string][] | null = null;
+
+ text(text: TextDescriptor): string {
+ // A helper method to render a string from either a fixed value or a dynamic function
+ if (typeof text === "string") {
+ return text;
+ }
+ return text(this);
+ }
+
+ predicate(node: Element): boolean {
+ // A method determining whether a node in TinyMCE represents this shortcode
+ // (e.g. whether the toolbar specific to this shortcode should be shown)
+ if (!("dataset" in node))
+ return false;
+ const dataset = (node as HTMLElement).dataset;
+ return dataset.shortcode && (dataset.shortcode == this.keyword || !this.keyword);
+ }
+
+ getNode(): HTMLElement | null {
+ // A helper method to get the shortcode node the user has currently selected
+ const node = this.editor.selection.getNode();
+ return this.predicate(node) ? node : null;
+ };
+
+ sortKWargs(kwpairs: Iterable<[string, string]> | [string, string][]): [string, string][] {
+ // A helper method determining a canonical order for keyword arguments
+ const order = this.kwargsOrder;
+ if (!(kwpairs instanceof Array)) kwpairs = Array.from(kwpairs);
+ return (kwpairs as Array<[string, string]>).sort((a, b) => {
+ const aPos = order.includes(a[0]) ? order.indexOf(a[0]) : order.length;
+ const bPos = order.includes(b[0]) ? order.indexOf(b[0]) : order.length;
+ return aPos - bPos;
+ });
+ }
+
+ validate(pargs: string[], kwargs: {[key: string]: string}): boolean {
+ if (pargs.length < this.minPargs || pargs.length > this.maxPargs)
+ return false;
+ // Positional arguments pass!
+
+ if (this.kwargs === AcceptArbitraryArguments)
+ return true; // Early exit if we don't define any required keyword arguments and accept everything
+ const keywords = new Set(Object.keys(kwargs));
+ const requiredKWargs = this.requiredKWargs;
+ // Check if any required keyword arguments are missing
+ if (requiredKWargs.difference(keywords).size > 0)
+ return false;
+ // Check if there are any keyword arguments that are not allowed
+ if (!this.acceptingArbitraryKWargs && keywords.difference(requiredKWargs).difference(this.optionalKWargs).size > 0)
+ return false;
+
+ // All arguments pass!
+ return true;
+ }
+
+ argsFromNode(node: HTMLElement | null): [string[], Map] {
+ let prefix = "parg";
+ const pargs = [...Object.entries(node !== null ? node.dataset : {})].reduce((acc, pair) => {
+ if (pair[0].startsWith(prefix)) {
+ const index = parseInt(pair[0].slice(prefix.length));
+ acc[index] = pair[1];
+ }
+ return acc;
+ }, []);
+ while (pargs.length < this.minPargs) {
+ pargs.push("");
+ }
+
+ prefix = "kw";
+ const kwargs = [...Object.entries(node !== null ? node.dataset : {})].reduce((acc, pair) => {
+ if (pair[0].startsWith(prefix)) {
+ // Revert camelCase transformation automatically done by the dataset api
+ let keyword = pair[0].replace(/[A-Z]/g, c => `-${c.toLowerCase()}`);
+ // Strip the prefix + dash
+ keyword = keyword.slice(prefix.length + 1);
+ acc.push([keyword, pair[1]]);
+ }
+ return acc;
+ }, []);
+
+ return [pargs, new Map(kwargs)];
+ }
+
+ truncateArgs(pargs: string[], kwargs: Map): [string[], Map] {
+ // Ensure the arguments fit the specification
+ let newPargs = [...pargs];
+ const newKWargs = new Map(kwargs);
+
+ // Ensure the correct number of positional arguments
+ if (newPargs.length > this.maxPargs) {
+ newPargs = newPargs.slice(0, this.maxPargs);
+ }
+ while (newPargs.length < this.minPargs) {
+ newPargs.push("");
+ }
+
+ // Ensure all required keywords exist
+ const requiredKWargs = this.requiredKWargs;
+ const optionalKWargs = this.optionalKWargs;
+ requiredKWargs.forEach(kwarg => {
+ if (!newKWargs.has(kwarg)) {
+ newKWargs.set(kwarg, "");
+ }
+ });
+ if (!this.acceptingArbitraryKWargs) {
+ // Ensure no unallowed keywords exist
+ kwargs.forEach((v, kwarg) => {
+ if (!(requiredKWargs.has(kwarg) || optionalKWargs.has(kwarg))) {
+ newKWargs.delete(kwarg);
+ }
+ });
+ }
+
+ return [newPargs, newKWargs];
+ }
+
+ renderShortcode(pargs: string[], kwargs: Map): string {
+ // The canonical text representation of the shortcode
+ const pairs = this.sortKWargs(kwargs.entries()).map(pair => pair.map(ShortcodeHandle.escape).join("="));
+ const parts = [ShortcodeHandle.escape(this.keyword), ...pargs.map(ShortcodeHandle.escape), ...pairs];
+ return `[${parts.join(" ")}]`;
+ }
+
+ renderPreviewNode(pargs: string[], kwargs: Map): string {
+ // The html string representation of the shortcode in the TinyMCE editor
+ // By default this is a span marked with mceNonEditable and the shortcode keyword and parameters
+ // and defers the visual presented to the user to be rendered to renderPreview()
+ const ppairs = pargs.map((arg, i) => `data-parg${i}=${ShortcodeHandle.escape(arg)}`);
+ const kwpairs = this.sortKWargs(kwargs.entries()).map(([key, value]) => `data-kw-${key}=${ShortcodeHandle.escape(value)}`);
+ const parts = [`class="mceNonEditable"`, `data-shortcode="${this.keyword}"`, ...ppairs, ...kwpairs];
+ return `${this.renderPreview(pargs, kwargs)}`;
+ }
+
+ findNodes(): NodeListOf {
+ return this.editor.contentDocument.querySelectorAll(`[data-shortcode="${this.keyword}"]`);
+ }
+
+ renderPreview(pargs: string[], kwargs: Map): string {
+ // The html string representation of the shortcode preview in the TinyMCE editor
+ // By default this is just the canonical text representation. This function will be overwritten by most subclasses.
+ return this.renderShortcode(pargs, kwargs);
+ }
+
+ refreshPreview(predicate: ((pargs: string[], kwargs: Map) => boolean) | undefined) {
+ const previousSelection = this.editor.selection.getBookmark();
+
+ this.findNodes().forEach(((node: HTMLElement) => {
+ const [pargs, kwargs] = this.argsFromNode(node);
+ if (predicate && !predicate(pargs, kwargs)) return;
+
+ this.editor.selection.select(node);
+ node.remove();
+ this.editor.insertContent(this.renderShortcode(pargs, new Map(Object.entries(kwargs))));
+ }).bind(this));
+
+ // Restore selection
+ this.editor.selection.moveToBookmark(previousSelection);
+ }
+
+ reconstructArgsFromDialog(api: DialogInstanceApi): [string[], {[key: string]: string}, [string, string][]] {
+ // Reconstruct the positional and keyword arguments from the form data
+ const data = api.getData();
+ const pargs = Object.entries(data).reduce((acc: string[], [key, value]) => {
+ const match = key.match(/^parg([0-9]+)$/);
+ if (match) {
+ acc[parseInt(match[1])] = value;
+ }
+ return acc;
+ }, []);
+ type TemporaryPairs = {[key: number]: [null | string, null | string]};
+ type FinalizedPairs = {[key: string]: string};
+ type OrderedPairs = [string, string][];
+ const orderedKWargs: OrderedPairs = [];
+ const kwargs = Object.entries(data).reduce((acc: TemporaryPairs & FinalizedPairs, [key, value]) => {
+ // First piece together the names with the values again
+ const match = key.match(/^(kw-(.+)|kwarg([0-9]+)-(name|value))$/);
+ if (match) {
+ if (match[2]) {
+ acc[match[2]] = value;
+ } else {
+ const id = parseInt(match[3]);
+ const which = match[4] == "name" ? 0 : 1;
+ if (acc[id] === undefined) acc[id] = [null, null];
+ acc[id][which] = value;
+ if (acc[id][(which+1) % 2] !== null) {
+ // Pair is complete!
+ [key, value] = acc[id];
+ // Normalize keys to dash-style
+ // This is not its own overridable function because it is already automatically transformed
+ // to dash-style on the marker node representing the shortode to TinyMCE (this is how HTML attributes work)
+ // and to camelCase by the dataset API on JS side.
+ key = key.toLowerCase().replace(/\s+/g, "-");
+ acc[key] = value;
+ orderedKWargs[id] = [key, value];
+ delete acc[id];
+ }
+ }
+ }
+ return acc;
+ }, {}) as unknown as FinalizedPairs;
+ return [pargs, kwargs, orderedKWargs];
+ }
+
+ openEditDialog() {
+ const node = this.getNode();
+ const [initialPargs, initialKWargs] = this.truncateArgs(...this.argsFromNode(node));
+ this.lastUnsavedPargs = initialPargs;
+ this.lastUnsavedKWargs = this.sortKWargs(initialKWargs.entries());
+ return this.displayEditDialog(initialPargs, this.lastUnsavedKWargs);
+ }
+
+ displayEditDialog(initialPargs: string[], initialKWargs: [string, string][]) {
+ // The default implementation for constructing the edit dialog for a generic shortcode
+ const argumentItems: BodyComponentSpec[] = [];
+ initialPargs.forEach((parg: string, i: number) => {
+ const explicit = this.getExplicitParg(i);
+ argumentItems.push.apply(argumentItems, [
+ {
+ type: "label",
+ label: explicit.unifiedDescription,
+ for: `parg${i}`,
+ items: [
+ {
+ type: "bar",
+ items: [
+ {
+ type: "input",
+ name: `parg${i}`,
+ //label: explicit.unifiedDescription,
+ },
+ ...(explicit.required ? [] : [{
+ type: "htmlpanel",
+ html: ``,
+ }] as BodyComponentSpec[]),
+ ],
+ },
+ ],
+ },
+ ]);
+ });
+ if (this.minPargs != this.maxPargs) {
+ argumentItems.push({
+ type: "htmlpanel",
+ html: ``.replace(/\s+/g, " "),
+ });
+ }
+ initialKWargs.forEach(([keyword, value]: [string, string], i: number) => {
+ const explicit = this.getExplicitKWarg(keyword);
+ if (this.acceptingArbitraryKWargs) {
+ argumentItems.push.apply(argumentItems, [
+ {
+ type: "label",
+ label: explicit.description || `Value`,
+ for: `parg${i}`,
+ items: [
+ {
+ type: "bar",
+ items: [
+ {
+ type: "input",
+ name: `kwarg${i}-name`,
+ label: explicit.name,
+ },
+ {
+ type: "input",
+ name: `kwarg${i}-value`,
+ //label: explicit.description || `Value`,
+ },
+ ...(explicit.required ? [] : [{
+ type: "htmlpanel",
+ html: ``,
+ }] as BodyComponentSpec[]),
+ ],
+ },
+ ],
+ },
+ ]);
+ } else {
+ argumentItems.push({
+ type: "label",
+ label: explicit.description || `Value`,
+ for: `parg${i}`,
+ items: [
+ {
+ type: "bar",
+ items: [
+ {
+ type: "input",
+ name: `kw-${keyword}`,
+ label: explicit.unifiedDescription,
+ },
+ ...(explicit.required ? [] : [{
+ type: "htmlpanel",
+ html: ``,
+ }] as BodyComponentSpec[]),
+ ],
+ },
+ ],
+ });
+ }
+ });
+ const optionalKWargs = this.optionalKWargs;
+ if (this.acceptingArbitraryKWargs || optionalKWargs.size > 0) {
+ const keys = new Set(initialKWargs.map(([key, value]) => key));
+ const missingRequired = this.requiredKWargs.difference(keys);
+ const missingOptional = optionalKWargs.difference(keys);
+ const givenOptional = optionalKWargs.intersection(keys);
+ argumentItems.push({
+ type: "bar",
+ items: [
+ {
+ type: "htmlpanel",
+ html: ``.replace(/\s+/g, " "),
+ },
+ ],
+ });
+ }
+
+ const dialogConfig: DialogSpec = {
+ title: this.text(this.editText),
+ body: {
+ type: "panel",
+ items: argumentItems,
+ },
+ buttons: [
+ {
+ type: "cancel",
+ text: this.tinymceConfig.getAttribute("data-dialog-cancel-text"),
+ },
+ {
+ type: "submit",
+ name: "submit",
+ text: this.tinymceConfig.getAttribute("data-dialog-submit-text"),
+ primary: true,
+ },
+ ],
+ initialData: this.defaultInitialData(initialPargs, initialKWargs),
+ onSubmit: this.defaultOnSubmit.bind(this),
+ onChange: this.defaultOnChange.bind(this),
+ };
+ console.log(`[${this.keyword}]`, this, dialogConfig);
+
+ setTimeout(this.defaultEditDialogRefinement.bind(this), 0);
+
+ return this.editor.windowManager.open(dialogConfig);
+ }
+
+ defaultInitialData(initialPargs: string[], initialKWargs: [string, string][]) {
+ return {
+ ...Object.fromEntries(initialPargs.map((parg: string, i: number) => [`parg${i}`, parg])),
+ ...Object.fromEntries(initialKWargs.reduce((acc, [keyword, value], i) => {
+ if (this.acceptingArbitraryKWargs) {
+ return acc.concat([
+ [`kwarg${i}-name`, keyword],
+ [`kwarg${i}-value`, value],
+ ]);
+ } else {
+ return acc.concat([
+ [`kw-${keyword}`, value],
+ ]);
+ }
+ }, [])),
+ };
+ }
+
+ defaultOnSubmit(api: DialogInstanceApi) {
+ // The default submit handler for the edit dialog
+ const [pargs, kwargs, orderedKWargs] = this.reconstructArgsFromDialog(api);
+ this.lastUnsavedPargs = pargs;
+ this.lastUnsavedKWargs = orderedKWargs;
+
+ // Don't close the dialog if the arguments are not valid
+ if (!this.validate(pargs, kwargs))
+ return console.error(`invalid arguments:`, pargs, kwargs);
+
+ api.close();
+ this.lastUnsavedPargs = null;
+ this.lastUnsavedKWargs = null;
+
+ // Either insert a new shortcode or update the existing one
+ const node = this.getNode();
+ if (!node) {
+ this.editor.insertContent(this.renderShortcode(pargs, new Map(Object.entries(kwargs))));
+ } else {
+ node.remove();
+ this.editor.insertContent(this.renderShortcode(pargs, new Map(Object.entries(kwargs))));
+ }
+ }
+
+ defaultOnChange(api: DialogInstanceApi) {
+ // The default change handler for the edit dialog
+ const [pargs, kwargs, orderedKWargs] = this.reconstructArgsFromDialog(api);
+ this.lastUnsavedPargs = pargs;
+ this.lastUnsavedKWargs = orderedKWargs;
+ }
+
+ defaultEditDialogRefinement() {
+ // The default function invoked after rendering the edit dialog,
+ // e.g. to manipulate its HTML in a way TinyMCEs DialogSpec doesn't support
+ const dialog = document.querySelector('.tox-dialog');
+ const pargRemove = dialog.querySelector('#parg-remove');
+ const pargAdd = dialog.querySelector('#parg-add');
+ if (pargRemove) {
+ pargRemove.addEventListener("click", (() => {
+ if (this.lastUnsavedPargs === null || this.lastUnsavedPargs.length <= this.minPargs)
+ return;
+ this.lastUnsavedPargs.pop();
+ this.editor.windowManager.close();
+ this.displayEditDialog(this.lastUnsavedPargs, this.lastUnsavedKWargs);
+ }).bind(this));
+ }
+ if (pargAdd) {
+ pargAdd.addEventListener("click", (() => {
+ if (this.lastUnsavedPargs === null || this.lastUnsavedPargs.length >= this.maxPargs)
+ return;
+ this.lastUnsavedPargs.push("");
+ this.editor.windowManager.close();
+ this.displayEditDialog(this.lastUnsavedPargs, this.lastUnsavedKWargs);
+ }).bind(this));
+ }
+ const kwargRemove = dialog.querySelector('#kwarg-remove');
+ const kwargAdd = dialog.querySelector('#kwarg-add');
+ if (kwargRemove) {
+ kwargRemove.addEventListener("click", (() => {
+ if (this.lastUnsavedKWargs === null)
+ return;
+ const requiredKWargs = this.requiredKWargs;
+ // Throw away the last keyword that is not required
+ for (let i = this.lastUnsavedKWargs.length-1; i >= 0; i--) {
+ if (requiredKWargs.has(this.lastUnsavedKWargs[i][0]))
+ continue;
+ const beforeThis = this.lastUnsavedKWargs.slice(0, i);
+ const afterThis = this.lastUnsavedKWargs.slice(i+1, this.lastUnsavedKWargs.length);
+ this.lastUnsavedKWargs = beforeThis.concat(afterThis);
+ break;
+ };
+ this.editor.windowManager.close();
+ this.displayEditDialog(this.lastUnsavedPargs, this.lastUnsavedKWargs);
+ }).bind(this));
+ }
+ if (kwargAdd) {
+ kwargAdd.addEventListener("click", (() => {
+ if (this.lastUnsavedKWargs === null)
+ return;
+ // A flat list of known keywords in canonical order
+ const order = this.kwargsOrder;
+ function index(x: string): number {
+ // Determine the canonical position of the keyword
+ const i = order.indexOf(x);
+ if (i == -1) return Infinity; // If the keyword is unknown, sort it last
+ return i;
+ }
+ const requiredKWargs = this.requiredKWargs;
+ const keys = new Set(this.lastUnsavedKWargs.map(([key, value]) => key));
+ const missingRequired = requiredKWargs.difference(keys);
+ let key;
+ if (missingRequired.size > 0) {
+ // Somehow, required keywords are missing. Add the first one by canonical order
+ key = Array.from(missingRequired).sort((a, b) => index(a) - index(b))[0];
+ } else {
+ const optionalKWargs = this.optionalKWargs;
+ const missingOptional = optionalKWargs.difference(keys);
+ if (missingOptional.size > 0) {
+ // Add the first known optional argument by canonical order. If we don't know any and we accept arbitrary arguments, leave the key empty
+ key = Array.from(missingOptional).sort((a, b) => index(a) - index(b))[0] || "";
+ } else if (this.acceptingArbitraryKWargs) {
+ key = "";
+ } else
+ return; // There are no arguments left to add, immediately stop without doing anything
+ }
+ // Finally, actually append the key value pair and retrigger the dialog
+ this.lastUnsavedKWargs.push([key, ""]);
+ this.editor.windowManager.close();
+ this.displayEditDialog(this.lastUnsavedPargs, this.lastUnsavedKWargs);
+ }).bind(this));
+ }
+ }
+
+ setup(editor: Editor): boolean {
+ /* Method to initialize the instance for an editor.
+ * Only if setup() returns true, the shortcode will be enabled.
+ */
+ /* default behavior:
+ * - menu item to insert shortcode → open dialog
+ * - context toolbar with edit and delete
+ */
+ this.editor = editor;
+ this.tinymceConfig = document.getElementById("tinymce-config-options");
+
+ const closeContextToolbar = () => {
+ editor.dispatch("contexttoolbar-hide", {
+ toolbarKey: `shortcode_${this.keyword}_context_form`,
+ });
+ };
+
+ editor.ui.registry.addMenuItem(`add_shortcode_${this.keyword}`, {
+ text: this.text(this.addText),
+ icon: this.addIcon,
+ onAction: this.openEditDialog.bind(this),
+ });
+
+ editor.ui.registry.addButton(`edit_shortcode_${this.keyword}`, {
+ text: this.text(this.editText),
+ tooltip: this.text(this.editText),
+ icon: this.editIcon,
+ onAction: ((api: ToolbarButtonInstanceApi) => {
+ this.openEditDialog();
+ closeContextToolbar();
+ }).bind(this),
+ });
+
+ editor.ui.registry.addButton(`remove_shortcode_${this.keyword}`, {
+ text: this.text(this.removeText),
+ tooltip: this.text(this.removeText),
+ icon: this.removeIcon,
+ onAction: (() => {
+ const node = this.getNode();
+ if (node) {
+ node.remove();
+ }
+ closeContextToolbar();
+ }).bind(this),
+ });
+
+ // This form opens when a shortcode is currently selected with the cursor
+ editor.ui.registry.addContextToolbar(`shortcode_${this.keyword}_context_form`, {
+ predicate: this.predicate.bind(this),
+ position: "node",
+ scope: "node",
+ items: `edit_shortcode_${this.keyword} remove_shortcode_${this.keyword}`,
+ });
+
+ return true;
+ }
+}
+
+
+class Registry {
+ static #instance: Registry;
+
+ handles: Map;
+ unknownHandleFactory: null | ((keyword: string) => ShortcodeHandle);
+
+ private constructor() {
+ this.handles = new Map();
+ this.unknownHandleFactory = null;
+ }
+
+ public static get instance(): Registry {
+ if (!Registry.#instance) {
+ Registry.#instance = new Registry();
+ }
+ return Registry.#instance;
+ }
+
+ public static has(keyword: string): boolean {
+ return Registry.instance.handles.has(keyword);
+ }
+
+ public static get(keyword: string): ShortcodeHandle | null {
+ return Registry.instance.handles.get(keyword);
+ }
+
+ public static register(handle: ShortcodeHandle) {
+ if (Registry.has(handle.keyword)) {
+ throw Error(`Keyword ${handle.keyword} already registered as ${Registry.get(handle.keyword)}`);
+ }
+ Registry.instance.handles.set(handle.keyword, handle);
+ }
+ public static unregister(handle: ShortcodeHandle | string): ShortcodeHandle {
+ const keyword = handle instanceof ShortcodeHandle ? handle.keyword : handle;
+ const old_handle = Registry.get(keyword);
+ if (handle instanceof ShortcodeHandle && old_handle !== handle) {
+ throw Error(`Keyword ${keyword} registered as a different handle: ${old_handle}`);
+ }
+ Registry.instance.handles.delete(keyword);
+ return old_handle;
+ }
+ public static unregisterAll() {
+ Registry.instance.handles.clear();
+ }
+
+ public static setUnknownHandleFactory(factory: (keyword: string) => ShortcodeHandle) {
+ this.instance.unknownHandleFactory = factory;
+ }
+
+ public static setupAll(editor: Editor, parser: Parser) {
+ Registry.instance.handles.forEach((value: ShortcodeHandle, key: string) => {
+ if (value.setup(editor)) {
+ parser.register(value.renderPreviewNode.bind(value), key, value.endword);
+ }
+ });
+ if (this.instance.unknownHandleFactory !== null) {
+ parser.setUnknownHandlerFactory((keyword: string) => {
+ const handle = this.instance.unknownHandleFactory(keyword);
+ const fn = handle.renderPreviewNode.bind(handle);
+ Registry.register(handle);
+ handle.setup(editor);
+ parser.register(fn, keyword);
+ console.log(`created and registered dummy handler for ${keyword}`);
+ return fn;
+ });
+ } else {
+ parser.setUnknownHandlerFactory(null);
+ }
+ }
+}
+
+
+export { ShortcodeHandle, Registry, AcceptArbitraryArguments, TextDescriptor, SingleParg, PargsDescriptor, SingleKWarg, KWargsDescriptor, ExplicitArg };
diff --git a/integreat_cms/static/src/js/tinymce-plugins/tinymce.d.ts b/integreat_cms/static/src/js/tinymce-plugins/tinymce.d.ts
new file mode 100644
index 0000000000..facc106ca8
--- /dev/null
+++ b/integreat_cms/static/src/js/tinymce-plugins/tinymce.d.ts
@@ -0,0 +1,3863 @@
+interface StringPathBookmark {
+ start: string;
+ end?: string;
+ forward?: boolean;
+}
+interface RangeBookmark {
+ rng: Range;
+ forward?: boolean;
+}
+interface IdBookmark {
+ id: string;
+ keep?: boolean;
+ forward?: boolean;
+}
+interface IndexBookmark {
+ name: string;
+ index: number;
+}
+interface PathBookmark {
+ start: number[];
+ end?: number[];
+ isFakeCaret?: boolean;
+ forward?: boolean;
+}
+type Bookmark = StringPathBookmark | RangeBookmark | IdBookmark | IndexBookmark | PathBookmark;
+type NormalizedEvent = E & {
+ readonly type: string;
+ readonly target: T;
+ readonly isDefaultPrevented: () => boolean;
+ readonly preventDefault: () => void;
+ readonly isPropagationStopped: () => boolean;
+ readonly stopPropagation: () => void;
+ readonly isImmediatePropagationStopped: () => boolean;
+ readonly stopImmediatePropagation: () => void;
+};
+type MappedEvent = K extends keyof T ? T[K] : any;
+interface NativeEventMap {
+ 'beforepaste': Event;
+ 'blur': FocusEvent;
+ 'beforeinput': InputEvent;
+ 'click': MouseEvent;
+ 'compositionend': Event;
+ 'compositionstart': Event;
+ 'compositionupdate': Event;
+ 'contextmenu': PointerEvent;
+ 'copy': ClipboardEvent;
+ 'cut': ClipboardEvent;
+ 'dblclick': MouseEvent;
+ 'drag': DragEvent;
+ 'dragdrop': DragEvent;
+ 'dragend': DragEvent;
+ 'draggesture': DragEvent;
+ 'dragover': DragEvent;
+ 'dragstart': DragEvent;
+ 'drop': DragEvent;
+ 'focus': FocusEvent;
+ 'focusin': FocusEvent;
+ 'focusout': FocusEvent;
+ 'input': InputEvent;
+ 'keydown': KeyboardEvent;
+ 'keypress': KeyboardEvent;
+ 'keyup': KeyboardEvent;
+ 'mousedown': MouseEvent;
+ 'mouseenter': MouseEvent;
+ 'mouseleave': MouseEvent;
+ 'mousemove': MouseEvent;
+ 'mouseout': MouseEvent;
+ 'mouseover': MouseEvent;
+ 'mouseup': MouseEvent;
+ 'paste': ClipboardEvent;
+ 'selectionchange': Event;
+ 'submit': Event;
+ 'touchend': TouchEvent;
+ 'touchmove': TouchEvent;
+ 'touchstart': TouchEvent;
+ 'touchcancel': TouchEvent;
+ 'wheel': WheelEvent;
+}
+type EditorEvent = NormalizedEvent;
+interface EventDispatcherSettings {
+ scope?: any;
+ toggleEvent?: (name: string, state: boolean) => void | boolean;
+ beforeFire?: (args: EditorEvent) => void;
+}
+interface EventDispatcherConstructor {
+ readonly prototype: EventDispatcher;
+ new (settings?: EventDispatcherSettings): EventDispatcher;
+ isNative: (name: string) => boolean;
+}
+declare class EventDispatcher {
+ static isNative(name: string): boolean;
+ private readonly settings;
+ private readonly scope;
+ private readonly toggleEvent;
+ private bindings;
+ constructor(settings?: EventDispatcherSettings);
+ fire>(name: K, args?: U): EditorEvent;
+ dispatch>(name: K, args?: U): EditorEvent;
+ on(name: K, callback: false | ((event: EditorEvent>) => void | boolean), prepend?: boolean, extra?: {}): this;
+ off(name?: K, callback?: (event: EditorEvent>) => void): this;
+ once(name: K, callback: (event: EditorEvent>) => void, prepend?: boolean): this;
+ has(name: string): boolean;
+}
+type UndoLevelType = 'fragmented' | 'complete';
+interface BaseUndoLevel {
+ type: UndoLevelType;
+ bookmark: Bookmark | null;
+ beforeBookmark: Bookmark | null;
+}
+interface FragmentedUndoLevel extends BaseUndoLevel {
+ type: 'fragmented';
+ fragments: string[];
+ content: '';
+}
+interface CompleteUndoLevel extends BaseUndoLevel {
+ type: 'complete';
+ fragments: null;
+ content: string;
+}
+type NewUndoLevel = CompleteUndoLevel | FragmentedUndoLevel;
+type UndoLevel = NewUndoLevel & {
+ bookmark: Bookmark;
+};
+interface UndoManager {
+ data: UndoLevel[];
+ typing: boolean;
+ add: (level?: Partial, event?: EditorEvent) => UndoLevel | null;
+ dispatchChange: () => void;
+ beforeChange: () => void;
+ undo: () => UndoLevel | undefined;
+ redo: () => UndoLevel | undefined;
+ clear: () => void;
+ reset: () => void;
+ hasUndo: () => boolean;
+ hasRedo: () => boolean;
+ transact: (callback: () => void) => UndoLevel | null;
+ ignore: (callback: () => void) => void;
+ extra: (callback1: () => void, callback2: () => void) => void;
+}
+type SchemaType = 'html4' | 'html5' | 'html5-strict';
+interface ElementSettings {
+ block_elements?: string;
+ boolean_attributes?: string;
+ move_caret_before_on_enter_elements?: string;
+ non_empty_elements?: string;
+ self_closing_elements?: string;
+ text_block_elements?: string;
+ text_inline_elements?: string;
+ void_elements?: string;
+ whitespace_elements?: string;
+ transparent_elements?: string;
+ wrap_block_elements?: string;
+}
+interface SchemaSettings extends ElementSettings {
+ custom_elements?: string | Record;
+ extended_valid_elements?: string;
+ invalid_elements?: string;
+ invalid_styles?: string | Record;
+ schema?: SchemaType;
+ valid_children?: string;
+ valid_classes?: string | Record;
+ valid_elements?: string;
+ valid_styles?: string | Record;
+ verify_html?: boolean;
+ padd_empty_block_inline_children?: boolean;
+}
+interface Attribute {
+ required?: boolean;
+ defaultValue?: string;
+ forcedValue?: string;
+ validValues?: Record;
+}
+interface DefaultAttribute {
+ name: string;
+ value: string;
+}
+interface AttributePattern extends Attribute {
+ pattern: RegExp;
+}
+interface ElementRule {
+ attributes: Record;
+ attributesDefault?: DefaultAttribute[];
+ attributesForced?: DefaultAttribute[];
+ attributesOrder: string[];
+ attributePatterns?: AttributePattern[];
+ attributesRequired?: string[];
+ paddEmpty?: boolean;
+ removeEmpty?: boolean;
+ removeEmptyAttrs?: boolean;
+ paddInEmptyBlock?: boolean;
+}
+interface SchemaElement extends ElementRule {
+ outputName?: string;
+ parentsRequired?: string[];
+ pattern?: RegExp;
+}
+interface SchemaMap {
+ [name: string]: {};
+}
+interface SchemaRegExpMap {
+ [name: string]: RegExp;
+}
+interface CustomElementSpec {
+ extends?: string;
+ attributes?: string[];
+ children?: string[];
+ padEmpty?: boolean;
+}
+interface Schema {
+ type: SchemaType;
+ children: Record;
+ elements: Record;
+ getValidStyles: () => Record | undefined;
+ getValidClasses: () => Record | undefined;
+ getBlockElements: () => SchemaMap;
+ getInvalidStyles: () => Record | undefined;
+ getVoidElements: () => SchemaMap;
+ getTextBlockElements: () => SchemaMap;
+ getTextInlineElements: () => SchemaMap;
+ getBoolAttrs: () => SchemaMap;
+ getElementRule: (name: string) => SchemaElement | undefined;
+ getSelfClosingElements: () => SchemaMap;
+ getNonEmptyElements: () => SchemaMap;
+ getMoveCaretBeforeOnEnterElements: () => SchemaMap;
+ getWhitespaceElements: () => SchemaMap;
+ getTransparentElements: () => SchemaMap;
+ getSpecialElements: () => SchemaRegExpMap;
+ isValidChild: (name: string, child: string) => boolean;
+ isValid: (name: string, attr?: string) => boolean;
+ isBlock: (name: string) => boolean;
+ isInline: (name: string) => boolean;
+ isWrapper: (name: string) => boolean;
+ getCustomElements: () => SchemaMap;
+ addValidElements: (validElements: string) => void;
+ setValidElements: (validElements: string) => void;
+ addCustomElements: (customElements: string | Record) => void;
+ addValidChildren: (validChildren: any) => void;
+}
+type Attributes$1 = Array<{
+ name: string;
+ value: string;
+}> & {
+ map: Record;
+};
+interface AstNodeConstructor {
+ readonly prototype: AstNode;
+ new (name: string, type: number): AstNode;
+ create(name: string, attrs?: Record): AstNode;
+}
+declare class AstNode {
+ static create(name: string, attrs?: Record): AstNode;
+ name: string;
+ type: number;
+ attributes?: Attributes$1;
+ value?: string;
+ parent?: AstNode | null;
+ firstChild?: AstNode | null;
+ lastChild?: AstNode | null;
+ next?: AstNode | null;
+ prev?: AstNode | null;
+ raw?: boolean;
+ constructor(name: string, type: number);
+ replace(node: AstNode): AstNode;
+ attr(name: string, value: string | null | undefined): AstNode | undefined;
+ attr(name: Record | undefined): AstNode | undefined;
+ attr(name: string): string | undefined;
+ clone(): AstNode;
+ wrap(wrapper: AstNode): AstNode;
+ unwrap(): void;
+ remove(): AstNode;
+ append(node: AstNode): AstNode;
+ insert(node: AstNode, refNode: AstNode, before?: boolean): AstNode;
+ getAll(name: string): AstNode[];
+ children(): AstNode[];
+ empty(): AstNode;
+ isEmpty(elements: SchemaMap, whitespace?: SchemaMap, predicate?: (node: AstNode) => boolean): boolean;
+ walk(prev?: boolean): AstNode | null | undefined;
+}
+type Content = string | AstNode;
+type ContentFormat = 'raw' | 'text' | 'html' | 'tree';
+interface GetContentArgs {
+ format: ContentFormat;
+ get: boolean;
+ getInner: boolean;
+ no_events?: boolean;
+ save?: boolean;
+ source_view?: boolean;
+ [key: string]: any;
+}
+interface SetContentArgs {
+ format: string;
+ set: boolean;
+ content: Content;
+ no_events?: boolean;
+ no_selection?: boolean;
+ paste?: boolean;
+ load?: boolean;
+ initial?: boolean;
+ [key: string]: any;
+}
+interface GetSelectionContentArgs extends GetContentArgs {
+ selection?: boolean;
+ contextual?: boolean;
+}
+interface SetSelectionContentArgs extends SetContentArgs {
+ content: string;
+ selection?: boolean;
+}
+interface BlobInfoData {
+ id?: string;
+ name?: string;
+ filename?: string;
+ blob: Blob;
+ base64: string;
+ blobUri?: string;
+ uri?: string;
+}
+interface BlobInfo {
+ id: () => string;
+ name: () => string;
+ filename: () => string;
+ blob: () => Blob;
+ base64: () => string;
+ blobUri: () => string;
+ uri: () => string | undefined;
+}
+interface BlobCache {
+ create: {
+ (o: BlobInfoData): BlobInfo;
+ (id: string, blob: Blob, base64: string, name?: string, filename?: string): BlobInfo;
+ };
+ add: (blobInfo: BlobInfo) => void;
+ get: (id: string) => BlobInfo | undefined;
+ getByUri: (blobUri: string) => BlobInfo | undefined;
+ getByData: (base64: string, type: string) => BlobInfo | undefined;
+ findFirst: (predicate: (blobInfo: BlobInfo) => boolean) => BlobInfo | undefined;
+ removeByUri: (blobUri: string) => void;
+ destroy: () => void;
+}
+interface BlobInfoImagePair {
+ image: HTMLImageElement;
+ blobInfo: BlobInfo;
+}
+declare class NodeChange {
+ private readonly editor;
+ private lastPath;
+ constructor(editor: Editor);
+ nodeChanged(args?: Record): void;
+ private isSameElementPath;
+}
+interface SelectionOverrides {
+ showCaret: (direction: number, node: HTMLElement, before: boolean, scrollIntoView?: boolean) => Range | null;
+ showBlockCaretContainer: (blockCaretContainer: HTMLElement) => void;
+ hideFakeCaret: () => void;
+ destroy: () => void;
+}
+interface Quirks {
+ refreshContentEditable(): void;
+ isHidden(): boolean;
+}
+type DecoratorData = Record;
+type Decorator = (uid: string, data: DecoratorData) => {
+ attributes?: {};
+ classes?: string[];
+};
+type AnnotationListener = (state: boolean, name: string, data?: {
+ uid: string;
+ nodes: any[];
+}) => void;
+type AnnotationListenerApi = AnnotationListener;
+interface AnnotatorSettings {
+ decorate: Decorator;
+ persistent?: boolean;
+}
+interface Annotator {
+ register: (name: string, settings: AnnotatorSettings) => void;
+ annotate: (name: string, data: DecoratorData) => void;
+ annotationChanged: (name: string, f: AnnotationListenerApi) => void;
+ remove: (name: string) => void;
+ removeAll: (name: string) => void;
+ getAll: (name: string) => Record;
+}
+interface IsEmptyOptions {
+ readonly skipBogus?: boolean;
+ readonly includeZwsp?: boolean;
+ readonly checkRootAsContent?: boolean;
+ readonly isContent?: (node: Node) => boolean;
+}
+interface GeomRect {
+ readonly x: number;
+ readonly y: number;
+ readonly w: number;
+ readonly h: number;
+}
+interface Rect {
+ inflate: (rect: GeomRect, w: number, h: number) => GeomRect;
+ relativePosition: (rect: GeomRect, targetRect: GeomRect, rel: string) => GeomRect;
+ findBestRelativePosition: (rect: GeomRect, targetRect: GeomRect, constrainRect: GeomRect, rels: string[]) => string | null;
+ intersect: (rect: GeomRect, cropRect: GeomRect) => GeomRect | null;
+ clamp: (rect: GeomRect, clampRect: GeomRect, fixedSize?: boolean) => GeomRect;
+ create: (x: number, y: number, w: number, h: number) => GeomRect;
+ fromClientRect: (clientRect: DOMRect) => GeomRect;
+}
+interface NotificationManagerImpl {
+ open: (spec: NotificationSpec, closeCallback: () => void, hasEditorFocus: () => boolean) => NotificationApi;
+ close: (notification: T) => void;
+ getArgs: (notification: T) => NotificationSpec;
+}
+interface NotificationSpec {
+ type?: 'info' | 'warning' | 'error' | 'success';
+ text: string;
+ icon?: string;
+ progressBar?: boolean;
+ timeout?: number;
+}
+interface NotificationApi {
+ close: () => void;
+ progressBar: {
+ value: (percent: number) => void;
+ };
+ text: (text: string) => void;
+ reposition: () => void;
+ getEl: () => HTMLElement;
+ settings: NotificationSpec;
+}
+interface NotificationManager {
+ open: (spec: NotificationSpec) => NotificationApi;
+ close: () => void;
+ getNotifications: () => NotificationApi[];
+}
+interface UploadFailure {
+ message: string;
+ remove?: boolean;
+}
+type ProgressFn = (percent: number) => void;
+type UploadHandler = (blobInfo: BlobInfo, progress: ProgressFn) => Promise;
+interface UploadResult$2 {
+ url: string;
+ blobInfo: BlobInfo;
+ status: boolean;
+ error?: UploadFailure;
+}
+type BlockPatternTrigger = 'enter' | 'space';
+interface RawPattern {
+ start?: any;
+ end?: any;
+ format?: any;
+ cmd?: any;
+ value?: any;
+ replacement?: any;
+ trigger?: BlockPatternTrigger;
+}
+interface InlineBasePattern {
+ readonly start: string;
+ readonly end: string;
+}
+interface InlineFormatPattern extends InlineBasePattern {
+ readonly type: 'inline-format';
+ readonly format: string[];
+}
+interface InlineCmdPattern extends InlineBasePattern {
+ readonly type: 'inline-command';
+ readonly cmd: string;
+ readonly value?: any;
+}
+type InlinePattern = InlineFormatPattern | InlineCmdPattern;
+interface BlockBasePattern {
+ readonly start: string;
+ readonly trigger: BlockPatternTrigger;
+}
+interface BlockFormatPattern extends BlockBasePattern {
+ readonly type: 'block-format';
+ readonly format: string;
+}
+interface BlockCmdPattern extends BlockBasePattern {
+ readonly type: 'block-command';
+ readonly cmd: string;
+ readonly value?: any;
+}
+type BlockPattern = BlockFormatPattern | BlockCmdPattern;
+type Pattern = InlinePattern | BlockPattern;
+interface DynamicPatternContext {
+ readonly text: string;
+ readonly block: Element;
+}
+type DynamicPatternsLookup = (ctx: DynamicPatternContext) => Pattern[];
+type RawDynamicPatternsLookup = (ctx: DynamicPatternContext) => RawPattern[];
+interface AlertBannerSpec {
+ type: 'alertbanner';
+ level: 'info' | 'warn' | 'error' | 'success';
+ text: string;
+ icon: string;
+ url?: string;
+}
+interface ButtonSpec {
+ type: 'button';
+ text: string;
+ enabled?: boolean;
+ primary?: boolean;
+ name?: string;
+ icon?: string;
+ borderless?: boolean;
+ buttonType?: 'primary' | 'secondary' | 'toolbar';
+}
+interface FormComponentSpec {
+ type: string;
+ name: string;
+}
+interface FormComponentWithLabelSpec extends FormComponentSpec {
+ label?: string;
+}
+interface CheckboxSpec extends FormComponentSpec {
+ type: 'checkbox';
+ label: string;
+ enabled?: boolean;
+}
+interface CollectionSpec extends FormComponentWithLabelSpec {
+ type: 'collection';
+}
+interface CollectionItem {
+ value: string;
+ text: string;
+ icon: string;
+}
+interface ColorInputSpec extends FormComponentWithLabelSpec {
+ type: 'colorinput';
+ storageKey?: string;
+}
+interface ColorPickerSpec extends FormComponentWithLabelSpec {
+ type: 'colorpicker';
+}
+interface CustomEditorInit {
+ setValue: (value: string) => void;
+ getValue: () => string;
+ destroy: () => void;
+}
+type CustomEditorInitFn = (elm: HTMLElement, settings: any) => Promise;
+interface CustomEditorOldSpec extends FormComponentSpec {
+ type: 'customeditor';
+ tag?: string;
+ init: (e: HTMLElement) => Promise;
+}
+interface CustomEditorNewSpec extends FormComponentSpec {
+ type: 'customeditor';
+ tag?: string;
+ scriptId: string;
+ scriptUrl: string;
+ onFocus?: (e: HTMLElement) => void;
+ settings?: any;
+}
+type CustomEditorSpec = CustomEditorOldSpec | CustomEditorNewSpec;
+interface DropZoneSpec extends FormComponentWithLabelSpec {
+ type: 'dropzone';
+}
+interface GridSpec {
+ type: 'grid';
+ columns: number;
+ items: BodyComponentSpec[];
+}
+interface HtmlPanelSpec {
+ type: 'htmlpanel';
+ html: string;
+ onInit?: (el: HTMLElement) => void;
+ presets?: 'presentation' | 'document';
+ stretched?: boolean;
+}
+interface IframeSpec extends FormComponentWithLabelSpec {
+ type: 'iframe';
+ border?: boolean;
+ sandboxed?: boolean;
+ streamContent?: boolean;
+ transparent?: boolean;
+}
+interface ImagePreviewSpec extends FormComponentSpec {
+ type: 'imagepreview';
+ height?: string;
+}
+interface InputSpec extends FormComponentWithLabelSpec {
+ type: 'input';
+ inputMode?: string;
+ placeholder?: string;
+ maximized?: boolean;
+ enabled?: boolean;
+}
+type Alignment = 'start' | 'center' | 'end';
+interface LabelSpec {
+ type: 'label';
+ label: string;
+ items: BodyComponentSpec[];
+ align?: Alignment;
+ for?: string;
+}
+interface ListBoxSingleItemSpec {
+ text: string;
+ value: string;
+}
+interface ListBoxNestedItemSpec {
+ text: string;
+ items: ListBoxItemSpec[];
+}
+type ListBoxItemSpec = ListBoxNestedItemSpec | ListBoxSingleItemSpec;
+interface ListBoxSpec extends FormComponentWithLabelSpec {
+ type: 'listbox';
+ items: ListBoxItemSpec[];
+ disabled?: boolean;
+}
+interface PanelSpec {
+ type: 'panel';
+ classes?: string[];
+ items: BodyComponentSpec[];
+}
+interface SelectBoxItemSpec {
+ text: string;
+ value: string;
+}
+interface SelectBoxSpec extends FormComponentWithLabelSpec {
+ type: 'selectbox';
+ items: SelectBoxItemSpec[];
+ size?: number;
+ enabled?: boolean;
+}
+interface SizeInputSpec extends FormComponentWithLabelSpec {
+ type: 'sizeinput';
+ constrain?: boolean;
+ enabled?: boolean;
+}
+interface SliderSpec extends FormComponentSpec {
+ type: 'slider';
+ label: string;
+ min?: number;
+ max?: number;
+}
+interface TableSpec {
+ type: 'table';
+ header: string[];
+ cells: string[][];
+}
+interface TextAreaSpec extends FormComponentWithLabelSpec {
+ type: 'textarea';
+ placeholder?: string;
+ maximized?: boolean;
+ enabled?: boolean;
+}
+interface BaseToolbarButtonSpec {
+ enabled?: boolean;
+ tooltip?: string;
+ icon?: string;
+ text?: string;
+ onSetup?: (api: I) => (api: I) => void;
+}
+interface BaseToolbarButtonInstanceApi {
+ isEnabled: () => boolean;
+ setEnabled: (state: boolean) => void;
+ setText: (text: string) => void;
+ setIcon: (icon: string) => void;
+}
+interface ToolbarButtonSpec extends BaseToolbarButtonSpec {
+ type?: 'button';
+ onAction: (api: ToolbarButtonInstanceApi) => void;
+ shortcut?: string;
+}
+interface ToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {
+}
+interface ToolbarGroupSetting {
+ name: string;
+ items: string[];
+}
+type ToolbarConfig = string | ToolbarGroupSetting[];
+interface GroupToolbarButtonInstanceApi extends BaseToolbarButtonInstanceApi {
+}
+interface GroupToolbarButtonSpec extends BaseToolbarButtonSpec {
+ type?: 'grouptoolbarbutton';
+ items?: ToolbarConfig;
+}
+interface CardImageSpec {
+ type: 'cardimage';
+ src: string;
+ alt?: string;
+ classes?: string[];
+}
+interface CardTextSpec {
+ type: 'cardtext';
+ text: string;
+ name?: string;
+ classes?: string[];
+}
+type CardItemSpec = CardContainerSpec | CardImageSpec | CardTextSpec;
+type CardContainerDirection = 'vertical' | 'horizontal';
+type CardContainerAlign = 'left' | 'right';
+type CardContainerValign = 'top' | 'middle' | 'bottom';
+interface CardContainerSpec {
+ type: 'cardcontainer';
+ items: CardItemSpec[];
+ direction?: CardContainerDirection;
+ align?: CardContainerAlign;
+ valign?: CardContainerValign;
+}
+interface CommonMenuItemSpec {
+ enabled?: boolean;
+ text?: string;
+ value?: string;
+ meta?: Record;
+ shortcut?: string;
+}
+interface CommonMenuItemInstanceApi {
+ isEnabled: () => boolean;
+ setEnabled: (state: boolean) => void;
+}
+interface CardMenuItemInstanceApi extends CommonMenuItemInstanceApi {
+}
+interface CardMenuItemSpec extends Omit {
+ type: 'cardmenuitem';
+ label?: string;
+ items: CardItemSpec[];
+ onSetup?: (api: CardMenuItemInstanceApi) => (api: CardMenuItemInstanceApi) => void;
+ onAction?: (api: CardMenuItemInstanceApi) => void;
+}
+interface ChoiceMenuItemSpec extends CommonMenuItemSpec {
+ type?: 'choiceitem';
+ icon?: string;
+}
+interface ChoiceMenuItemInstanceApi extends CommonMenuItemInstanceApi {
+ isActive: () => boolean;
+ setActive: (state: boolean) => void;
+}
+interface ContextMenuItem extends CommonMenuItemSpec {
+ text: string;
+ icon?: string;
+ type?: 'item';
+ onAction: () => void;
+}
+interface ContextSubMenu extends CommonMenuItemSpec {
+ type: 'submenu';
+ text: string;
+ icon?: string;
+ getSubmenuItems: () => string | Array;
+}
+type ContextMenuContents = string | ContextMenuItem | SeparatorMenuItemSpec | ContextSubMenu;
+interface ContextMenuApi {
+ update: (element: Element) => string | Array;
+}
+interface FancyActionArgsMap {
+ 'inserttable': {
+ numRows: number;
+ numColumns: number;
+ };
+ 'colorswatch': {
+ value: string;
+ };
+}
+interface BaseFancyMenuItemSpec {
+ type: 'fancymenuitem';
+ fancytype: T;
+ initData?: Record;
+ onAction?: (data: FancyActionArgsMap[T]) => void;
+}
+interface InsertTableMenuItemSpec extends BaseFancyMenuItemSpec<'inserttable'> {
+ fancytype: 'inserttable';
+ initData?: {};
+}
+interface ColorSwatchMenuItemSpec extends BaseFancyMenuItemSpec<'colorswatch'> {
+ fancytype: 'colorswatch';
+ select?: (value: string) => boolean;
+ initData?: {
+ allowCustomColors?: boolean;
+ colors?: ChoiceMenuItemSpec[];
+ storageKey?: string;
+ };
+}
+type FancyMenuItemSpec = InsertTableMenuItemSpec | ColorSwatchMenuItemSpec;
+interface MenuItemSpec extends CommonMenuItemSpec {
+ type?: 'menuitem';
+ icon?: string;
+ onSetup?: (api: MenuItemInstanceApi) => (api: MenuItemInstanceApi) => void;
+ onAction?: (api: MenuItemInstanceApi) => void;
+}
+interface MenuItemInstanceApi extends CommonMenuItemInstanceApi {
+}
+interface SeparatorMenuItemSpec {
+ type?: 'separator';
+ text?: string;
+}
+interface ToggleMenuItemSpec extends CommonMenuItemSpec {
+ type?: 'togglemenuitem';
+ icon?: string;
+ active?: boolean;
+ onSetup?: (api: ToggleMenuItemInstanceApi) => void;
+ onAction: (api: ToggleMenuItemInstanceApi) => void;
+}
+interface ToggleMenuItemInstanceApi extends CommonMenuItemInstanceApi {
+ isActive: () => boolean;
+ setActive: (state: boolean) => void;
+}
+type NestedMenuItemContents = string | MenuItemSpec | NestedMenuItemSpec | ToggleMenuItemSpec | SeparatorMenuItemSpec | FancyMenuItemSpec;
+interface NestedMenuItemSpec extends CommonMenuItemSpec {
+ type?: 'nestedmenuitem';
+ icon?: string;
+ getSubmenuItems: () => string | Array;
+ onSetup?: (api: NestedMenuItemInstanceApi) => (api: NestedMenuItemInstanceApi) => void;
+}
+interface NestedMenuItemInstanceApi extends CommonMenuItemInstanceApi {
+ setTooltip: (tooltip: string) => void;
+ setIconFill: (id: string, value: string) => void;
+}
+type MenuButtonItemTypes = NestedMenuItemContents;
+type SuccessCallback$1 = (menu: string | MenuButtonItemTypes[]) => void;
+interface MenuButtonFetchContext {
+ pattern: string;
+}
+interface BaseMenuButtonSpec {
+ text?: string;
+ tooltip?: string;
+ icon?: string;
+ search?: boolean | {
+ placeholder?: string;
+ };
+ fetch: (success: SuccessCallback$1, fetchContext: MenuButtonFetchContext, api: BaseMenuButtonInstanceApi) => void;
+ onSetup?: (api: BaseMenuButtonInstanceApi) => (api: BaseMenuButtonInstanceApi) => void;
+}
+interface BaseMenuButtonInstanceApi {
+ isEnabled: () => boolean;
+ setEnabled: (state: boolean) => void;
+ isActive: () => boolean;
+ setActive: (state: boolean) => void;
+ setText: (text: string) => void;
+ setIcon: (icon: string) => void;
+}
+interface ToolbarMenuButtonSpec extends BaseMenuButtonSpec {
+ type?: 'menubutton';
+ onSetup?: (api: ToolbarMenuButtonInstanceApi) => (api: ToolbarMenuButtonInstanceApi) => void;
+}
+interface ToolbarMenuButtonInstanceApi extends BaseMenuButtonInstanceApi {
+}
+type ToolbarSplitButtonItemTypes = ChoiceMenuItemSpec | SeparatorMenuItemSpec;
+type SuccessCallback = (menu: ToolbarSplitButtonItemTypes[]) => void;
+type SelectPredicate = (value: string) => boolean;
+type PresetTypes = 'color' | 'normal' | 'listpreview';
+type ColumnTypes$1 = number | 'auto';
+interface ToolbarSplitButtonSpec {
+ type?: 'splitbutton';
+ tooltip?: string;
+ icon?: string;
+ text?: string;
+ select?: SelectPredicate;
+ presets?: PresetTypes;
+ columns?: ColumnTypes$1;
+ fetch: (success: SuccessCallback) => void;
+ onSetup?: (api: ToolbarSplitButtonInstanceApi) => (api: ToolbarSplitButtonInstanceApi) => void;
+ onAction: (api: ToolbarSplitButtonInstanceApi) => void;
+ onItemAction: (api: ToolbarSplitButtonInstanceApi, value: string) => void;
+}
+interface ToolbarSplitButtonInstanceApi {
+ isEnabled: () => boolean;
+ setEnabled: (state: boolean) => void;
+ setIconFill: (id: string, value: string) => void;
+ isActive: () => boolean;
+ setActive: (state: boolean) => void;
+ setTooltip: (tooltip: string) => void;
+ setText: (text: string) => void;
+ setIcon: (icon: string) => void;
+}
+interface BaseToolbarToggleButtonSpec extends BaseToolbarButtonSpec {
+ active?: boolean;
+}
+interface BaseToolbarToggleButtonInstanceApi extends BaseToolbarButtonInstanceApi {
+ isActive: () => boolean;
+ setActive: (state: boolean) => void;
+}
+interface ToolbarToggleButtonSpec extends BaseToolbarToggleButtonSpec {
+ type?: 'togglebutton';
+ onAction: (api: ToolbarToggleButtonInstanceApi) => void;
+ shortcut?: string;
+}
+interface ToolbarToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {
+}
+type Id = string;
+interface TreeSpec {
+ type: 'tree';
+ items: TreeItemSpec[];
+ onLeafAction?: (id: Id) => void;
+ defaultExpandedIds?: Id[];
+ onToggleExpand?: (expandedIds: Id[], { expanded, node }: {
+ expanded: boolean;
+ node: Id;
+ }) => void;
+ defaultSelectedId?: Id;
+}
+interface BaseTreeItemSpec {
+ title: string;
+ id: Id;
+ menu?: ToolbarMenuButtonSpec;
+}
+interface DirectorySpec extends BaseTreeItemSpec {
+ type: 'directory';
+ children: TreeItemSpec[];
+}
+interface LeafSpec extends BaseTreeItemSpec {
+ type: 'leaf';
+}
+type TreeItemSpec = DirectorySpec | LeafSpec;
+interface UrlInputSpec extends FormComponentWithLabelSpec {
+ type: 'urlinput';
+ filetype?: 'image' | 'media' | 'file';
+ enabled?: boolean;
+ picker_text?: string;
+}
+interface UrlInputData {
+ value: string;
+ meta: {
+ text?: string;
+ };
+}
+type BodyComponentSpec = BarSpec | ButtonSpec | CheckboxSpec | TextAreaSpec | InputSpec | ListBoxSpec | SelectBoxSpec | SizeInputSpec | SliderSpec | IframeSpec | HtmlPanelSpec | UrlInputSpec | DropZoneSpec | ColorInputSpec | GridSpec | ColorPickerSpec | ImagePreviewSpec | AlertBannerSpec | CollectionSpec | LabelSpec | TableSpec | TreeSpec | PanelSpec | CustomEditorSpec;
+interface BarSpec {
+ type: 'bar';
+ items: BodyComponentSpec[];
+}
+interface DialogToggleMenuItemSpec extends CommonMenuItemSpec {
+ type?: 'togglemenuitem';
+ name: string;
+}
+type DialogFooterMenuButtonItemSpec = DialogToggleMenuItemSpec;
+interface BaseDialogFooterButtonSpec {
+ name?: string;
+ align?: 'start' | 'end';
+ primary?: boolean;
+ enabled?: boolean;
+ icon?: string;
+ buttonType?: 'primary' | 'secondary';
+}
+interface DialogFooterNormalButtonSpec extends BaseDialogFooterButtonSpec {
+ type: 'submit' | 'cancel' | 'custom';
+ text: string;
+}
+interface DialogFooterMenuButtonSpec extends BaseDialogFooterButtonSpec {
+ type: 'menu';
+ text?: string;
+ tooltip?: string;
+ icon?: string;
+ items: DialogFooterMenuButtonItemSpec[];
+}
+interface DialogFooterToggleButtonSpec extends BaseDialogFooterButtonSpec {
+ type: 'togglebutton';
+ tooltip?: string;
+ icon?: string;
+ text?: string;
+ active?: boolean;
+}
+type DialogFooterButtonSpec = DialogFooterNormalButtonSpec | DialogFooterMenuButtonSpec | DialogFooterToggleButtonSpec;
+interface TabSpec {
+ name?: string;
+ title: string;
+ items: BodyComponentSpec[];
+}
+interface TabPanelSpec {
+ type: 'tabpanel';
+ tabs: TabSpec[];
+}
+type DialogDataItem = any;
+type DialogData = Record;
+interface DialogInstanceApi {
+ getData: () => T;
+ setData: (data: Partial) => void;
+ setEnabled: (name: string, state: boolean) => void;
+ focus: (name: string) => void;
+ showTab: (name: string) => void;
+ redial: (nu: DialogSpec) => void;
+ block: (msg: string) => void;
+ unblock: () => void;
+ toggleFullscreen: () => void;
+ close: () => void;
+}
+interface DialogActionDetails {
+ name: string;
+ value?: any;
+}
+interface DialogChangeDetails {
+ name: keyof T;
+}
+interface DialogTabChangeDetails {
+ newTabName: string;
+ oldTabName: string;
+}
+type DialogActionHandler = (api: DialogInstanceApi, details: DialogActionDetails) => void;
+type DialogChangeHandler = (api: DialogInstanceApi, details: DialogChangeDetails) => void;
+type DialogSubmitHandler = (api: DialogInstanceApi) => void;
+type DialogCloseHandler = () => void;
+type DialogCancelHandler = (api: DialogInstanceApi) => void;
+type DialogTabChangeHandler = (api: DialogInstanceApi, details: DialogTabChangeDetails) => void;
+type DialogSize = 'normal' | 'medium' | 'large';
+interface DialogSpec {
+ title: string;
+ size?: DialogSize;
+ body: TabPanelSpec | PanelSpec;
+ buttons?: DialogFooterButtonSpec[];
+ initialData?: Partial;
+ onAction?: DialogActionHandler;
+ onChange?: DialogChangeHandler;
+ onSubmit?: DialogSubmitHandler;
+ onClose?: DialogCloseHandler;
+ onCancel?: DialogCancelHandler;
+ onTabChange?: DialogTabChangeHandler;
+}
+interface UrlDialogInstanceApi {
+ block: (msg: string) => void;
+ unblock: () => void;
+ close: () => void;
+ sendMessage: (msg: any) => void;
+}
+interface UrlDialogActionDetails {
+ name: string;
+ value?: any;
+}
+interface UrlDialogMessage {
+ mceAction: string;
+ [key: string]: any;
+}
+type UrlDialogActionHandler = (api: UrlDialogInstanceApi, actions: UrlDialogActionDetails) => void;
+type UrlDialogCloseHandler = () => void;
+type UrlDialogCancelHandler = (api: UrlDialogInstanceApi) => void;
+type UrlDialogMessageHandler = (api: UrlDialogInstanceApi, message: UrlDialogMessage) => void;
+interface UrlDialogFooterButtonSpec extends DialogFooterNormalButtonSpec {
+ type: 'cancel' | 'custom';
+}
+interface UrlDialogSpec {
+ title: string;
+ url: string;
+ height?: number;
+ width?: number;
+ buttons?: UrlDialogFooterButtonSpec[];
+ onAction?: UrlDialogActionHandler;
+ onClose?: UrlDialogCloseHandler;
+ onCancel?: UrlDialogCancelHandler;
+ onMessage?: UrlDialogMessageHandler;
+}
+type ColumnTypes = number | 'auto';
+type SeparatorItemSpec = SeparatorMenuItemSpec;
+interface AutocompleterItemSpec {
+ type?: 'autocompleteitem';
+ value: string;
+ text?: string;
+ icon?: string;
+ meta?: Record;
+}
+type AutocompleterContents = SeparatorItemSpec | AutocompleterItemSpec | CardMenuItemSpec;
+interface AutocompleterSpec {
+ type?: 'autocompleter';
+ trigger: string;
+ minChars?: number;
+ columns?: ColumnTypes;
+ matches?: (rng: Range, text: string, pattern: string) => boolean;
+ fetch: (pattern: string, maxResults: number, fetchOptions: Record) => Promise;
+ onAction: (autocompleterApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record) => void;
+ maxResults?: number;
+ highlightOn?: string[];
+}
+interface AutocompleterInstanceApi {
+ hide: () => void;
+ reload: (fetchOptions: Record) => void;
+}
+type ContextPosition = 'node' | 'selection' | 'line';
+type ContextScope = 'node' | 'editor';
+interface ContextBarSpec {
+ predicate?: (elem: Element) => boolean;
+ position?: ContextPosition;
+ scope?: ContextScope;
+}
+interface ContextFormLaunchButtonApi extends BaseToolbarButtonSpec {
+ type: 'contextformbutton';
+}
+interface ContextFormLaunchToggleButtonSpec extends BaseToolbarToggleButtonSpec {
+ type: 'contextformtogglebutton';
+}
+interface ContextFormButtonInstanceApi extends BaseToolbarButtonInstanceApi {
+}
+interface ContextFormToggleButtonInstanceApi extends BaseToolbarToggleButtonInstanceApi {
+}
+interface ContextFormButtonSpec extends BaseToolbarButtonSpec {
+ type?: 'contextformbutton';
+ primary?: boolean;
+ onAction: (formApi: ContextFormInstanceApi, api: ContextFormButtonInstanceApi) => void;
+}
+interface ContextFormToggleButtonSpec extends BaseToolbarToggleButtonSpec {
+ type?: 'contextformtogglebutton';
+ onAction: (formApi: ContextFormInstanceApi, buttonApi: ContextFormToggleButtonInstanceApi) => void;
+ primary?: boolean;
+}
+interface ContextFormInstanceApi {
+ hide: () => void;
+ getValue: () => string;
+}
+interface ContextFormSpec extends ContextBarSpec {
+ type?: 'contextform';
+ initValue?: () => string;
+ label?: string;
+ launch?: ContextFormLaunchButtonApi | ContextFormLaunchToggleButtonSpec;
+ commands: Array;
+}
+interface ContextToolbarSpec extends ContextBarSpec {
+ type?: 'contexttoolbar';
+ items: string;
+}
+type PublicDialog_d_AlertBannerSpec = AlertBannerSpec;
+type PublicDialog_d_BarSpec = BarSpec;
+type PublicDialog_d_BodyComponentSpec = BodyComponentSpec;
+type PublicDialog_d_ButtonSpec = ButtonSpec;
+type PublicDialog_d_CheckboxSpec = CheckboxSpec;
+type PublicDialog_d_CollectionItem = CollectionItem;
+type PublicDialog_d_CollectionSpec = CollectionSpec;
+type PublicDialog_d_ColorInputSpec = ColorInputSpec;
+type PublicDialog_d_ColorPickerSpec = ColorPickerSpec;
+type PublicDialog_d_CustomEditorSpec = CustomEditorSpec;
+type PublicDialog_d_CustomEditorInit = CustomEditorInit;
+type PublicDialog_d_CustomEditorInitFn = CustomEditorInitFn;
+type PublicDialog_d_DialogData = DialogData;
+type PublicDialog_d_DialogSize = DialogSize;
+type PublicDialog_d_DialogSpec = DialogSpec;
+type PublicDialog_d_DialogInstanceApi = DialogInstanceApi;
+type PublicDialog_d_DialogFooterButtonSpec = DialogFooterButtonSpec;
+type PublicDialog_d_DialogActionDetails = DialogActionDetails;
+type PublicDialog_d_DialogChangeDetails = DialogChangeDetails;
+type PublicDialog_d_DialogTabChangeDetails = DialogTabChangeDetails;
+type PublicDialog_d_DropZoneSpec = DropZoneSpec;
+type PublicDialog_d_GridSpec = GridSpec;
+type PublicDialog_d_HtmlPanelSpec = HtmlPanelSpec;
+type PublicDialog_d_IframeSpec = IframeSpec;
+type PublicDialog_d_ImagePreviewSpec = ImagePreviewSpec;
+type PublicDialog_d_InputSpec = InputSpec;
+type PublicDialog_d_LabelSpec = LabelSpec;
+type PublicDialog_d_ListBoxSpec = ListBoxSpec;
+type PublicDialog_d_ListBoxItemSpec = ListBoxItemSpec;
+type PublicDialog_d_ListBoxNestedItemSpec = ListBoxNestedItemSpec;
+type PublicDialog_d_ListBoxSingleItemSpec = ListBoxSingleItemSpec;
+type PublicDialog_d_PanelSpec = PanelSpec;
+type PublicDialog_d_SelectBoxSpec = SelectBoxSpec;
+type PublicDialog_d_SelectBoxItemSpec = SelectBoxItemSpec;
+type PublicDialog_d_SizeInputSpec = SizeInputSpec;
+type PublicDialog_d_SliderSpec = SliderSpec;
+type PublicDialog_d_TableSpec = TableSpec;
+type PublicDialog_d_TabSpec = TabSpec;
+type PublicDialog_d_TabPanelSpec = TabPanelSpec;
+type PublicDialog_d_TextAreaSpec = TextAreaSpec;
+type PublicDialog_d_TreeSpec = TreeSpec;
+type PublicDialog_d_TreeItemSpec = TreeItemSpec;
+type PublicDialog_d_UrlInputData = UrlInputData;
+type PublicDialog_d_UrlInputSpec = UrlInputSpec;
+type PublicDialog_d_UrlDialogSpec = UrlDialogSpec;
+type PublicDialog_d_UrlDialogFooterButtonSpec = UrlDialogFooterButtonSpec;
+type PublicDialog_d_UrlDialogInstanceApi = UrlDialogInstanceApi;
+type PublicDialog_d_UrlDialogActionDetails = UrlDialogActionDetails;
+type PublicDialog_d_UrlDialogMessage = UrlDialogMessage;
+declare namespace PublicDialog_d {
+ export { PublicDialog_d_AlertBannerSpec as AlertBannerSpec, PublicDialog_d_BarSpec as BarSpec, PublicDialog_d_BodyComponentSpec as BodyComponentSpec, PublicDialog_d_ButtonSpec as ButtonSpec, PublicDialog_d_CheckboxSpec as CheckboxSpec, PublicDialog_d_CollectionItem as CollectionItem, PublicDialog_d_CollectionSpec as CollectionSpec, PublicDialog_d_ColorInputSpec as ColorInputSpec, PublicDialog_d_ColorPickerSpec as ColorPickerSpec, PublicDialog_d_CustomEditorSpec as CustomEditorSpec, PublicDialog_d_CustomEditorInit as CustomEditorInit, PublicDialog_d_CustomEditorInitFn as CustomEditorInitFn, PublicDialog_d_DialogData as DialogData, PublicDialog_d_DialogSize as DialogSize, PublicDialog_d_DialogSpec as DialogSpec, PublicDialog_d_DialogInstanceApi as DialogInstanceApi, PublicDialog_d_DialogFooterButtonSpec as DialogFooterButtonSpec, PublicDialog_d_DialogActionDetails as DialogActionDetails, PublicDialog_d_DialogChangeDetails as DialogChangeDetails, PublicDialog_d_DialogTabChangeDetails as DialogTabChangeDetails, PublicDialog_d_DropZoneSpec as DropZoneSpec, PublicDialog_d_GridSpec as GridSpec, PublicDialog_d_HtmlPanelSpec as HtmlPanelSpec, PublicDialog_d_IframeSpec as IframeSpec, PublicDialog_d_ImagePreviewSpec as ImagePreviewSpec, PublicDialog_d_InputSpec as InputSpec, PublicDialog_d_LabelSpec as LabelSpec, PublicDialog_d_ListBoxSpec as ListBoxSpec, PublicDialog_d_ListBoxItemSpec as ListBoxItemSpec, PublicDialog_d_ListBoxNestedItemSpec as ListBoxNestedItemSpec, PublicDialog_d_ListBoxSingleItemSpec as ListBoxSingleItemSpec, PublicDialog_d_PanelSpec as PanelSpec, PublicDialog_d_SelectBoxSpec as SelectBoxSpec, PublicDialog_d_SelectBoxItemSpec as SelectBoxItemSpec, PublicDialog_d_SizeInputSpec as SizeInputSpec, PublicDialog_d_SliderSpec as SliderSpec, PublicDialog_d_TableSpec as TableSpec, PublicDialog_d_TabSpec as TabSpec, PublicDialog_d_TabPanelSpec as TabPanelSpec, PublicDialog_d_TextAreaSpec as TextAreaSpec, PublicDialog_d_TreeSpec as TreeSpec, PublicDialog_d_TreeItemSpec as TreeItemSpec, DirectorySpec as TreeDirectorySpec, LeafSpec as TreeLeafSpec, PublicDialog_d_UrlInputData as UrlInputData, PublicDialog_d_UrlInputSpec as UrlInputSpec, PublicDialog_d_UrlDialogSpec as UrlDialogSpec, PublicDialog_d_UrlDialogFooterButtonSpec as UrlDialogFooterButtonSpec, PublicDialog_d_UrlDialogInstanceApi as UrlDialogInstanceApi, PublicDialog_d_UrlDialogActionDetails as UrlDialogActionDetails, PublicDialog_d_UrlDialogMessage as UrlDialogMessage, };
+}
+type PublicInlineContent_d_AutocompleterSpec = AutocompleterSpec;
+type PublicInlineContent_d_AutocompleterItemSpec = AutocompleterItemSpec;
+type PublicInlineContent_d_AutocompleterContents = AutocompleterContents;
+type PublicInlineContent_d_AutocompleterInstanceApi = AutocompleterInstanceApi;
+type PublicInlineContent_d_ContextPosition = ContextPosition;
+type PublicInlineContent_d_ContextScope = ContextScope;
+type PublicInlineContent_d_ContextFormSpec = ContextFormSpec;
+type PublicInlineContent_d_ContextFormInstanceApi = ContextFormInstanceApi;
+type PublicInlineContent_d_ContextFormButtonSpec = ContextFormButtonSpec;
+type PublicInlineContent_d_ContextFormButtonInstanceApi = ContextFormButtonInstanceApi;
+type PublicInlineContent_d_ContextFormToggleButtonSpec = ContextFormToggleButtonSpec;
+type PublicInlineContent_d_ContextFormToggleButtonInstanceApi = ContextFormToggleButtonInstanceApi;
+type PublicInlineContent_d_ContextToolbarSpec = ContextToolbarSpec;
+type PublicInlineContent_d_SeparatorItemSpec = SeparatorItemSpec;
+declare namespace PublicInlineContent_d {
+ export { PublicInlineContent_d_AutocompleterSpec as AutocompleterSpec, PublicInlineContent_d_AutocompleterItemSpec as AutocompleterItemSpec, PublicInlineContent_d_AutocompleterContents as AutocompleterContents, PublicInlineContent_d_AutocompleterInstanceApi as AutocompleterInstanceApi, PublicInlineContent_d_ContextPosition as ContextPosition, PublicInlineContent_d_ContextScope as ContextScope, PublicInlineContent_d_ContextFormSpec as ContextFormSpec, PublicInlineContent_d_ContextFormInstanceApi as ContextFormInstanceApi, PublicInlineContent_d_ContextFormButtonSpec as ContextFormButtonSpec, PublicInlineContent_d_ContextFormButtonInstanceApi as ContextFormButtonInstanceApi, PublicInlineContent_d_ContextFormToggleButtonSpec as ContextFormToggleButtonSpec, PublicInlineContent_d_ContextFormToggleButtonInstanceApi as ContextFormToggleButtonInstanceApi, PublicInlineContent_d_ContextToolbarSpec as ContextToolbarSpec, PublicInlineContent_d_SeparatorItemSpec as SeparatorItemSpec, };
+}
+type PublicMenu_d_MenuItemSpec = MenuItemSpec;
+type PublicMenu_d_MenuItemInstanceApi = MenuItemInstanceApi;
+type PublicMenu_d_NestedMenuItemContents = NestedMenuItemContents;
+type PublicMenu_d_NestedMenuItemSpec = NestedMenuItemSpec;
+type PublicMenu_d_NestedMenuItemInstanceApi = NestedMenuItemInstanceApi;
+type PublicMenu_d_FancyMenuItemSpec = FancyMenuItemSpec;
+type PublicMenu_d_ColorSwatchMenuItemSpec = ColorSwatchMenuItemSpec;
+type PublicMenu_d_InsertTableMenuItemSpec = InsertTableMenuItemSpec;
+type PublicMenu_d_ToggleMenuItemSpec = ToggleMenuItemSpec;
+type PublicMenu_d_ToggleMenuItemInstanceApi = ToggleMenuItemInstanceApi;
+type PublicMenu_d_ChoiceMenuItemSpec = ChoiceMenuItemSpec;
+type PublicMenu_d_ChoiceMenuItemInstanceApi = ChoiceMenuItemInstanceApi;
+type PublicMenu_d_SeparatorMenuItemSpec = SeparatorMenuItemSpec;
+type PublicMenu_d_ContextMenuApi = ContextMenuApi;
+type PublicMenu_d_ContextMenuContents = ContextMenuContents;
+type PublicMenu_d_ContextMenuItem = ContextMenuItem;
+type PublicMenu_d_ContextSubMenu = ContextSubMenu;
+type PublicMenu_d_CardMenuItemSpec = CardMenuItemSpec;
+type PublicMenu_d_CardMenuItemInstanceApi = CardMenuItemInstanceApi;
+type PublicMenu_d_CardItemSpec = CardItemSpec;
+type PublicMenu_d_CardContainerSpec = CardContainerSpec;
+type PublicMenu_d_CardImageSpec = CardImageSpec;
+type PublicMenu_d_CardTextSpec = CardTextSpec;
+declare namespace PublicMenu_d {
+ export { PublicMenu_d_MenuItemSpec as MenuItemSpec, PublicMenu_d_MenuItemInstanceApi as MenuItemInstanceApi, PublicMenu_d_NestedMenuItemContents as NestedMenuItemContents, PublicMenu_d_NestedMenuItemSpec as NestedMenuItemSpec, PublicMenu_d_NestedMenuItemInstanceApi as NestedMenuItemInstanceApi, PublicMenu_d_FancyMenuItemSpec as FancyMenuItemSpec, PublicMenu_d_ColorSwatchMenuItemSpec as ColorSwatchMenuItemSpec, PublicMenu_d_InsertTableMenuItemSpec as InsertTableMenuItemSpec, PublicMenu_d_ToggleMenuItemSpec as ToggleMenuItemSpec, PublicMenu_d_ToggleMenuItemInstanceApi as ToggleMenuItemInstanceApi, PublicMenu_d_ChoiceMenuItemSpec as ChoiceMenuItemSpec, PublicMenu_d_ChoiceMenuItemInstanceApi as ChoiceMenuItemInstanceApi, PublicMenu_d_SeparatorMenuItemSpec as SeparatorMenuItemSpec, PublicMenu_d_ContextMenuApi as ContextMenuApi, PublicMenu_d_ContextMenuContents as ContextMenuContents, PublicMenu_d_ContextMenuItem as ContextMenuItem, PublicMenu_d_ContextSubMenu as ContextSubMenu, PublicMenu_d_CardMenuItemSpec as CardMenuItemSpec, PublicMenu_d_CardMenuItemInstanceApi as CardMenuItemInstanceApi, PublicMenu_d_CardItemSpec as CardItemSpec, PublicMenu_d_CardContainerSpec as CardContainerSpec, PublicMenu_d_CardImageSpec as CardImageSpec, PublicMenu_d_CardTextSpec as CardTextSpec, };
+}
+interface SidebarInstanceApi {
+ element: () => HTMLElement;
+}
+interface SidebarSpec {
+ icon?: string;
+ tooltip?: string;
+ onShow?: (api: SidebarInstanceApi) => void;
+ onSetup?: (api: SidebarInstanceApi) => (api: SidebarInstanceApi) => void;
+ onHide?: (api: SidebarInstanceApi) => void;
+}
+type PublicSidebar_d_SidebarSpec = SidebarSpec;
+type PublicSidebar_d_SidebarInstanceApi = SidebarInstanceApi;
+declare namespace PublicSidebar_d {
+ export { PublicSidebar_d_SidebarSpec as SidebarSpec, PublicSidebar_d_SidebarInstanceApi as SidebarInstanceApi, };
+}
+type PublicToolbar_d_ToolbarButtonSpec = ToolbarButtonSpec;
+type PublicToolbar_d_ToolbarButtonInstanceApi = ToolbarButtonInstanceApi;
+type PublicToolbar_d_ToolbarSplitButtonSpec = ToolbarSplitButtonSpec;
+type PublicToolbar_d_ToolbarSplitButtonInstanceApi = ToolbarSplitButtonInstanceApi;
+type PublicToolbar_d_ToolbarMenuButtonSpec = ToolbarMenuButtonSpec;
+type PublicToolbar_d_ToolbarMenuButtonInstanceApi = ToolbarMenuButtonInstanceApi;
+type PublicToolbar_d_ToolbarToggleButtonSpec = ToolbarToggleButtonSpec;
+type PublicToolbar_d_ToolbarToggleButtonInstanceApi = ToolbarToggleButtonInstanceApi;
+type PublicToolbar_d_GroupToolbarButtonSpec = GroupToolbarButtonSpec;
+type PublicToolbar_d_GroupToolbarButtonInstanceApi = GroupToolbarButtonInstanceApi;
+declare namespace PublicToolbar_d {
+ export { PublicToolbar_d_ToolbarButtonSpec as ToolbarButtonSpec, PublicToolbar_d_ToolbarButtonInstanceApi as ToolbarButtonInstanceApi, PublicToolbar_d_ToolbarSplitButtonSpec as ToolbarSplitButtonSpec, PublicToolbar_d_ToolbarSplitButtonInstanceApi as ToolbarSplitButtonInstanceApi, PublicToolbar_d_ToolbarMenuButtonSpec as ToolbarMenuButtonSpec, PublicToolbar_d_ToolbarMenuButtonInstanceApi as ToolbarMenuButtonInstanceApi, PublicToolbar_d_ToolbarToggleButtonSpec as ToolbarToggleButtonSpec, PublicToolbar_d_ToolbarToggleButtonInstanceApi as ToolbarToggleButtonInstanceApi, PublicToolbar_d_GroupToolbarButtonSpec as GroupToolbarButtonSpec, PublicToolbar_d_GroupToolbarButtonInstanceApi as GroupToolbarButtonInstanceApi, };
+}
+interface ViewButtonApi {
+ setIcon: (newIcon: string) => void;
+}
+interface ViewToggleButtonApi extends ViewButtonApi {
+ isActive: () => boolean;
+ setActive: (state: boolean) => void;
+}
+interface BaseButtonSpec {
+ text?: string;
+ icon?: string;
+ tooltip?: string;
+ buttonType?: 'primary' | 'secondary';
+ borderless?: boolean;
+ onAction: (api: Api) => void;
+}
+interface ViewNormalButtonSpec extends BaseButtonSpec {
+ text: string;
+ type: 'button';
+}
+interface ViewToggleButtonSpec extends BaseButtonSpec {
+ type: 'togglebutton';
+ active?: boolean;
+ onAction: (api: ViewToggleButtonApi) => void;
+}
+interface ViewButtonsGroupSpec {
+ type: 'group';
+ buttons: Array;
+}
+type ViewButtonSpec = ViewNormalButtonSpec | ViewToggleButtonSpec | ViewButtonsGroupSpec;
+interface ViewInstanceApi {
+ getContainer: () => HTMLElement;
+}
+interface ViewSpec {
+ buttons?: ViewButtonSpec[];
+ onShow: (api: ViewInstanceApi) => void;
+ onHide: (api: ViewInstanceApi) => void;
+}
+type PublicView_d_ViewSpec = ViewSpec;
+type PublicView_d_ViewInstanceApi = ViewInstanceApi;
+declare namespace PublicView_d {
+ export { PublicView_d_ViewSpec as ViewSpec, PublicView_d_ViewInstanceApi as ViewInstanceApi, };
+}
+interface Registry$1 {
+ addButton: (name: string, spec: ToolbarButtonSpec) => void;
+ addGroupToolbarButton: (name: string, spec: GroupToolbarButtonSpec) => void;
+ addToggleButton: (name: string, spec: ToolbarToggleButtonSpec) => void;
+ addMenuButton: (name: string, spec: ToolbarMenuButtonSpec) => void;
+ addSplitButton: (name: string, spec: ToolbarSplitButtonSpec) => void;
+ addMenuItem: (name: string, spec: MenuItemSpec) => void;
+ addNestedMenuItem: (name: string, spec: NestedMenuItemSpec) => void;
+ addToggleMenuItem: (name: string, spec: ToggleMenuItemSpec) => void;
+ addContextMenu: (name: string, spec: ContextMenuApi) => void;
+ addContextToolbar: (name: string, spec: ContextToolbarSpec) => void;
+ addContextForm: (name: string, spec: ContextFormSpec) => void;
+ addIcon: (name: string, svgData: string) => void;
+ addAutocompleter: (name: string, spec: AutocompleterSpec) => void;
+ addSidebar: (name: string, spec: SidebarSpec) => void;
+ addView: (name: string, spec: ViewSpec) => void;
+ getAll: () => {
+ buttons: Record;
+ menuItems: Record;
+ popups: Record;
+ contextMenus: Record;
+ contextToolbars: Record;
+ icons: Record;
+ sidebars: Record;
+ views: Record;
+ };
+}
+interface AutocompleteLookupData {
+ readonly matchText: string;
+ readonly items: AutocompleterContents[];
+ readonly columns: ColumnTypes;
+ readonly onAction: (autoApi: AutocompleterInstanceApi, rng: Range, value: string, meta: Record) => void;
+ readonly highlightOn: string[];
+}
+interface AutocompleterEventArgs {
+ readonly lookupData: AutocompleteLookupData[];
+}
+interface RangeLikeObject {
+ startContainer: Node;
+ startOffset: number;
+ endContainer: Node;
+ endOffset: number;
+}
+type ApplyFormat = BlockFormat | InlineFormat | SelectorFormat;
+type RemoveFormat = RemoveBlockFormat | RemoveInlineFormat | RemoveSelectorFormat;
+type Format = ApplyFormat | RemoveFormat;
+type Formats = Record;
+type FormatAttrOrStyleValue = string | ((vars?: FormatVars) => string | null);
+type FormatVars = Record;
+interface BaseFormat {
+ ceFalseOverride?: boolean;
+ classes?: string | string[];
+ collapsed?: boolean;
+ exact?: boolean;
+ expand?: boolean;
+ links?: boolean;
+ mixed?: boolean;
+ block_expand?: boolean;
+ onmatch?: (node: Element, fmt: T, itemName: string) => boolean;
+ remove?: 'none' | 'empty' | 'all';
+ remove_similar?: boolean;
+ split?: boolean;
+ deep?: boolean;
+ preserve_attributes?: string[];
+}
+interface Block {
+ block: string;
+ list_block?: string;
+ wrapper?: boolean;
+}
+interface Inline {
+ inline: string;
+}
+interface Selector {
+ selector: string;
+ inherit?: boolean;
+}
+interface CommonFormat extends BaseFormat {
+ attributes?: Record;
+ styles?: Record;
+ toggle?: boolean;
+ preview?: string | false;
+ onformat?: (elm: Element, fmt: T, vars?: FormatVars, node?: Node | RangeLikeObject | null) => void;
+ clear_child_styles?: boolean;
+ merge_siblings?: boolean;
+ merge_with_parents?: boolean;
+}
+interface BlockFormat extends Block, CommonFormat {
+}
+interface InlineFormat extends Inline, CommonFormat {
+}
+interface SelectorFormat extends Selector, CommonFormat {
+}
+interface CommonRemoveFormat extends BaseFormat {
+ attributes?: string[] | Record;
+ styles?: string[] | Record;
+}
+interface RemoveBlockFormat extends Block, CommonRemoveFormat {
+}
+interface RemoveInlineFormat extends Inline, CommonRemoveFormat {
+}
+interface RemoveSelectorFormat extends Selector, CommonRemoveFormat {
+}
+interface Filter {
+ name: string;
+ callbacks: C[];
+}
+interface ParserArgs {
+ getInner?: boolean | number;
+ forced_root_block?: boolean | string;
+ context?: string;
+ isRootContent?: boolean;
+ format?: string;
+ invalid?: boolean;
+ no_events?: boolean;
+ [key: string]: any;
+}
+type ParserFilterCallback = (nodes: AstNode[], name: string, args: ParserArgs) => void;
+interface ParserFilter extends Filter {
+}
+interface DomParserSettings {
+ allow_html_data_urls?: boolean;
+ allow_svg_data_urls?: boolean;
+ allow_conditional_comments?: boolean;
+ allow_html_in_named_anchor?: boolean;
+ allow_script_urls?: boolean;
+ allow_unsafe_link_target?: boolean;
+ blob_cache?: BlobCache;
+ convert_fonts_to_spans?: boolean;
+ convert_unsafe_embeds?: boolean;
+ document?: Document;
+ fix_list_elements?: boolean;
+ font_size_legacy_values?: string;
+ forced_root_block?: boolean | string;
+ forced_root_block_attrs?: Record;
+ inline_styles?: boolean;
+ pad_empty_with_br?: boolean;
+ preserve_cdata?: boolean;
+ root_name?: string;
+ sandbox_iframes?: boolean;
+ sandbox_iframes_exclusions?: string[];
+ sanitize?: boolean;
+ validate?: boolean;
+}
+interface DomParser {
+ schema: Schema;
+ addAttributeFilter: (name: string, callback: ParserFilterCallback) => void;
+ getAttributeFilters: () => ParserFilter[];
+ removeAttributeFilter: (name: string, callback?: ParserFilterCallback) => void;
+ addNodeFilter: (name: string, callback: ParserFilterCallback) => void;
+ getNodeFilters: () => ParserFilter[];
+ removeNodeFilter: (name: string, callback?: ParserFilterCallback) => void;
+ parse: (html: string, args?: ParserArgs) => AstNode;
+}
+interface StyleSheetLoaderSettings {
+ maxLoadTime?: number;
+ contentCssCors?: boolean;
+ referrerPolicy?: ReferrerPolicy;
+}
+interface StyleSheetLoader {
+ load: (url: string) => Promise;
+ loadRawCss: (key: string, css: string) => void;
+ loadAll: (urls: string[]) => Promise;
+ unload: (url: string) => void;
+ unloadRawCss: (key: string) => void;
+ unloadAll: (urls: string[]) => void;
+ _setReferrerPolicy: (referrerPolicy: ReferrerPolicy) => void;
+ _setContentCssCors: (contentCssCors: boolean) => void;
+}
+type Registry = Registry$1;
+interface EditorUiApi {
+ show: () => void;
+ hide: () => void;
+ setEnabled: (state: boolean) => void;
+ isEnabled: () => boolean;
+}
+interface EditorUi extends EditorUiApi {
+ registry: Registry;
+ styleSheetLoader: StyleSheetLoader;
+}
+type Ui_d_Registry = Registry;
+type Ui_d_EditorUiApi = EditorUiApi;
+type Ui_d_EditorUi = EditorUi;
+declare namespace Ui_d {
+ export { Ui_d_Registry as Registry, PublicDialog_d as Dialog, PublicInlineContent_d as InlineContent, PublicMenu_d as Menu, PublicView_d as View, PublicSidebar_d as Sidebar, PublicToolbar_d as Toolbar, Ui_d_EditorUiApi as EditorUiApi, Ui_d_EditorUi as EditorUi, };
+}
+interface WindowParams {
+ readonly inline?: 'cursor' | 'toolbar' | 'bottom';
+ readonly ariaAttrs?: boolean;
+ readonly persistent?: boolean;
+}
+type InstanceApi = UrlDialogInstanceApi | DialogInstanceApi;
+interface WindowManagerImpl {
+ open: (config: DialogSpec, params: WindowParams | undefined, closeWindow: (dialog: DialogInstanceApi) => void) => DialogInstanceApi;
+ openUrl: (config: UrlDialogSpec, closeWindow: (dialog: UrlDialogInstanceApi) => void) => UrlDialogInstanceApi;
+ alert: (message: string, callback: () => void) => void;
+ confirm: (message: string, callback: (state: boolean) => void) => void;
+ close: (dialog: InstanceApi) => void;
+}
+interface WindowManager {
+ open: (config: DialogSpec, params?: WindowParams) => DialogInstanceApi;
+ openUrl: (config: UrlDialogSpec) => UrlDialogInstanceApi;
+ alert: (message: string, callback?: () => void, scope?: any) => void;
+ confirm: (message: string, callback?: (state: boolean) => void, scope?: any) => void;
+ close: () => void;
+}
+interface ExecCommandEvent {
+ command: string;
+ ui: boolean;
+ value?: any;
+}
+interface BeforeGetContentEvent extends GetContentArgs {
+ selection?: boolean;
+}
+interface GetContentEvent extends BeforeGetContentEvent {
+ content: string;
+}
+interface BeforeSetContentEvent extends SetContentArgs {
+ content: string;
+ selection?: boolean;
+}
+interface SetContentEvent extends BeforeSetContentEvent {
+ content: string;
+}
+interface SaveContentEvent extends GetContentEvent {
+ save: boolean;
+}
+interface NewBlockEvent {
+ newBlock: Element;
+}
+interface NodeChangeEvent {
+ element: Element;
+ parents: Node[];
+ selectionChange?: boolean;
+ initial?: boolean;
+}
+interface FormatEvent {
+ format: string;
+ vars?: FormatVars;
+ node?: Node | RangeLikeObject | null;
+}
+interface ObjectResizeEvent {
+ target: HTMLElement;
+ width: number;
+ height: number;
+ origin: string;
+}
+interface ObjectSelectedEvent {
+ target: Node;
+ targetClone?: Node;
+}
+interface ScrollIntoViewEvent {
+ elm: HTMLElement;
+ alignToTop: boolean | undefined;
+}
+interface SetSelectionRangeEvent {
+ range: Range;
+ forward: boolean | undefined;
+}
+interface ShowCaretEvent {
+ target: Node;
+ direction: number;
+ before: boolean;
+}
+interface SwitchModeEvent {
+ mode: string;
+}
+interface ChangeEvent {
+ level: UndoLevel;
+ lastLevel: UndoLevel | undefined;
+}
+interface AddUndoEvent extends ChangeEvent {
+ originalEvent: Event | undefined;
+}
+interface UndoRedoEvent {
+ level: UndoLevel;
+}
+interface WindowEvent {
+ dialog: InstanceApi;
+}
+interface ProgressStateEvent {
+ state: boolean;
+ time?: number;
+}
+interface AfterProgressStateEvent {
+ state: boolean;
+}
+interface PlaceholderToggleEvent {
+ state: boolean;
+}
+interface LoadErrorEvent {
+ message: string;
+}
+interface PreProcessEvent extends ParserArgs {
+ node: Element;
+}
+interface PostProcessEvent extends ParserArgs {
+ content: string;
+}
+interface PastePlainTextToggleEvent {
+ state: boolean;
+}
+interface PastePreProcessEvent {
+ content: string;
+ readonly internal: boolean;
+}
+interface PastePostProcessEvent {
+ node: HTMLElement;
+ readonly internal: boolean;
+}
+interface EditableRootStateChangeEvent {
+ state: boolean;
+}
+interface NewTableRowEvent {
+ node: HTMLTableRowElement;
+}
+interface NewTableCellEvent {
+ node: HTMLTableCellElement;
+}
+interface TableEventData {
+ readonly structure: boolean;
+ readonly style: boolean;
+}
+interface TableModifiedEvent extends TableEventData {
+ readonly table: HTMLTableElement;
+}
+interface BeforeOpenNotificationEvent {
+ notification: NotificationSpec;
+}
+interface OpenNotificationEvent {
+ notification: NotificationApi;
+}
+interface EditorEventMap extends Omit {
+ 'activate': {
+ relatedTarget: Editor | null;
+ };
+ 'deactivate': {
+ relatedTarget: Editor;
+ };
+ 'focus': {
+ blurredEditor: Editor | null;
+ };
+ 'blur': {
+ focusedEditor: Editor | null;
+ };
+ 'resize': UIEvent;
+ 'scroll': UIEvent;
+ 'input': InputEvent;
+ 'beforeinput': InputEvent;
+ 'detach': {};
+ 'remove': {};
+ 'init': {};
+ 'ScrollIntoView': ScrollIntoViewEvent;
+ 'AfterScrollIntoView': ScrollIntoViewEvent;
+ 'ObjectResized': ObjectResizeEvent;
+ 'ObjectResizeStart': ObjectResizeEvent;
+ 'SwitchMode': SwitchModeEvent;
+ 'ScrollWindow': Event;
+ 'ResizeWindow': UIEvent;
+ 'SkinLoaded': {};
+ 'SkinLoadError': LoadErrorEvent;
+ 'PluginLoadError': LoadErrorEvent;
+ 'ModelLoadError': LoadErrorEvent;
+ 'IconsLoadError': LoadErrorEvent;
+ 'ThemeLoadError': LoadErrorEvent;
+ 'LanguageLoadError': LoadErrorEvent;
+ 'BeforeExecCommand': ExecCommandEvent;
+ 'ExecCommand': ExecCommandEvent;
+ 'NodeChange': NodeChangeEvent;
+ 'FormatApply': FormatEvent;
+ 'FormatRemove': FormatEvent;
+ 'ShowCaret': ShowCaretEvent;
+ 'SelectionChange': {};
+ 'ObjectSelected': ObjectSelectedEvent;
+ 'BeforeObjectSelected': ObjectSelectedEvent;
+ 'GetSelectionRange': {
+ range: Range;
+ };
+ 'SetSelectionRange': SetSelectionRangeEvent;
+ 'AfterSetSelectionRange': SetSelectionRangeEvent;
+ 'BeforeGetContent': BeforeGetContentEvent;
+ 'GetContent': GetContentEvent;
+ 'BeforeSetContent': BeforeSetContentEvent;
+ 'SetContent': SetContentEvent;
+ 'SaveContent': SaveContentEvent;
+ 'RawSaveContent': SaveContentEvent;
+ 'LoadContent': {
+ load: boolean;
+ element: HTMLElement;
+ };
+ 'PreviewFormats': {};
+ 'AfterPreviewFormats': {};
+ 'ScriptsLoaded': {};
+ 'PreInit': {};
+ 'PostRender': {};
+ 'NewBlock': NewBlockEvent;
+ 'ClearUndos': {};
+ 'TypingUndo': {};
+ 'Redo': UndoRedoEvent;
+ 'Undo': UndoRedoEvent;
+ 'BeforeAddUndo': AddUndoEvent;
+ 'AddUndo': AddUndoEvent;
+ 'change': ChangeEvent;
+ 'CloseWindow': WindowEvent;
+ 'OpenWindow': WindowEvent;
+ 'ProgressState': ProgressStateEvent;
+ 'AfterProgressState': AfterProgressStateEvent;
+ 'PlaceholderToggle': PlaceholderToggleEvent;
+ 'tap': TouchEvent;
+ 'longpress': TouchEvent;
+ 'longpresscancel': {};
+ 'PreProcess': PreProcessEvent;
+ 'PostProcess': PostProcessEvent;
+ 'AutocompleterStart': AutocompleterEventArgs;
+ 'AutocompleterUpdate': AutocompleterEventArgs;
+ 'AutocompleterEnd': {};
+ 'PastePlainTextToggle': PastePlainTextToggleEvent;
+ 'PastePreProcess': PastePreProcessEvent;
+ 'PastePostProcess': PastePostProcessEvent;
+ 'TableModified': TableModifiedEvent;
+ 'NewRow': NewTableRowEvent;
+ 'NewCell': NewTableCellEvent;
+ 'SetAttrib': SetAttribEvent;
+ 'hide': {};
+ 'show': {};
+ 'dirty': {};
+ 'BeforeOpenNotification': BeforeOpenNotificationEvent;
+ 'OpenNotification': OpenNotificationEvent;
+}
+interface EditorManagerEventMap {
+ 'AddEditor': {
+ editor: Editor;
+ };
+ 'RemoveEditor': {
+ editor: Editor;
+ };
+ 'BeforeUnload': {
+ returnValue: any;
+ };
+}
+type EventTypes_d_ExecCommandEvent = ExecCommandEvent;
+type EventTypes_d_BeforeGetContentEvent = BeforeGetContentEvent;
+type EventTypes_d_GetContentEvent = GetContentEvent;
+type EventTypes_d_BeforeSetContentEvent = BeforeSetContentEvent;
+type EventTypes_d_SetContentEvent = SetContentEvent;
+type EventTypes_d_SaveContentEvent = SaveContentEvent;
+type EventTypes_d_NewBlockEvent = NewBlockEvent;
+type EventTypes_d_NodeChangeEvent = NodeChangeEvent;
+type EventTypes_d_FormatEvent = FormatEvent;
+type EventTypes_d_ObjectResizeEvent = ObjectResizeEvent;
+type EventTypes_d_ObjectSelectedEvent = ObjectSelectedEvent;
+type EventTypes_d_ScrollIntoViewEvent = ScrollIntoViewEvent;
+type EventTypes_d_SetSelectionRangeEvent = SetSelectionRangeEvent;
+type EventTypes_d_ShowCaretEvent = ShowCaretEvent;
+type EventTypes_d_SwitchModeEvent = SwitchModeEvent;
+type EventTypes_d_ChangeEvent = ChangeEvent;
+type EventTypes_d_AddUndoEvent = AddUndoEvent;
+type EventTypes_d_UndoRedoEvent = UndoRedoEvent;
+type EventTypes_d_WindowEvent = WindowEvent;
+type EventTypes_d_ProgressStateEvent = ProgressStateEvent;
+type EventTypes_d_AfterProgressStateEvent = AfterProgressStateEvent;
+type EventTypes_d_PlaceholderToggleEvent = PlaceholderToggleEvent;
+type EventTypes_d_LoadErrorEvent = LoadErrorEvent;
+type EventTypes_d_PreProcessEvent = PreProcessEvent;
+type EventTypes_d_PostProcessEvent = PostProcessEvent;
+type EventTypes_d_PastePlainTextToggleEvent = PastePlainTextToggleEvent;
+type EventTypes_d_PastePreProcessEvent = PastePreProcessEvent;
+type EventTypes_d_PastePostProcessEvent = PastePostProcessEvent;
+type EventTypes_d_EditableRootStateChangeEvent = EditableRootStateChangeEvent;
+type EventTypes_d_NewTableRowEvent = NewTableRowEvent;
+type EventTypes_d_NewTableCellEvent = NewTableCellEvent;
+type EventTypes_d_TableEventData = TableEventData;
+type EventTypes_d_TableModifiedEvent = TableModifiedEvent;
+type EventTypes_d_BeforeOpenNotificationEvent = BeforeOpenNotificationEvent;
+type EventTypes_d_OpenNotificationEvent = OpenNotificationEvent;
+type EventTypes_d_EditorEventMap = EditorEventMap;
+type EventTypes_d_EditorManagerEventMap = EditorManagerEventMap;
+declare namespace EventTypes_d {
+ export { EventTypes_d_ExecCommandEvent as ExecCommandEvent, EventTypes_d_BeforeGetContentEvent as BeforeGetContentEvent, EventTypes_d_GetContentEvent as GetContentEvent, EventTypes_d_BeforeSetContentEvent as BeforeSetContentEvent, EventTypes_d_SetContentEvent as SetContentEvent, EventTypes_d_SaveContentEvent as SaveContentEvent, EventTypes_d_NewBlockEvent as NewBlockEvent, EventTypes_d_NodeChangeEvent as NodeChangeEvent, EventTypes_d_FormatEvent as FormatEvent, EventTypes_d_ObjectResizeEvent as ObjectResizeEvent, EventTypes_d_ObjectSelectedEvent as ObjectSelectedEvent, EventTypes_d_ScrollIntoViewEvent as ScrollIntoViewEvent, EventTypes_d_SetSelectionRangeEvent as SetSelectionRangeEvent, EventTypes_d_ShowCaretEvent as ShowCaretEvent, EventTypes_d_SwitchModeEvent as SwitchModeEvent, EventTypes_d_ChangeEvent as ChangeEvent, EventTypes_d_AddUndoEvent as AddUndoEvent, EventTypes_d_UndoRedoEvent as UndoRedoEvent, EventTypes_d_WindowEvent as WindowEvent, EventTypes_d_ProgressStateEvent as ProgressStateEvent, EventTypes_d_AfterProgressStateEvent as AfterProgressStateEvent, EventTypes_d_PlaceholderToggleEvent as PlaceholderToggleEvent, EventTypes_d_LoadErrorEvent as LoadErrorEvent, EventTypes_d_PreProcessEvent as PreProcessEvent, EventTypes_d_PostProcessEvent as PostProcessEvent, EventTypes_d_PastePlainTextToggleEvent as PastePlainTextToggleEvent, EventTypes_d_PastePreProcessEvent as PastePreProcessEvent, EventTypes_d_PastePostProcessEvent as PastePostProcessEvent, EventTypes_d_EditableRootStateChangeEvent as EditableRootStateChangeEvent, EventTypes_d_NewTableRowEvent as NewTableRowEvent, EventTypes_d_NewTableCellEvent as NewTableCellEvent, EventTypes_d_TableEventData as TableEventData, EventTypes_d_TableModifiedEvent as TableModifiedEvent, EventTypes_d_BeforeOpenNotificationEvent as BeforeOpenNotificationEvent, EventTypes_d_OpenNotificationEvent as OpenNotificationEvent, EventTypes_d_EditorEventMap as EditorEventMap, EventTypes_d_EditorManagerEventMap as EditorManagerEventMap, };
+}
+type Format_d_Formats = Formats;
+type Format_d_Format = Format;
+type Format_d_ApplyFormat = ApplyFormat;
+type Format_d_BlockFormat = BlockFormat;
+type Format_d_InlineFormat = InlineFormat;
+type Format_d_SelectorFormat = SelectorFormat;
+type Format_d_RemoveFormat = RemoveFormat;
+type Format_d_RemoveBlockFormat = RemoveBlockFormat;
+type Format_d_RemoveInlineFormat = RemoveInlineFormat;
+type Format_d_RemoveSelectorFormat = RemoveSelectorFormat;
+declare namespace Format_d {
+ export { Format_d_Formats as Formats, Format_d_Format as Format, Format_d_ApplyFormat as ApplyFormat, Format_d_BlockFormat as BlockFormat, Format_d_InlineFormat as InlineFormat, Format_d_SelectorFormat as SelectorFormat, Format_d_RemoveFormat as RemoveFormat, Format_d_RemoveBlockFormat as RemoveBlockFormat, Format_d_RemoveInlineFormat as RemoveInlineFormat, Format_d_RemoveSelectorFormat as RemoveSelectorFormat, };
+}
+type StyleFormat = BlockStyleFormat | InlineStyleFormat | SelectorStyleFormat;
+type AllowedFormat = Separator | FormatReference | StyleFormat | NestedFormatting;
+interface Separator {
+ title: string;
+}
+interface FormatReference {
+ title: string;
+ format: string;
+ icon?: string;
+}
+interface NestedFormatting {
+ title: string;
+ items: Array;
+}
+interface CommonStyleFormat {
+ name?: string;
+ title: string;
+ icon?: string;
+}
+interface BlockStyleFormat extends BlockFormat, CommonStyleFormat {
+}
+interface InlineStyleFormat extends InlineFormat, CommonStyleFormat {
+}
+interface SelectorStyleFormat extends SelectorFormat, CommonStyleFormat {
+}
+type EntityEncoding = 'named' | 'numeric' | 'raw' | 'named,numeric' | 'named+numeric' | 'numeric,named' | 'numeric+named';
+interface ContentLanguage {
+ readonly title: string;
+ readonly code: string;
+ readonly customCode?: string;
+}
+type ThemeInitFunc = (editor: Editor, elm: HTMLElement) => {
+ editorContainer: HTMLElement;
+ iframeContainer: HTMLElement;
+ height?: number;
+ iframeHeight?: number;
+ api?: EditorUiApi;
+};
+type SetupCallback = (editor: Editor) => void;
+type FilePickerCallback = (callback: (value: string, meta?: Record) => void, value: string, meta: Record) => void;
+type FilePickerValidationStatus = 'valid' | 'unknown' | 'invalid' | 'none';
+type FilePickerValidationCallback = (info: {
+ type: string;
+ url: string;
+}, callback: (validation: {
+ status: FilePickerValidationStatus;
+ message: string;
+}) => void) => void;
+type PastePreProcessFn = (editor: Editor, args: PastePreProcessEvent) => void;
+type PastePostProcessFn = (editor: Editor, args: PastePostProcessEvent) => void;
+type URLConverter = (url: string, name: string, elm?: string | Element) => string;
+type URLConverterCallback = (url: string, node: Node | string | undefined, on_save: boolean, name: string) => string;
+interface ToolbarGroup {
+ name?: string;
+ items: string[];
+}
+type ToolbarMode = 'floating' | 'sliding' | 'scrolling' | 'wrap';
+type ToolbarLocation = 'top' | 'bottom' | 'auto';
+interface BaseEditorOptions {
+ a11y_advanced_options?: boolean;
+ add_form_submit_trigger?: boolean;
+ add_unload_trigger?: boolean;
+ allow_conditional_comments?: boolean;
+ allow_html_data_urls?: boolean;
+ allow_html_in_named_anchor?: boolean;
+ allow_script_urls?: boolean;
+ allow_svg_data_urls?: boolean;
+ allow_unsafe_link_target?: boolean;
+ anchor_bottom?: false | string;
+ anchor_top?: false | string;
+ auto_focus?: string | true;
+ automatic_uploads?: boolean;
+ base_url?: string;
+ block_formats?: string;
+ block_unsupported_drop?: boolean;
+ body_id?: string;
+ body_class?: string;
+ br_in_pre?: boolean;
+ br_newline_selector?: string;
+ browser_spellcheck?: boolean;
+ branding?: boolean;
+ cache_suffix?: string;
+ color_cols?: number;
+ color_cols_foreground?: number;
+ color_cols_background?: number;
+ color_map?: string[];
+ color_map_foreground?: string[];
+ color_map_background?: string[];
+ color_default_foreground?: string;
+ color_default_background?: string;
+ content_css?: boolean | string | string[];
+ content_css_cors?: boolean;
+ content_security_policy?: string;
+ content_style?: string;
+ content_langs?: ContentLanguage[];
+ contextmenu?: string | string[] | false;
+ contextmenu_never_use_native?: boolean;
+ convert_fonts_to_spans?: boolean;
+ convert_unsafe_embeds?: boolean;
+ convert_urls?: boolean;
+ custom_colors?: boolean;
+ custom_elements?: string | Record;
+ custom_ui_selector?: string;
+ custom_undo_redo_levels?: number;
+ default_font_stack?: string[];
+ deprecation_warnings?: boolean;
+ directionality?: 'ltr' | 'rtl';
+ doctype?: string;
+ document_base_url?: string;
+ draggable_modal?: boolean;
+ editable_class?: string;
+ editable_root?: boolean;
+ element_format?: 'xhtml' | 'html';
+ elementpath?: boolean;
+ encoding?: string;
+ end_container_on_empty_block?: boolean | string;
+ entities?: string;
+ entity_encoding?: EntityEncoding;
+ extended_valid_elements?: string;
+ event_root?: string;
+ file_picker_callback?: FilePickerCallback;
+ file_picker_types?: string;
+ file_picker_validator_handler?: FilePickerValidationCallback;
+ fix_list_elements?: boolean;
+ fixed_toolbar_container?: string;
+ fixed_toolbar_container_target?: HTMLElement;
+ font_css?: string | string[];
+ font_family_formats?: string;
+ font_size_classes?: string;
+ font_size_legacy_values?: string;
+ font_size_style_values?: string;
+ font_size_formats?: string;
+ font_size_input_default_unit?: string;
+ forced_root_block?: string;
+ forced_root_block_attrs?: Record;
+ formats?: Formats;
+ format_noneditable_selector?: string;
+ height?: number | string;
+ help_accessibility?: boolean;
+ hidden_input?: boolean;
+ highlight_on_focus?: boolean;
+ icons?: string;
+ icons_url?: string;
+ id?: string;
+ iframe_aria_text?: string;
+ iframe_attrs?: Record;
+ images_file_types?: string;
+ images_replace_blob_uris?: boolean;
+ images_reuse_filename?: boolean;
+ images_upload_base_path?: string;
+ images_upload_credentials?: boolean;
+ images_upload_handler?: UploadHandler;
+ images_upload_url?: string;
+ indent?: boolean;
+ indent_after?: string;
+ indent_before?: string;
+ indent_use_margin?: boolean;
+ indentation?: string;
+ init_instance_callback?: SetupCallback;
+ inline?: boolean;
+ inline_boundaries?: boolean;
+ inline_boundaries_selector?: string;
+ inline_styles?: boolean;
+ invalid_elements?: string;
+ invalid_styles?: string | Record;
+ keep_styles?: boolean;
+ language?: string;
+ language_load?: boolean;
+ language_url?: string;
+ line_height_formats?: string;
+ max_height?: number;
+ max_width?: number;
+ menu?: Record;
+ menubar?: boolean | string;
+ min_height?: number;
+ min_width?: number;
+ model?: string;
+ model_url?: string;
+ newdocument_content?: string;
+ newline_behavior?: 'block' | 'linebreak' | 'invert' | 'default';
+ no_newline_selector?: string;
+ noneditable_class?: string;
+ noneditable_regexp?: RegExp | RegExp[];
+ nowrap?: boolean;
+ object_resizing?: boolean | string;
+ pad_empty_with_br?: boolean;
+ paste_as_text?: boolean;
+ paste_block_drop?: boolean;
+ paste_data_images?: boolean;
+ paste_merge_formats?: boolean;
+ paste_postprocess?: PastePostProcessFn;
+ paste_preprocess?: PastePreProcessFn;
+ paste_remove_styles_if_webkit?: boolean;
+ paste_tab_spaces?: number;
+ paste_webkit_styles?: string;
+ placeholder?: string;
+ preserve_cdata?: boolean;
+ preview_styles?: false | string;
+ promotion?: boolean;
+ protect?: RegExp[];
+ readonly?: boolean;
+ referrer_policy?: ReferrerPolicy;
+ relative_urls?: boolean;
+ remove_script_host?: boolean;
+ remove_trailing_brs?: boolean;
+ removed_menuitems?: string;
+ resize?: boolean | 'both';
+ resize_img_proportional?: boolean;
+ root_name?: string;
+ sandbox_iframes?: boolean;
+ sandbox_iframes_exclusions?: string[];
+ schema?: SchemaType;
+ selector?: string;
+ setup?: SetupCallback;
+ sidebar_show?: string;
+ skin?: boolean | string;
+ skin_url?: string;
+ smart_paste?: boolean;
+ statusbar?: boolean;
+ style_formats?: AllowedFormat[];
+ style_formats_autohide?: boolean;
+ style_formats_merge?: boolean;
+ submit_patch?: boolean;
+ suffix?: string;
+ table_tab_navigation?: boolean;
+ target?: HTMLElement;
+ text_patterns?: RawPattern[] | false;
+ text_patterns_lookup?: RawDynamicPatternsLookup;
+ theme?: string | ThemeInitFunc | false;
+ theme_url?: string;
+ toolbar?: boolean | string | string[] | Array;
+ toolbar1?: string;
+ toolbar2?: string;
+ toolbar3?: string;
+ toolbar4?: string;
+ toolbar5?: string;
+ toolbar6?: string;
+ toolbar7?: string;
+ toolbar8?: string;
+ toolbar9?: string;
+ toolbar_groups?: Record;
+ toolbar_location?: ToolbarLocation;
+ toolbar_mode?: ToolbarMode;
+ toolbar_sticky?: boolean;
+ toolbar_sticky_offset?: number;
+ typeahead_urls?: boolean;
+ ui_mode?: 'combined' | 'split';
+ url_converter?: URLConverter;
+ url_converter_scope?: any;
+ urlconverter_callback?: URLConverterCallback;
+ valid_children?: string;
+ valid_classes?: string | Record;
+ valid_elements?: string;
+ valid_styles?: string | Record;
+ verify_html?: boolean;
+ visual?: boolean;
+ visual_anchor_class?: string;
+ visual_table_class?: string;
+ width?: number | string;
+ xss_sanitization?: boolean;
+ license_key?: string;
+ disable_nodechange?: boolean;
+ forced_plugins?: string | string[];
+ plugin_base_urls?: Record;
+ service_message?: string;
+ [key: string]: any;
+}
+interface RawEditorOptions extends BaseEditorOptions {
+ external_plugins?: Record;
+ mobile?: RawEditorOptions;
+ plugins?: string | string[];
+}
+interface NormalizedEditorOptions extends BaseEditorOptions {
+ external_plugins: Record;
+ forced_plugins: string[];
+ plugins: string[];
+}
+interface EditorOptions extends NormalizedEditorOptions {
+ a11y_advanced_options: boolean;
+ allow_unsafe_link_target: boolean;
+ anchor_bottom: string;
+ anchor_top: string;
+ automatic_uploads: boolean;
+ block_formats: string;
+ body_class: string;
+ body_id: string;
+ br_newline_selector: string;
+ color_map: string[];
+ color_cols: number;
+ color_cols_foreground: number;
+ color_cols_background: number;
+ color_default_background: string;
+ color_default_foreground: string;
+ content_css: string[];
+ contextmenu: string[];
+ convert_unsafe_embeds: boolean;
+ custom_colors: boolean;
+ default_font_stack: string[];
+ document_base_url: string;
+ init_content_sync: boolean;
+ draggable_modal: boolean;
+ editable_class: string;
+ editable_root: boolean;
+ font_css: string[];
+ font_family_formats: string;
+ font_size_classes: string;
+ font_size_formats: string;
+ font_size_input_default_unit: string;
+ font_size_legacy_values: string;
+ font_size_style_values: string;
+ forced_root_block: string;
+ forced_root_block_attrs: Record;
+ format_noneditable_selector: string;
+ height: number | string;
+ highlight_on_focus: boolean;
+ iframe_attrs: Record;
+ images_file_types: string;
+ images_upload_base_path: string;
+ images_upload_credentials: boolean;
+ images_upload_url: string;
+ indent_use_margin: boolean;
+ indentation: string;
+ inline: boolean;
+ inline_boundaries_selector: string;
+ language: string;
+ language_load: boolean;
+ language_url: string;
+ line_height_formats: string;
+ menu: Record;
+ menubar: boolean | string;
+ model: string;
+ newdocument_content: string;
+ no_newline_selector: string;
+ noneditable_class: string;
+ noneditable_regexp: RegExp[];
+ object_resizing: string;
+ pad_empty_with_br: boolean;
+ paste_as_text: boolean;
+ preview_styles: string;
+ promotion: boolean;
+ readonly: boolean;
+ removed_menuitems: string;
+ sandbox_iframes: boolean;
+ sandbox_iframes_exclusions: string[];
+ toolbar: boolean | string | string[] | Array;
+ toolbar_groups: Record;
+ toolbar_location: ToolbarLocation;
+ toolbar_mode: ToolbarMode;
+ toolbar_persist: boolean;
+ toolbar_sticky: boolean;
+ toolbar_sticky_offset: number;
+ text_patterns: Pattern[];
+ text_patterns_lookup: DynamicPatternsLookup;
+ visual: boolean;
+ visual_anchor_class: string;
+ visual_table_class: string;
+ width: number | string;
+ xss_sanitization: boolean;
+}
+type StyleMap = Record;
+interface StylesSettings {
+ allow_script_urls?: boolean;
+ allow_svg_data_urls?: boolean;
+ url_converter?: URLConverter;
+ url_converter_scope?: any;
+}
+interface Styles {
+ parse: (css: string | undefined) => Record;
+ serialize: (styles: StyleMap, elementName?: string) => string;
+}
+type EventUtilsCallback = (event: EventUtilsEvent) => void | boolean;
+type EventUtilsEvent = NormalizedEvent & {
+ metaKey: boolean;
+};
+interface Callback$1 {
+ func: EventUtilsCallback;
+ scope: any;
+}
+interface CallbackList extends Array> {
+ fakeName: string | false;
+ capture: boolean;
+ nativeHandler: EventListener;
+}
+interface EventUtilsConstructor {
+ readonly prototype: EventUtils;
+ new (): EventUtils;
+ Event: EventUtils;
+}
+declare class EventUtils {
+ static Event: EventUtils;
+ domLoaded: boolean;
+ events: Record>>;
+ private readonly expando;
+ private hasFocusIn;
+ private count;
+ constructor();
+ bind(target: any, name: K, callback: EventUtilsCallback, scope?: any): EventUtilsCallback;
+ bind(target: any, names: string, callback: EventUtilsCallback, scope?: any): EventUtilsCallback;
+ unbind(target: any, name: K, callback?: EventUtilsCallback): this;
+ unbind(target: any, names: string, callback?: EventUtilsCallback): this;
+ unbind(target: any): this;
+ fire(target: any, name: string, args?: {}): this;
+ dispatch(target: any, name: string, args?: {}): this;
+ clean(target: any): this;
+ destroy(): void;
+ cancel(e: EventUtilsEvent): boolean;
+ private executeHandlers;
+}
+interface SetAttribEvent {
+ attrElm: HTMLElement;
+ attrName: string;
+ attrValue: string | boolean | number | null;
+}
+interface DOMUtilsSettings {
+ schema: Schema;
+ url_converter: URLConverter;
+ url_converter_scope: any;
+ ownEvents: boolean;
+ keep_values: boolean;
+ update_styles: boolean;
+ root_element: HTMLElement | null;
+ collect: boolean;
+ onSetAttrib: (event: SetAttribEvent) => void;
+ contentCssCors: boolean;
+ referrerPolicy: ReferrerPolicy;
+}
+type Target = Node | Window;
+type RunArguments = string | T | Array | null;
+type BoundEvent = [
+ Target,
+ string,
+ EventUtilsCallback,
+ any
+];
+type Callback = EventUtilsCallback>;
+type RunResult = T extends Array ? R[] : false | R;
+interface DOMUtils {
+ doc: Document;
+ settings: Partial;
+ win: Window;
+ files: Record;
+ stdMode: boolean;
+ boxModel: boolean;
+ styleSheetLoader: StyleSheetLoader;
+ boundEvents: BoundEvent[];
+ styles: Styles;
+ schema: Schema;
+ events: EventUtils;
+ root: Node | null;
+ isBlock: {
+ (node: Node | null): node is HTMLElement;
+ (node: string): boolean;
+ };
+ clone: (node: Node, deep: boolean) => Node;
+ getRoot: () => HTMLElement;
+ getViewPort: (argWin?: Window) => GeomRect;
+ getRect: (elm: string | HTMLElement) => GeomRect;
+ getSize: (elm: string | HTMLElement) => {
+ w: number;
+ h: number;
+ };
+ getParent: {
+ (node: string | Node | null, selector: K, root?: Node): HTMLElementTagNameMap[K] | null;
+ (node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node): T | null;
+ (node: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node): Node | null;
+ };
+ getParents: {
+ (elm: string | HTMLElementTagNameMap[K] | null, selector: K, root?: Node, collect?: boolean): Array;
+ (node: string | Node | null, selector: string | ((node: Node) => node is T), root?: Node, collect?: boolean): T[];
+ (elm: string | Node | null, selector?: string | ((node: Node) => boolean | void), root?: Node, collect?: boolean): Node[];
+ };
+ get: {
+ (elm: T): T;
+ (elm: string): HTMLElement | null;
+ };
+ getNext: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null;
+ getPrev: (node: Node | null, selector: string | ((node: Node) => boolean)) => Node | null;
+ select: {
+ (selector: K, scope?: string | Node): Array;
+ (selector: string, scope?: string | Node): T[];
+ };
+ is: {
+