[MIG] web_editor_class_selector: Migration to version 18.0

pull/3161/head
Carlos Lopez 2025-04-22 09:48:48 -05:00
parent fa26af9fa2
commit e871d8a88e
14 changed files with 163 additions and 175 deletions

View File

@ -17,13 +17,13 @@ Web editor class selector
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/17.0/web_editor_class_selector
:target: https://github.com/OCA/web/tree/18.0/web_editor_class_selector
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_editor_class_selector
:target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_editor_class_selector
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=17.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@ -60,7 +60,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_editor_class_selector%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_editor_class_selector%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@ -85,6 +85,14 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/17.0/web_editor_class_selector>`_ project on GitHub.
.. |maintainer-carlos-lopez-tecnativa| image:: https://github.com/carlos-lopez-tecnativa.png?size=40px
:target: https://github.com/carlos-lopez-tecnativa
:alt: carlos-lopez-tecnativa
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-carlos-lopez-tecnativa|
This module is part of the `OCA/web <https://github.com/OCA/web/tree/18.0/web_editor_class_selector>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -1,6 +1,6 @@
{
"name": "Web editor class selector",
"version": "17.0.1.1.0",
"version": "18.0.1.0.0",
"summary": "",
"author": "Tecnativa, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
@ -17,15 +17,14 @@
],
"assets": {
"web.assets_backend": [
"web_editor_class_selector/static/src/js/backend/**/*",
],
"web_editor.backend_assets_wysiwyg": [
"web_editor_class_selector/static/src/js/odoo-editor/**/*",
"web_editor_class_selector/static/src/js/wysiwyg/**/*",
"web_editor_class_selector/static/src/js/css_selector/**/*",
"web_editor_class_selector/static/src/scss/demo_styles.scss",
"web_editor_class_selector/static/src/xml/**/",
"web_editor_class_selector/static/src/js/fields/**/*",
"web_editor_class_selector/static/src/js/utils/**/*",
"web_editor_class_selector/static/src/js/wysiwyg/**/*",
],
},
"maintainers": ["carlos-lopez-tecnativa"],
"installable": True,
"auto_install": False,
"license": "AGPL-3",

View File

@ -369,7 +369,7 @@ ul.auto-toc {
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:53a338e09db97f68da45d08f963625f60a4069424d8d2f75bcca9887a201824b
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/17.0/web_editor_class_selector"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-17-0/web-17-0-web_editor_class_selector"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/web/tree/18.0/web_editor_class_selector"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_editor_class_selector"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/web&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows users to create custom CSS class records, which can
then be selected and applied directly in the HTML editor. Note: The
actual CSS file containing the class definitions is not provided by this
@ -408,7 +408,7 @@ supported)</p>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_editor_class_selector%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_editor_class_selector%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
@ -428,7 +428,9 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/17.0/web_editor_class_selector">OCA/web</a> project on GitHub.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/carlos-lopez-tecnativa"><img alt="carlos-lopez-tecnativa" src="https://github.com/carlos-lopez-tecnativa.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/18.0/web_editor_class_selector">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>

View File

@ -0,0 +1,24 @@
import {Component, useState} from "@odoo/owl";
import {Dropdown} from "@web/core/dropdown/dropdown";
import {DropdownItem} from "@web/core/dropdown/dropdown_item";
import {toolbarButtonProps} from "@html_editor/main/toolbar/toolbar";
export class CssSelector extends Component {
static template = "web_editor_class_selector.CssSelector";
static props = {
getItems: Function,
getDisplay: Function,
onSelected: Function,
...toolbarButtonProps,
};
static components = {Dropdown, DropdownItem};
setup() {
this.items = this.props.getItems();
this.state = useState(this.props.getDisplay());
}
onSelected(item) {
this.props.onSelected(item);
}
}

View File

@ -0,0 +1,20 @@
<templates xml:space="preserve">
<t t-name="web_editor_class_selector.CssSelector">
<Dropdown>
<button class="btn btn-light" t-att-title="props.title">
<span class="px-1" t-esc="state.displayName" />
</button>
<t t-set-slot="content">
<t t-foreach="items" t-as="item" t-key="item_index">
<DropdownItem
class="item.class_name"
onSelected="() => this.onSelected(item)"
t-on-pointerdown.prevent="() => {}"
>
<t t-esc="item.name" />
</DropdownItem>
</t>
</t>
</Dropdown>
</t>
</templates>

View File

