diff --git a/.coveragerc b/.coveragerc
index e7ca5551..46a3d86d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,6 +1,8 @@
[run]
branch = True
-source = djangocms_text_ckeditor
+source =
+ djangocms_text
+ djangocms_text_ckeditor
omit =
migrations/*
tests/*
diff --git a/.github/workflows/publish-to-live-pypi.yml b/.github/workflows/publish-to-live-pypi.yml
index 1607f772..e20962ed 100644
--- a/.github/workflows/publish-to-live-pypi.yml
+++ b/.github/workflows/publish-to-live-pypi.yml
@@ -22,6 +22,13 @@ jobs:
pip install
build
--user
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - name: Install dependencies
+ run: npm install
+ - name: Build client
+ run: webpack --mode production
- name: Build a binary wheel and a source tarball
run: >-
python -m
diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml
index d590f480..9ab14a10 100644
--- a/.github/workflows/publish-to-test-pypi.yml
+++ b/.github/workflows/publish-to-test-pypi.yml
@@ -3,7 +3,7 @@ name: Publish Python 🐍 distributions 📦 to TestPyPI
on:
push:
branches:
- - master
+ - main
jobs:
build-n-publish:
@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- - name: Set up Python 3.9
+ - name: Set up Python 3.12
uses: actions/setup-python@v1
with:
- python-version: 3.9
+ python-version: 3.12
- name: Install pypa/build
run: >-
@@ -22,6 +22,14 @@ jobs:
pip install
build
--user
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - name: Install dependencies
+ run: npm install
+ - name: Build client
+ run: webpack --mode production
+
- name: Build a binary wheel and a source tarball
run: >-
python -m
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8647eebd..b25a8d1f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,10 +8,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: [ "3.10", "3.11"]
+ python-version: [ "3.10", "3.11", "3.12"]
requirements-file: [
- dj32_cms311.txt,
- dj32_cms41.txt,
dj42_cms311.txt,
dj42_cms41.txt,
dj50_cms41.txt
diff --git a/README.rst b/README.rst
index 04d07575..bfa484b6 100644
--- a/README.rst
+++ b/README.rst
@@ -731,7 +731,7 @@ Old djangocms-text-ckeditor readme:
:target: http://badge.fury.io/py/djangocms-text
.. |coverage| image:: https://codecov.io/gh/django-cms/djangocms-text/branch/main/graph/badge.svg
:target: https://codecov.io/gh/django-cms/djangocms-text
-.. |python| image:: https://img.shields.io/badge/python-3.7+-blue.svg
+.. |python| image:: https://img.shields.io/badge/python-3.10+-blue.svg
:target: https://pypi.org/project/djangocms-text/
.. |django| image:: https://img.shields.io/badge/django-3.2--5.0-blue.svg
:target: https://www.djangoproject.com/
diff --git a/djangocms_text/cms_plugins.py b/djangocms_text/cms_plugins.py
index 2d7eaabf..3b4869b8 100644
--- a/djangocms_text/cms_plugins.py
+++ b/djangocms_text/cms_plugins.py
@@ -587,6 +587,14 @@ def get_child_plugin_candidates(cls, slot, page):
]
return text_enabled_plugins
+ def render_plugin_icon(self, plugin):
+ icon = getattr(plugin, "text_icon", None)
+ if icon is None:
+ return
+ if "cms-icon" in icon:
+ return f''
+ return icon
+
def get_plugins(self, obj=None):
plugin = getattr(self, "cms_plugin_instance", None) or obj
if not plugin:
@@ -614,7 +622,7 @@ def get_plugins(self, obj=None):
{
"value": plugin.value,
"name": names.get(plugin.value, plugin.name),
- "icon": getattr(plugin, "text_icon", None),
+ "icon": self.render_plugin_icon(plugin),
"module": modules.get(plugin.value, plugin.module),
}
)
diff --git a/djangocms_text/editors.py b/djangocms_text/editors.py
index 3780f124..a757fa7c 100644
--- a/djangocms_text/editors.py
+++ b/djangocms_text/editors.py
@@ -183,20 +183,6 @@ def default(self, obj):
+ ' \n'
+ "",
},
- "LinkPlugin": {
- "title": _("Link"),
- "icon": '",
- },
- "ImagePlugin": {
- "title": _("Image"),
- "icon": '",
- },
"Unlink": {
"title": _("Unlink"),
"icon": '',
}
}
diff --git a/djangocms_text/widgets.py b/djangocms_text/widgets.py
index c085cd1d..9c2f67a8 100644
--- a/djangocms_text/widgets.py
+++ b/djangocms_text/widgets.py
@@ -100,7 +100,13 @@ def render_textarea(self, name, value, attrs=None, renderer=None):
return super().render(name, value, attrs, renderer)
def get_toolbar_setting(self, toolbar):
- return get_editor_base_config()
+ toolbar_setting = get_editor_base_config()
+ for plugin in self.installed_plugins:
+ toolbar_setting[plugin["value"]] = {
+ "title": plugin["name"],
+ "icon": plugin["icon"],
+ }
+ return toolbar_setting
def get_editor_settings(self, language):
configuration = deepcopy(self.configuration)
diff --git a/private/css/cms.balloon-toolbar.css b/private/css/cms.balloon-toolbar.css
index 998d9406..7014e76e 100644
--- a/private/css/cms.balloon-toolbar.css
+++ b/private/css/cms.balloon-toolbar.css
@@ -7,7 +7,7 @@
border-radius: 3px;
box-shadow: none;
/* box-shadow: 0 1.5px 1.5px rgba(var(--dca-shadow),.4); */
- right: calc(100% + 1rem);
+ inset-inline-end: calc(100% + 1rem);
width: calc(1.6*var(--size));
height: calc(1.6*var(--size));
line-height: calc(1.3*var(--size));
@@ -23,7 +23,8 @@
[role="menubar"] {
padding: 3px;
position: absolute;
- top: 100%;
+ inset-block-start: 100%;
+ inset-block-end: unset;
bottom: unset;
width: unset;
max-width: 100vw;
diff --git a/private/css/cms.linkfield.css b/private/css/cms.linkfield.css
index 282c7fb2..c2d432a6 100644
--- a/private/css/cms.linkfield.css
+++ b/private/css/cms.linkfield.css
@@ -3,8 +3,8 @@
font-size: 0.8rem;
position: relative;
input[type="text"] {
- padding-right: 3em;
- background: var(--dca-white) url('data:image/svg+xml;utf8,') no-repeat right center;
+ padding-inline-end: 3em;
+ background: var(--dca-white) url('data:image/svg+xml;utf8,') no-repeat inline-end center;
background-size: auto 1em;
}
.cms-linkfield-selected {
@@ -18,16 +18,16 @@
z-index: 1;
visibility: hidden;
position: absolute;
- max-height: 400px;
+ max-block-size: 400px;
overflow: auto;
- left: 0;
- top: 100%;
+ inset-inline-start: 0;
+ inset-block-start: 100%;
border: 1px solid var(--dca-gray-lighter);
background: var(--dca-white);
- width: 120%;
+ inline-size: 120%;
resize: both;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
+ border-end-start-radius: 4px;
+ border-end-end-radius: 4px;
box-shadow: 0 1.5px 1.5px rgba(var(--dca-shadow),.4);
.cms-linkfield-error {
color: red;
@@ -37,7 +37,7 @@
padding: 0.5rem 6px;
white-space: nowrap;
font-weight: normal;
- border-bottom: 1px solid var(--dca-gray-lighter);
+ border-block-end: 1px solid var(--dca-gray-lighter);
&:last-child {
border-bottom: none;
}
diff --git a/private/css/cms.tiptap.css b/private/css/cms.tiptap.css
index cd0d2ab2..4834a432 100644
--- a/private/css/cms.tiptap.css
+++ b/private/css/cms.tiptap.css
@@ -16,7 +16,7 @@
.ProseMirror {
overflow-y: scroll;
padding: 1rem 0.8rem 0.2rem 0.2rem;
- border-top: 2px solid var(--dca-gray-lighter);
+ border-top: 2px solid var(--dca-gray-lighter, var(--hairline-color));
&:focus-visible {
outline: none;
}
@@ -30,7 +30,7 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
.cms-editor-inline-wrapper {
&.textarea .tiptap {
- border: 1px solid var(--dca-gray-lighter);
+ border: 1px solid var(--dca-gray-lighter, var(--hairline-color));
padding: 6px;
min-height: 3rem;
border-radius: 3px;
@@ -71,10 +71,10 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
pointer-events: all;
display: flex;
flex-flow: row wrap;
- border: solid 1px var(--dca-gray-light);
+ border: solid 1px var(--dca-gray-light, var(--hairline-color));
box-shadow: 0 1.5px 1.5px rgba(var(--dca-shadow),.4);
- color: var(--dca-black);
- background: var(--dca-white);
+ color: var(--dca-black, var(--body-fg));
+ background: var(--dca-white, var(--body-bg));
opacity: 1;
div.grouper {
padding: 0;
@@ -94,11 +94,11 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
outline: none;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
- border: 1px solid var(--dca-gray-lighter);
+ border: 1px solid var(--dca-gray-lighter, var(--hairline-color));
border-bottom: none;
.dropdown-content {
- top: 100%;
- left: 0;
+ inset-block-start: 100%;
+ inset-inline-start: 0;
}
}
button, [role="button"] {
@@ -112,18 +112,19 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
justify-content: center;
cursor: pointer;
border: none !important;
- background: var(--dca-white);
+ color: var(--dca-black, var(--body-fg));
+ background: var(--dca-white, var(--body-bg));
border-radius: 2px;
text-align: center;
vertical-align: middle;
line-height: 1.2;
padding: 6px 4px !important;
&:active, &.active {
- background: var(--dca-gray-lighter) !important;
+ background: var(--dca-gray-lighter, var(--selected-bg)) !important;
}
&:hover:not(:disabled),&.show {
- color: var(--dca-white) !important;
- background: var(--dca-primary) !important;
+ color: var(--dca-white, var(--button-fg)) !important;
+ background: var(--dca-primary, var(--button-bg)) !important;
}
&:disabled {
color: var(--dca-gray-light);
@@ -133,7 +134,13 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
&.dropdown {
position: relative;
font-size: 0.8rem;
- padding: 6px 8px !important;
+ padding: 6px 4px !important;
+ &:after{
+ content: "▼";
+ margin-block-start: 3px;
+ margin-inline-start: 6px;
+ font-size: 0.8rem;
+ }
}
svg {
zoom: 120%;
@@ -142,31 +149,51 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
}
}
.dropdown-content {
- color: var(--dca-black);
+ color: var(--dca-black, var(--body-fg));
border-radius: 0;
visibility: hidden;
position: absolute;
- top: 100%;
- left: 0;
+ inset-block-start: 100%;
+ inset-inline-start: 0;
display: flex;
flex-flow: row;
- border: solid 1px var(--dca-gray-light);
+ border: solid 1px var(--dca-gray-light, var(--hairline-color));
box-shadow: 0 1.5px 1.5px rgba(var(--dca-shadow),.4);
- background: var(--dca-white);
+ background: var(--dca-white, var(--body-bg));
padding: 6px 0.8rem;
&.vertical {
flex-flow: column;
font-size: 1rem;
padding: 0;
button {
- text-align: left;
- justify-content: left;
+ text-align: start;
+ justify-content: start;
padding: 8px 1.2rem !important;
small {
margin: 0;
}
}
}
+ &.plugins {
+ max-height: 16rem;
+ overflow-y: auto;
+ text-align: start;
+ .header {
+ background: var(--dca-gray-lighter, var(--hairline-color));
+ padding-top: 0.4rem;
+ padding-bottom: 0.4rem;
+ padding-inline-start: 6px;
+ }
+ button {
+ > * {
+ width: 1rem;
+ margin-inline-end: 0.8rem;
+ margin-inline-start: -0.3rem;
+ }
+ text-align: start;
+ white-space: nowrap;
+ }
+ }
}
span:empty {
margin-left: 4px;
@@ -181,8 +208,8 @@ p:not(.is-empty) > br.ProseMirror-trailingBreak {
}
.dropback, .toolbar-dropback {
position: fixed;
- top: 0;
- left: 0;
+ inset-block-start: 0;
+ inset-inline-start: 0;
width: 100%;
height: 100%;
z-index: -1;
diff --git a/private/js/cms.editor.js b/private/js/cms.editor.js
index 7d57ecca..ba3d3b85 100644
--- a/private/js/cms.editor.js
+++ b/private/js/cms.editor.js
@@ -185,8 +185,15 @@ class CMSEditor {
});
}
- // CMS Editor: get_settings
- // Get settings from json script element
+ /**
+ * Retrieves the settings for the given editor.
+ * If the element is a string, it will be treated as an element's ID.
+ * Reads settings from a json script element.
+ *
+ * @param {string|HTMLElement} el - The element or element's ID to retrieve the settings for.
+ *
+ * @return {Object} - The settings object for the element.
+ */
getSettings(el) {
if (typeof el === "string") {
if (this._editor_settings[el]) {
@@ -205,6 +212,18 @@ class CMSEditor {
return {};
}
+ /**
+ * Retrieves the list of installed plugins. (Returns empty list of no editor has been initialized.)
+ *
+ * @returns {Array} - The list of installed plugins.
+ */
+ getInstalledPlugins() {
+ if (this._editor_settings) {
+ return this.getSettings(Object.keys(this._editor_settings)[0]).installed_plugins;
+ }
+ return [];
+ }
+
// CMS Editor: init_all
initAll () {
// Get global options from script element
diff --git a/private/js/cms.tiptap.js b/private/js/cms.tiptap.js
index 887aa5ce..c4977f9d 100644
--- a/private/js/cms.tiptap.js
+++ b/private/js/cms.tiptap.js
@@ -201,7 +201,7 @@ class CMSTipTapPlugin {
}
_createBlockToolbar(el, editor, options) {
- const toolbar = this._populateToolbar(options.toolbar || this.settings.toolbar_HTMLField, 'block');
+ const toolbar = this._populateToolbar(editor,options.toolbar || this.settings.toolbar_HTMLField, 'block');
const ballonToolbar = new CmsBalloonToolbar(editor, toolbar,
(event) => this._handleToolbarClick(event, editor),
(el) => this._updateToolbar(editor, el));
@@ -213,7 +213,7 @@ class CMSTipTapPlugin {
toolbarElement.classList.add('cms-toolbar');
// create the toolbar html from the settings
- toolbarElement.innerHTML = `
${this._populateToolbar(toolbar, filter)}`;
+ toolbarElement.innerHTML = `${this._populateToolbar(editor, toolbar, filter)}`;
toolbarElement.querySelector('.toolbar-dropback').addEventListener('click', (event) => {
console.log(toolbarElement.querySelector('.dropdown.show'));
@@ -319,7 +319,7 @@ class CMSTipTapPlugin {
editor.commands.focus();
}
} else if (TiptapToolbar[action]) {
- TiptapToolbar[action].action(editor, event);
+ TiptapToolbar[action].action(editor, button);
this._updateToolbar(editor);
// Close dropdowns after command execution
this._closeAllDropdowns(event, editor);
@@ -353,7 +353,7 @@ class CMSTipTapPlugin {
.forEach((el) => el.classList.remove('show'));
}
- _populateToolbar(array, filter) {
+ _populateToolbar(editor, array, filter) {
let html = '';
for (let item of array) {
if (item in TiptapToolbar && TiptapToolbar[item].insitu) {
@@ -366,12 +366,12 @@ class CMSTipTapPlugin {
item.icon = repr.icon;
}
if (Array.isArray(item)) {
- const group = this._populateToolbar(item, filter);
+ const group = this._populateToolbar(editor, item, filter);
if (group.length > 0) {
- html += this._populateToolbar(item, filter) + this.separator_markup;
+ html += this._populateToolbar(editor, item, filter) + this.separator_markup;
}
} else if (item.constructor === Object) {
- const dropdown = this._populateToolbar(item.items, filter);
+ const dropdown = this._populateToolbar(editor, item.items, filter);
// Are there any items in the dropdown?
if (dropdown.replaceAll(this.separator_markup, '').replaceAll(this.space_markup, '').length > 0) {
const title = item.title && item.icon ? `title='${item.title}' ` : '';
@@ -400,7 +400,12 @@ class CMSTipTapPlugin {
break;
default:
// Button
- html += this._createToolbarButton(item, filter);
+ if (item in TiptapToolbar && TiptapToolbar[item].render) {
+ html += TiptapToolbar[item].render(editor, TiptapToolbar[item], filter);
+ } else {
+ html += this._createToolbarButton(editor, item, filter);
+ }
+ break;
}
}
}
@@ -418,21 +423,39 @@ class CMSTipTapPlugin {
* @param {string} item - The item to get the representation for.
* @return {string} - The representation of the specified item, or the "failed" representation from the TiptapToolbar.
*/
- _getRepresentation(item) {
- if (this.lang && item in this.lang) {
+ _getRepresentation(item, filter) {
+ if (item.endsWith('Plugin')) {
+ for (const plugin of window.CMS_Editor.getInstalledPlugins()) {
+ if (plugin.value === item && filter !== 'block') {
+ return {
+ title: plugin.name,
+ icon: plugin.icon,
+ cmsplugin: plugin.value,
+ dataaction: 'CMSPlugins',
+ };
+ }
+ }
+ return null;
+ }
+ if (this.lang && item in this.lang && item in TiptapToolbar) {
+ if (filter && filter !== TiptapToolbar[item].type) {
+ return null;
+ }
return Object.assign({}, TiptapToolbar[item] || {}, this.lang[item]);
}
return TiptapToolbar.failed;
}
// create the html for a toolbar button
- _createToolbarButton(itemName, filter) {
+ _createToolbarButton(editor, itemName, filter) {
const item = itemName.split(' ')[0];
- if (TiptapToolbar[item] && (!filter || TiptapToolbar[item].type === filter)) {
- const repr = this._getRepresentation(item);
+ const repr = this._getRepresentation(item, filter);
+ if (repr) {
+ repr.dataaction = repr.dataaction || item;
const title = repr && repr.icon ? `title='${repr.title}' ` : '';
const position = repr.position ? `style="float :${repr.position};" ` : '';
+ const cmsplugin = repr.cmsplugin ? `data-cmsplugin="${repr.cmsplugin}" ` : '';
let form = '';
let classes = 'button';
if (repr.toolbarForm) {
@@ -449,7 +472,7 @@ class CMSTipTapPlugin {
`;
}
- return `