Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
24
web_editor_class_selector/static/src/js/css_selector/css_selector.esm.js
Executable file
24
web_editor_class_selector/static/src/js/css_selector/css_selector.esm.js
Executable 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);
|
||||
}
|
||||
}
|
||||
20
web_editor_class_selector/static/src/js/css_selector/css_selector.xml
Executable file
20
web_editor_class_selector/static/src/js/css_selector/css_selector.xml
Executable 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
29
web_editor_class_selector/static/src/js/fields/html_field.esm.js
Executable file
29
web_editor_class_selector/static/src/js/fields/html_field.esm.js
Executable 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;
|
||||
},
|
||||
});
|
||||
34
web_editor_class_selector/static/src/js/utils/utils.esm.js
Executable file
34
web_editor_class_selector/static/src/js/utils/utils.esm.js
Executable 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);
|
||||
}
|
||||
16
web_editor_class_selector/static/src/js/wysiwyg/wysiwyg.esm.js
Executable file
16
web_editor_class_selector/static/src/js/wysiwyg/wysiwyg.esm.js
Executable 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;
|
||||
},
|
||||
});
|
||||
21
web_editor_class_selector/static/src/scss/demo_styles.scss
Executable file
21
web_editor_class_selector/static/src/scss/demo_styles.scss
Executable 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;
|
||||
}
|
||||
Reference in New Issue
Block a user