@ -0,0 +1,71 @@
import {CssSelector} from "./css_selector.esm";
import {Plugin} from "@html_editor/plugin";
import {_t} from "@web/core/l10n/translation";
import {reactive} from "@odoo/owl";
import {closestElement} from "@html_editor/utils/dom_traversal";
import {isVisibleTextNode} from "@html_editor/utils/dom_info";
import {withSequence} from "@html_editor/utils/resource";
export class CssSelectorPlugin extends Plugin {
static id = "css_selector_plugin";
static dependencies = ["selection", "format"];
resources = {
toolbar_groups: [withSequence(60, {id: "css-selector"})],
toolbar_items: [
{
id: "css-selector",
groupId: "css-selector",
title: _t("Custom CSS"),
Component: CssSelector,
props: {
getItems: () => this.custom_class_css,
getDisplay: () => this.custom_css,
onSelected: (item) => {
this.dependencies.format.formatSelection(item.class_name, {
formatProps: {className: item.class_name},
applyStyle: true,
});
this.updateCustomCssSelectorParams();
},
},
},
],
/** Handlers */
selectionchange_handlers: [this.updateCustomCssSelectorParams.bind(this)],
post_undo_handlers: [this.updateCustomCssSelectorParams.bind(this)],
post_redo_handlers: [this.updateCustomCssSelectorParams.bind(this)],
};
setup() {
this.custom_css = reactive({displayName: this.defaultCustomCssName});
this.custom_class_css = this.config.custom_class_css;
}
updateCustomCssSelectorParams() {
this.custom_css.displayName = this.customCssName;
}
get defaultCustomCssName() {
return _t("Custom CSS");
}
get customCssName() {
const selectedNodes = this.dependencies.selection
.getSelectedNodes()
.filter(
(n) =>
n.nodeType === Node.TEXT_NODE &&
closestElement(n).isContentEditable &&
isVisibleTextNode(n)
);
let activeLabel = this.defaultCustomCssName;
for (const selectedTextNode of selectedNodes) {
const parentNode = selectedTextNode.parentElement;
for (const customCss of this.custom_class_css) {
const isActive = parentNode.classList.contains(customCss.class_name);
if (isActive) {
activeLabel = customCss.name;
break;
}
}
}
return activeLabel;
}
}

View File

@ -1,5 +1,5 @@
/** @odoo-module **/
import {HtmlField} from "@web_editor/js/backend/html_field";
import {CssSelectorPlugin} from "../css_selector/css_selector_plugin.esm";
import {HtmlField} from "@html_editor/fields/html_field";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
@ -18,14 +18,12 @@ patch(HtmlField.prototype, {
);
});
},
get wysiwygOptions() {
// Provide the custom_class_css to the toolbar through the toolbarOptions.
return {
...super.wysiwygOptions,
toolbarOptions: {
...super.wysiwygOptions.toolbarOptions,
custom_class_css: this.custom_class_css,
},
};
getConfig() {
// Add the new Plugin to the list of plugins.
// Provide the custom_class_css to the toolbar.
const config = super.getConfig(...arguments);
config.Plugins.push(CssSelectorPlugin);
config.custom_class_css = this.custom_class_css;
return config;
},
});

View File

@ -1,68 +0,0 @@
/** @odoo-module **/
import {
closestElement,
getSelectedNodes,
isVisibleTextNode,
} from "@web_editor/js/editor/odoo-editor/src/utils/utils";
import {OdooEditor} from "@web_editor/js/editor/odoo-editor/src/OdooEditor";
import {_t} from "@web/core/l10n/translation";
import {patch} from "@web/core/utils/patch";
patch(OdooEditor.prototype, {
_updateToolbar(show) {
const res = super._updateToolbar(show);
if (!this.toolbar || !this.custom_class_css) {
return res;
}
const sel = this.document.getSelection();
if (!this.isSelectionInEditable(sel)) {
return res;
}
// Get selected nodes within td to handle non-p elements like h1, h2...
// Targeting <br> to ensure span stays inside its corresponding block node.
const selectedNodesInTds = [
...this.editable.querySelectorAll(".o_selected_td"),
].map((node) => closestElement(node).querySelector("br"));
const selectedNodes = getSelectedNodes(this.editable).filter(
(n) =>
n.nodeType === Node.TEXT_NODE &&
closestElement(n).isContentEditable &&
isVisibleTextNode(n)
);
const selectedTextNodes = selectedNodes.length
? selectedNodes
: selectedNodesInTds;
let activeLabel = "";
for (const selectedTextNode of selectedTextNodes) {
const parentNode = selectedTextNode.parentElement;
for (const customCss of this.custom_class_css) {
const button = this.toolbar.querySelector("#" + customCss.class_name);
if (button) {
const isActive = parentNode.classList.contains(
customCss.class_name
);
button.classList.toggle("active", isActive);
if (isActive) {
activeLabel = button.textContent;
}
}
}
}
// Show current class active in the toolbar
// or remove active class if nothing is selected
const styleSection = this.toolbar.querySelector("#custom_class");
if (styleSection) {
if (!activeLabel) {
const css_selectors = this.toolbar.querySelectorAll(".css_selector");
for (const node of css_selectors) {
node.classList.toggle("active", false);
}
}
styleSection.querySelector("button span").textContent = activeLabel
? activeLabel
: _t("Custom CSS");
}
return res;
},
});

