Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f2960fb
draft shortcode integration into tinymce
PeterNerlich Sep 10, 2025
c4bedf0
WIP
PeterNerlich Sep 15, 2025
1831ed4
WIP add comments describing methods
PeterNerlich Oct 3, 2025
761bd6a
WIP fix parsing, handle unknown shortcode
PeterNerlich Oct 5, 2025
7724a10
WIP fix toolbar
PeterNerlich Oct 5, 2025
ec1a7aa
WIP fix dynamically adding/removing arguments
PeterNerlich Oct 5, 2025
03b52d3
WIP required/optional kwargs (TODO: ts set methods not recognized → u…
PeterNerlich Oct 5, 2025
7ac6934
WIP enable polyfills from new ecma specs
PeterNerlich Oct 6, 2025
080dc30
WIP fix arguments
PeterNerlich Oct 7, 2025
e983096
WIP
PeterNerlich Oct 9, 2025
fd333b5
WIP fix some kwarg stuff
PeterNerlich Oct 10, 2025
d9f87d7
WIP fix kwargs handling, disable add/remove buttons accordingly
PeterNerlich Oct 12, 2025
3f78f3f
WIP introduce symbol to define acceppting arbitrary arguments
PeterNerlich Oct 15, 2025
385503d
WIP explicit arg getters
PeterNerlich Oct 17, 2025
840bcf0
WIP display argument names and descriptions
PeterNerlich Oct 19, 2025
6f3ee35
WIP uh I forgot
PeterNerlich Oct 22, 2025
f83ef1a
WIP: add page shortcode UI
PeterNerlich Feb 6, 2026
b42e152
WIP finished proper page data cache
PeterNerlich Feb 8, 2026
571a77e
WIP fix other languages, cache population
PeterNerlich Feb 9, 2026
76807c8
WIP fix overwriting add/edit/remove things after object creation
PeterNerlich Feb 9, 2026
aa7a0d0
WIP better preview
PeterNerlich Feb 9, 2026
bc5a6be
WIP: make old link plugin not pop up toolbar for page links
PeterNerlich Feb 9, 2026
5fe8e70
WIP shortcodes
PeterNerlich Feb 16, 2026
a29e0d0
WIP refresh preview nodes after fetching page metadata
PeterNerlich Apr 28, 2026
8c7df67
WIP hint at unimplemented lang kwarg
PeterNerlich Apr 28, 2026
3b9d303
WIP adjust tooltip texts for page shortcode
PeterNerlich May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integreat_cms/api/v3/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions integreat_cms/cms/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion integreat_cms/cms/templates/_tinymce_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
<div id="tinymce-config-options"
data-base-url="{% get_base_url %}"
data-webapp-url="{% get_webapp_url %}"
data-language="{{ LANGUAGE_CODE|slice:'0:2' }}"
data-region-slug="{{ request.region.slug }}"
data-language-slug="{{ language_slug }}"
data-directionality="{% if right_to_left %}rtl{% else %}ltr{% endif %}"
data-no-translate-tooltip='{% translate "Do not translate the selected text." %}'
data-no-translate-text='{% translate "Do not translate" %}'
Expand Down Expand Up @@ -74,7 +77,7 @@
data-link-dialog-title-text='{% translate "Add Link" %}'
data-link-dialog-url-text='{% translate "URL" %}'
data-link-dialog-text-text='{% translate "Text to display" %}'
data-link-dialog-internal_link-text='{% translate "Or link to existing content" %}'
data-link-dialog-internal_link-text='{% translate "Search for a page" %}'
data-link-dialog-autoupdate-text='{% translate "Automatically use the title of the linked content for the link" %}'
data-custom-plugins="{% get_base_url %}{{ editor_content_js_files.0.url }}"
data-content-css="{% get_base_url %}{{ editor_content_css_files.0.url }}"
Expand Down
1 change: 1 addition & 0 deletions integreat_cms/static/src/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ $fp-4x3-path: "flagpack-dart-sass/flags/4x3/";
}

@import "./upload_form.css";
@import "./tinymce_ui_overrides";
@import "./tomselect.scss";

$list-bg-color: white;
Expand Down
21 changes: 0 additions & 21 deletions integreat_cms/static/src/css/tinymce_custom.css

This file was deleted.

50 changes: 50 additions & 0 deletions integreat_cms/static/src/css/tinymce_custom_content_styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@import "../fonts/open-sans/open-sans.css";
@import "../fonts/lateef/lateef.css";
@import "../fonts/noto-sans-sc/noto-sans-sc.css";
@import "../fonts/noto-sans-ethiopic/noto-sans-ethiopic.css";
@import "../fonts/noto-sans-georgian/noto-sans-georgian.css";
@import "./contact_card.css";

/* Enforce expected standard link color,
so setting a different color on specific links to communicate changes
doesn't backfire in some edge case on some device where the defaults are changed */
:link { color: #0000EE; }
:visited { color: #551A8B; }
:link:active, :visited:active { color: #FF0000; }


[translate="no"]:not(.contact-card) {
background-color: rgb(239, 68, 68);
direction: ltr;
display: inline-block;
white-space: pre-wrap;
}
[contenteditable="false"] a {
pointer-events: none;
}
.contact-card {
cursor: default !important;
font-family: "Open Sans" !important;
font-size: 1rem !important;
}

/* Colored border version
[data-shortcode="page"] {
border: .15em solid rgba(0,0,255, 0.5);
border-radius: .2em;
}
[data-shortcode="page"]:has(.error) {
border-color: rgba(255,63,0, 0.5);
}
*/

/* Text color version */
[data-shortcode="page"], [data-shortcode="page"] a { /* mention the link specifically so it binds stronger than :visited on the initialization */
color: #008866;
}
[data-shortcode="page"]:has(.error) {
color: #CC6600;
text-decoration: underline;
text-decoration-style: wavy;
text-decoration-color: #CC6600;
}
8 changes: 8 additions & 0 deletions integreat_cms/static/src/css/tinymce_ui_overrides.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.tox label.tox-label {
/* .tox label.tox-label
* is a more specific selector than
* .tox .tox-label
* and thus takes precedence over the default
*/
white-space: wrap;
}
3 changes: 2 additions & 1 deletion integreat_cms/static/src/editor_content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "./js/tinymce-plugins/autolink_tel/plugin.js";
import "./js/tinymce-plugins/custom_link_input/plugin.js";
import "./js/tinymce-plugins/custom_contact_input/plugin.js";
import "./js/tinymce-plugins/mediacenter/plugin.js";
import "./js/tinymce-plugins/shortcodes/plugin.js";

/* Custom tinymce content css */
import "./css/tinymce_custom.css";
import "./css/tinymce_custom_content_styles.css";
3 changes: 2 additions & 1 deletion integreat_cms/static/src/js/forms/tinymce-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ window.addEventListener("load", () => {
},
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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getCsrfToken } from "../../utils/csrf-token";
import { stripProtocol } from "../../utils/url-tools";

(() => {
const tinymceConfig = document.getElementById("tinymce-config-options");
Expand Down Expand Up @@ -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) {
Expand Down
74 changes: 74 additions & 0 deletions integreat_cms/static/src/js/tinymce-plugins/shortcodes/contact.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading