[ADD] web_editor_class_selector: new module to add custom CSS in HTML editor.

This module allows users to create custom CSS classes, which can then be selected and applied directly in the HTML editor.
This commit is contained in:
Carlos Lopez
2024-09-24 10:27:55 -05:00
parent 990d84e999
commit a0996d3626
22 changed files with 1002 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
/** @odoo-module **/
import {HtmlField} from "@web_editor/js/backend/html_field";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
const {onWillStart} = owl;
patch(HtmlField.prototype, "web_editor_class_selector.HtmlField", {
setup() {
this._super(...arguments);
this.orm = useService("orm");
this.custom_class_css = [];
onWillStart(async () => {
this.custom_class_css = await this.orm.searchRead(
"web.editor.class",
[],
["name", "class_name"]
);
});
},
async startWysiwyg(wysiwyg) {
// Provide the custom class css to the wysiwyg editor
// to render the custom class css in the toolbar
wysiwyg.options.custom_class_css = this.custom_class_css;
return this._super(wysiwyg);
},
});

View File

@@ -0,0 +1,68 @@
/** @odoo-module **/
import {_t} from "web.core";
import {patch} from "web.utils";
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";
patch(OdooEditor.prototype, "web_editor_class_selector.OdooEditor", {
_updateToolbar(show) {
const res = this._super(show);
if (!this.toolbar) {
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

@@ -0,0 +1,12 @@
/** @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

@@ -0,0 +1,37 @@
/** @odoo-module **/
import {
closestElement,
formatsSpecs,
} from "@web_editor/js/editor/odoo-editor/src/utils/utils";
// This function is called in the _configureToolbar method of the Wysiwyg class
// It generates the new formatsSpecs object with the custom CSS class
export function createCustomCssFormats(custom_class_css) {
const newformatsSpecs = {};
const class_names = custom_class_css.map((customCss) => customCss.class_name);
const removeCustomClass = (node) => {
for (const class_name of class_names) {
node.classList.remove(class_name);
if (node.parentElement) {
node.parentElement.classList.remove(class_name);
}
}
};
for (const customCss of custom_class_css) {
const className = customCss.class_name;
newformatsSpecs[className] = {
tagName: "span",
isFormatted: (node) => closestElement(node).classList.contains(className),
isTag: (node) =>
["SPAN"].includes(node.tagName) && node.classList.contains(className),
hasStyle: (node) => closestElement(node).classList.contains(className),
addStyle: (node) => {
removeCustomClass(node);
node.classList.add(className);
},
addNeutralStyle: (node) => removeCustomClass(node),
removeStyle: (node) => removeCustomClass(node),
};
}
Object.assign(formatsSpecs, newformatsSpecs);
}

View File

@@ -0,0 +1,25 @@
/** @odoo-module **/
import Wysiwyg from "web_editor.wysiwyg";
import core from "web.core";
import {createCustomCssFormats} from "../odoo-editor/utils.esm";
const Qweb = core.qweb;
Wysiwyg.include({
_configureToolbar: function (options) {
this._super(options);
if (options.custom_class_css && options.custom_class_css.length > 0) {
const $dialogContent = $(
Qweb.render("web_editor_class_selector.custom_class_css", {
custom_class_css: options.custom_class_css,
})
);
$dialogContent.appendTo(this.toolbar.$el);
// Binding the new commands to the editor
// to react to the click on the new options
this.odooEditor.bindExecCommand($dialogContent[0]);
this.odooEditor.custom_class_css = options.custom_class_css;
createCustomCssFormats(options.custom_class_css);
}
},
});

View File

@@ -0,0 +1,21 @@
.demo_menu {
font-weight: bold;
font-style: italic;
color: #714b67;
}
.demo_button {
border: 1px solid #71639e;
border-radius: 0.25rem;
padding: 0.25rem 0.7rem;
font-weight: bold;
color: #343a40;
background-color: #dee2e6;
border-color: #dee2e6 !important;
}
.demo_field {
border-top: 1px solid grey;
border-bottom: 1px solid grey;
font-weight: bold;
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="web_editor_class_selector.custom_class_css">
<t t-jquery="#decoration" t-operation="before">
<div id="custom_class" class="btn-group dropdown">
<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="custom_class_css" t-as="line">
<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>
</t>
</t>
</templates>