View File

@ -1,12 +0,0 @@
/** @odoo-module **/
import {editorCommands} from "@web_editor/js/editor/odoo-editor/src/commands/commands";
import {formatSelection} from "@web_editor/js/editor/odoo-editor/src/utils/utils";
const newCommands = {
setCustomCss: (editor, ...args) => {
const selectedId = parseInt(args[0], 10);
const record = editor.custom_class_css.find((item) => item.id === selectedId);
formatSelection(editor, record.class_name);
},
};
Object.assign(editorCommands, newCommands);

View File

@ -1,13 +0,0 @@
/** @odoo-module */
import {Toolbar} from "@web_editor/js/editor/toolbar";
import {patch} from "@web/core/utils/patch";
patch(Toolbar.props, {
...Toolbar.props,
custom_class_css: {type: Array, optional: true},
});
patch(Toolbar.defaultProps, {
...Toolbar.defaultProps,
custom_class_css: [],
});

View File

@ -1,10 +1,7 @@
/** @odoo-module **/
import {
closestElement,
formatsSpecs,
} from "@web_editor/js/editor/odoo-editor/src/utils/utils";
import {closestElement} from "@html_editor/utils/dom_traversal";
import {formatsSpecs} from "@html_editor/utils/formatting";
// This function is called in the _configureToolbar method of the Wysiwyg class
// This function is called in the getEditorConfig method of the Wysiwyg class
// It generates the new formatsSpecs object with the custom CSS class
export function createCustomCssFormats(custom_class_css) {
const newformatsSpecs = {};

View File

@ -1,17 +1,16 @@
/** @odoo-module **/
import {Wysiwyg} from "@web_editor/js/wysiwyg/wysiwyg";
import {createCustomCssFormats} from "../odoo-editor/utils.esm";
import {Wysiwyg} from "@html_editor/wysiwyg";
import {createCustomCssFormats} from "../utils/utils.esm";
import {patch} from "@web/core/utils/patch";
patch(Wysiwyg.prototype, {
_configureToolbar(options) {
super._configureToolbar(options);
getEditorConfig() {
const res = super.getEditorConfig(...arguments);
if (
options.toolbarOptions.custom_class_css &&
options.toolbarOptions.custom_class_css.length > 0
this.props.config.custom_class_css &&
this.props.config.custom_class_css.length > 0
) {
this.odooEditor.custom_class_css = options.toolbarOptions.custom_class_css;
createCustomCssFormats(options.toolbarOptions.custom_class_css);
createCustomCssFormats(this.props.config.custom_class_css);
}
return res;
},
});

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-inherit="web_editor.toolbar" t-inherit-mode="extension">
<xpath expr="//div[@id='chatgpt']" position="after">
<div
id="custom_class"
class="btn-group dropdown"
t-if="props.custom_class_css and props.custom_class_css.length"
>
<button
type="button"
class="btn dropdown-toggle"
data-bs-toggle="dropdown"
title="Custom CSS"
tabindex="-1"
data-bs-original-title="Custom CSS"
aria-expanded="false"
>
<span>Custom CSS</span>
</button>
<ul class="dropdown-menu">
<li t-foreach="props.custom_class_css" t-as="line" t-key="line.id">
<a
class="dropdown-item css_selector"
t-att-id="line.class_name"
href="#"
data-call="setCustomCss"
t-att-data-arg1="line.id"
>
<span t-att-class="line.class_name" t-out="line.name" />
</a>
</li>
</ul>
</div>
</xpath>
</t>
</templates>

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_web_editor_class_tree" model="ir.ui.view">
<field name="name">view.web.editor.class.tree</field>
<field name="name">view.web.editor.class.list</field>
<field name="model">web.editor.class</field>
<field name="arch" type="xml">
<tree>
<list>
<field name="sequence" widget="handle" />
<field name="name" />
<field name="class_name" />
</tree>
</list>
</field>
</record>
@ -50,7 +50,7 @@
<field name="name">Web Editor Class</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">web.editor.class</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Click here to add new Web Editor Class.