Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

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

@@ -0,0 +1,29 @@
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";
const {onWillStart} = owl;
patch(HtmlField.prototype, {
setup() {
super.setup(...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"]
);
});
},
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

@@ -0,0 +1,34 @@
import {closestElement} from "@html_editor/utils/dom_traversal";
import {formatsSpecs} from "@html_editor/utils/formatting";
// 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 = {};
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,16 @@
import {Wysiwyg} from "@html_editor/wysiwyg";
import {createCustomCssFormats} from "../utils/utils.esm";
import {patch} from "@web/core/utils/patch";
patch(Wysiwyg.prototype, {
getEditorConfig() {
const res = super.getEditorConfig(...arguments);
if (
this.props.config.custom_class_css &&
this.props.config.custom_class_css.length > 0
) {
createCustomCssFormats(this.props.config.custom_class_css);
}
return res;
},
});

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;
}