diff --git a/addons/html_builder/static/src/builder_components/builder_colorpicker.js b/addons/html_builder/static/src/builder_components/builder_colorpicker.js
index 7652d3c164af0..61f124b2fbddb 100644
--- a/addons/html_builder/static/src/builder_components/builder_colorpicker.js
+++ b/addons/html_builder/static/src/builder_components/builder_colorpicker.js
@@ -13,6 +13,7 @@ export class BuilderColorPicker extends Component {
static props = {
...basicContainerBuilderComponentProps,
unit: { type: String, optional: true },
+ title: { type: String, optional: true },
};
static components = {
ColorSelector,
diff --git a/addons/html_builder/static/src/builder_components/builder_colorpicker.xml b/addons/html_builder/static/src/builder_components/builder_colorpicker.xml
index cf0e3eceff4ae..abe4a0a421b90 100644
--- a/addons/html_builder/static/src/builder_components/builder_colorpicker.xml
+++ b/addons/html_builder/static/src/builder_components/builder_colorpicker.xml
@@ -3,7 +3,7 @@
-
+
{} } = {}) {
export function useClickableBuilderComponent() {
useBuilderComponent();
const comp = useComponent();
- const { getAllActions, callOperation } = getAllActionsAndOperations(comp);
+ const { getAllActions, callOperation, isApplied } = getAllActionsAndOperations(comp);
const getAction = comp.env.editor.shared.builderActions.getAction;
const applyOperation = comp.env.editor.shared.history.makePreviewableOperation(callApply);
const shouldToggle = !comp.env.actionBus;
@@ -255,7 +255,7 @@ export function useClickableBuilderComponent() {
function callApply(applySpecs) {
comp.env.selectableContext?.cleanSelectedItem(applySpecs);
const cleans = comp.props.inheritedActions
- ?.map((actionId) => comp.env.dependencyManager.get(actionId).cleanSelectedItem)
+ ?.map((actionId) => comp.env.dependencyManager.get(actionId)?.cleanSelectedItem)
.filter(Boolean);
for (const clean of new Set(cleans)) {
clean(applySpecs);
@@ -281,33 +281,6 @@ export function useClickableBuilderComponent() {
}
}
}
- function isApplied() {
- const editingElements = comp.env.getEditingElements();
- if (!editingElements.length) {
- return;
- }
- const areActionsActiveTabs = getAllActions().map((o) => {
- const { actionId, actionParam, actionValue } = o;
- // TODO isApplied === first editing el or all ?
- const editingElement = editingElements[0];
- const isApplied = getAction(actionId).isApplied?.({
- editingElement,
- param: actionParam,
- value: actionValue,
- });
- return comp.props.inverseAction ? !isApplied : isApplied;
- });
- // If there is no `isApplied` method for the widget return false
- if (areActionsActiveTabs.every((el) => el === undefined)) {
- return false;
- }
- // If `isApplied` is explicitly false for an action return false
- if (areActionsActiveTabs.some((el) => el === false)) {
- return false;
- }
- // `isApplied` is true for at least one action
- return true;
- }
function getPriority() {
return (
getAllActions()
@@ -546,6 +519,11 @@ export function getAllActionsAndOperations(comp) {
if (!applySpec.load) {
return;
}
+ const shouldToggle = !comp.env.actionBus;
+ if (shouldToggle && isApplied()) {
+ // The element will be cleaned, do not load
+ return;
+ }
const result = await applySpec.load({
editingElement: applySpec.editingElement,
param: applySpec.actionParam,
@@ -558,8 +536,37 @@ export function getAllActionsAndOperations(comp) {
}
);
}
+ function isApplied() {
+ const getAction = comp.env.editor.shared.builderActions.getAction;
+ const editingElements = comp.env.getEditingElements();
+ if (!editingElements.length) {
+ return;
+ }
+ const areActionsActiveTabs = getAllActions().map((o) => {
+ const { actionId, actionParam, actionValue } = o;
+ // TODO isApplied === first editing el or all ?
+ const editingElement = editingElements[0];
+ const isApplied = getAction(actionId).isApplied?.({
+ editingElement,
+ param: actionParam,
+ value: actionValue,
+ });
+ return comp.props.inverseAction ? !isApplied : isApplied;
+ });
+ // If there is no `isApplied` method for the widget return false
+ if (areActionsActiveTabs.every((el) => el === undefined)) {
+ return false;
+ }
+ // If `isApplied` is explicitly false for an action return false
+ if (areActionsActiveTabs.some((el) => el === false)) {
+ return false;
+ }
+ // `isApplied` is true for at least one action
+ return true;
+ }
return {
getAllActions: getAllActions,
callOperation: callOperation,
+ isApplied: isApplied,
};
}
diff --git a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js
index 7276b0d1d2af3..fc26749a7e9cb 100644
--- a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js
+++ b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.js
@@ -173,8 +173,8 @@ export class BuilderSidebar extends Component {
// Ensure that the iframe is loaded and the editor is created before
// instantiating the sub components that potentially need the
// editor.
- const iframeEl = await this.props.iframeLoaded;
- this.editor.attachTo(iframeEl.contentDocument.body.querySelector("#wrapwrap"));
+ this.iframeEl = await this.props.iframeLoaded;
+ this.editor.attachTo(this.iframeEl.contentDocument.body.querySelector("#wrapwrap"));
});
useSubEnv({
diff --git a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss
index f4942847b1935..57e0ce4d18b51 100644
--- a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss
+++ b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.scss
@@ -100,3 +100,65 @@
border-width: 4px;
border-bottom: none !important;
}
+
+.o_pager_container {
+ overflow-y: scroll;
+ scroll-behavior: smooth;
+}
+
+.builder_select_page {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: $o-we-item-spacing / 2;
+ padding: $o-we-item-spacing;
+ background-color: $o-we-bg-lighter;
+
+ button {
+ --PreviewAlphaBg-background-size: 16px;
+
+ @extend %o-preview-alpha-background;
+ padding: $o-we-item-spacing;
+ background-color: transparent;
+ }
+ // For background shapes
+ .button_shape {
+ grid-column: span 2;
+ padding: 0;
+
+ button, div {
+ width: 100%;
+ height: 50px;
+ }
+ }
+ // button.active {
+ // background-color: rgba($o-we-toolbar-color-accent, 0.2);
+
+ // .o_we_shape {
+ // outline: 4px solid $o-we-toolbar-color-accent;
+ // outline-offset: -2px;
+ // }
+ // }
+ img {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: contain;
+ }
+}
+
+.o_we_shape_animated_label {
+ @include o-position-absolute(0, 0);
+ padding: 0 4px;
+ background: $o-we-toolbar-color-accent;
+ color: white;
+
+ > span {
+ @include o-text-overflow(inline-block);
+ max-width: 0;
+ }
+}
+
+.o_pager_nav_angle {
+ @include button-variant($o-we-bg-light, $o-we-bg-light);
+ padding: $o-we-item-spacing / 2;
+ font-size: $o-we-sidebar-font-size * 1.4;
+}
\ No newline at end of file
diff --git a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml
index fa0943f8ea9c7..eb5fbe4e005ec 100644
--- a/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml
+++ b/addons/html_builder/static/src/builder_sidebar/builder_sidebar.xml
@@ -30,7 +30,7 @@
-
+
theme
diff --git a/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js b/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js
index f47f5a7930805..113601d991854 100644
--- a/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js
+++ b/addons/html_builder/static/src/builder_sidebar/tabs/customize_tab.js
@@ -1,4 +1,4 @@
-import { Component, useState } from "@odoo/owl";
+import { Component, useState, useSubEnv } from "@odoo/owl";
import { OptionsContainer } from "../../components/option_container";
import { useVisibilityObserver } from "../../builder_components/utils";
@@ -8,6 +8,7 @@ export class CustomizeTab extends Component {
static props = {
currentOptionsContainers: { type: Array, optional: true },
snippetModel: { type: Object },
+ iframeEl: { type: HTMLElement },
};
static defaultProps = {
currentOptionsContainers: [],
@@ -20,5 +21,8 @@ export class CustomizeTab extends Component {
useVisibilityObserver("content", (hasContent) => {
this.state.hasContent = hasContent;
});
+ useSubEnv({
+ iframeEl: this.props.iframeEl,
+ });
}
}
diff --git a/addons/html_builder/static/src/components/background_shape_component.js b/addons/html_builder/static/src/components/background_shape_component.js
new file mode 100644
index 0000000000000..ce2ab4644f40b
--- /dev/null
+++ b/addons/html_builder/static/src/components/background_shape_component.js
@@ -0,0 +1,181 @@
+import { defaultBuilderComponents } from "@html_builder/builder_components/default_builder_components";
+import { Component, useRef } from "@odoo/owl";
+
+const connectionShapes = [
+ { shape: "web_editor/Connections/01", label: "Connections 01" },
+ { shape: "web_editor/Connections/02", label: "Connections 02" },
+ { shape: "web_editor/Connections/03", label: "Connections 03" },
+ { shape: "web_editor/Connections/04", label: "Connections 04" },
+ { shape: "web_editor/Connections/05", label: "Connections 05" },
+ { shape: "web_editor/Connections/06", label: "Connections 06" },
+ { shape: "web_editor/Connections/07", label: "Connections 07" },
+ { shape: "web_editor/Connections/08", label: "Connections 08" },
+ { shape: "web_editor/Connections/09", label: "Connections 09" },
+ { shape: "web_editor/Connections/10", label: "Connections 10" },
+ { shape: "web_editor/Connections/11", label: "Connections 11" },
+ { shape: "web_editor/Connections/12", label: "Connections 12" },
+ { shape: "web_editor/Connections/13", label: "Connections 13" },
+ { shape: "web_editor/Connections/14", label: "Connections 14" },
+ { shape: "web_editor/Connections/15", label: "Connections 15" },
+ { shape: "web_editor/Connections/16", label: "Connections 16" },
+ { shape: "web_editor/Connections/17", label: "Connections 17" },
+ { shape: "web_editor/Connections/18", label: "Connections 18" },
+ { shape: "web_editor/Connections/19", label: "Connections 19" },
+ { shape: "web_editor/Connections/20", label: "Connections 20" },
+];
+const originShapes = [
+ { shape: "web_editor/Origins/02_001", label: "Origins 01" },
+ { shape: "web_editor/Origins/05", label: "Origins 02" },
+ { shape: "web_editor/Origins/06_001", label: "Origins 03" },
+ { shape: "web_editor/Origins/07_002", label: "Origins 04" },
+ { shape: "web_editor/Origins/09_001", label: "Origins 05" },
+ { shape: "web_editor/Origins/16", label: "Origins 06", animated: true },
+ { shape: "web_editor/Origins/17", label: "Origins 07", animated: true },
+ { shape: "web_editor/Origins/19", label: "Origins 08" },
+];
+const boldShapes = [
+ { shape: "web_editor/Bold/01", label: "Bold 01", animated: true },
+ { shape: "web_editor/Bold/03", label: "Bold 02" },
+ { shape: "web_editor/Bold/04", label: "Bold 03" },
+ { shape: "web_editor/Bold/05_001", label: "Bold 04" },
+ { shape: "web_editor/Bold/06_001", label: "Bold 05" },
+ { shape: "web_editor/Bold/07_001", label: "Bold 06" },
+ { shape: "web_editor/Bold/08", label: "Bold 07" },
+ { shape: "web_editor/Bold/09", label: "Bold 08" },
+ { shape: "web_editor/Bold/10_001", label: "Bold 09" },
+ { shape: "web_editor/Bold/02_001", label: "Bold 10" },
+];
+const blobShapes = [
+ { shape: "web_editor/Blobs/01_001", label: "Blobs 01" },
+ { shape: "web_editor/Blobs/02", label: "Blobs 02" },
+ { shape: "web_editor/Blobs/03", label: "Blobs 03" },
+ { shape: "web_editor/Blobs/04", label: "Blobs 04" },
+ { shape: "web_editor/Blobs/05", label: "Blobs 05" },
+ { shape: "web_editor/Blobs/06", label: "Blobs 06" },
+ { shape: "web_editor/Blobs/07", label: "Blobs 07" },
+ { shape: "web_editor/Blobs/08", label: "Blobs 08" },
+ { shape: "web_editor/Blobs/09", label: "Blobs 09" },
+ { shape: "web_editor/Blobs/10_001", label: "Blobs 10" },
+ { shape: "web_editor/Blobs/11", label: "Blobs 11" },
+ { shape: "web_editor/Blobs/12", label: "Blobs 12" },
+];
+const airyAndZigShapes = [
+ { shape: "web_editor/Airy/01", label: "Airy 01" },
+ { shape: "web_editor/Airy/06", label: "Airy 02" },
+ { shape: "web_editor/Airy/02", label: "Airy 03" },
+ { shape: "web_editor/Airy/07", label: "Airy 04" },
+ { shape: "web_editor/Airy/08", label: "Airy 05" },
+ { shape: "web_editor/Airy/10", label: "Airy 06" },
+ { shape: "web_editor/Airy/09", label: "Airy 07" },
+ { shape: "web_editor/Airy/11", label: "Airy 08" },
+ { shape: "web_editor/Airy/03_001", label: "Airy 09", animated: true },
+ { shape: "web_editor/Airy/04_001", label: "Airy 10", animated: true },
+ { shape: "web_editor/Airy/05_001", label: "Airy 11", animated: true },
+ { shape: "web_editor/Airy/12_001", label: "Airy 12", animated: true },
+ { shape: "web_editor/Airy/13_001", label: "Airy 13", animated: true },
+ { shape: "web_editor/Airy/14", label: "Airy 14" },
+ { shape: "web_editor/Zigs/01_001", label: "Zigs 01", animated: true },
+ { shape: "web_editor/Zigs/02_001", label: "Zigs 02", animated: true },
+ { shape: "web_editor/Zigs/03", label: "Zigs 03" },
+ { shape: "web_editor/Zigs/04", label: "Zigs 04" },
+];
+const wavyShapes = [
+ { shape: "web_editor/Wavy/03", label: "Wavy 01" },
+ { shape: "web_editor/Wavy/10", label: "Wavy 02" },
+ { shape: "web_editor/Wavy/24", label: "Wavy 03", animated: true },
+ { shape: "web_editor/Wavy/26", label: "Wavy 04", animated: true },
+ { shape: "web_editor/Wavy/27", label: "Wavy 05", animated: true },
+ { shape: "web_editor/Wavy/04", label: "Wavy 06" },
+ { shape: "web_editor/Wavy/06_001", label: "Wavy 07" },
+ { shape: "web_editor/Wavy/07", label: "Wavy 08" },
+ { shape: "web_editor/Wavy/08", label: "Wavy 09" },
+ { shape: "web_editor/Wavy/09", label: "Wavy 10" },
+ { shape: "web_editor/Wavy/11", label: "Wavy 11" },
+ { shape: "web_editor/Wavy/28", label: "Wavy 12", animated: true },
+ { shape: "web_editor/Wavy/16", label: "Wavy 13" },
+ { shape: "web_editor/Wavy/17", label: "Wavy 14" },
+ { shape: "web_editor/Wavy/18", label: "Wavy 15" },
+ { shape: "web_editor/Wavy/19", label: "Wavy 16" },
+ { shape: "web_editor/Wavy/22", label: "Wavy 17" },
+ { shape: "web_editor/Wavy/23", label: "Wavy 18" },
+];
+const blockAndRainyShapes = [
+ { shape: "web_editor/Blocks/02_001", label: "Blocks 01" },
+ { shape: "web_editor/Rainy/01_001", label: "Rainy 01", animated: true },
+ { shape: "web_editor/Blocks/01_001", label: "Blocks 02" },
+ { shape: "web_editor/Rainy/02_001", label: "Rainy 02", animated: true },
+ { shape: "web_editor/Rainy/06", label: "Rainy 03" },
+ { shape: "web_editor/Blocks/04", label: "Blocks 04" },
+ { shape: "web_editor/Rainy/07", label: "Rainy 04" },
+ { shape: "web_editor/Rainy/10", label: "Rainy 05", animated: true },
+ { shape: "web_editor/Rainy/08_001", label: "Rainy 06", animated: true },
+ { shape: "web_editor/Rainy/09_001", label: "Rainy 07" },
+];
+const floatingShapes = [
+ { shape: "web_editor/Floats/01", label: "Float 01", animated: true },
+ { shape: "web_editor/Floats/02", label: "Float 02", animated: true },
+ { shape: "web_editor/Floats/03", label: "Float 03", animated: true },
+ { shape: "web_editor/Floats/04", label: "Float 04", animated: true },
+ { shape: "web_editor/Floats/05", label: "Float 05", animated: true },
+ { shape: "web_editor/Floats/06", label: "Float 06", animated: true },
+ { shape: "web_editor/Floats/07", label: "Float 07", animated: true },
+ { shape: "web_editor/Floats/08", label: "Float 08", animated: true },
+ { shape: "web_editor/Floats/09", label: "Float 09", animated: true },
+ { shape: "web_editor/Floats/10", label: "Float 10", animated: true },
+ { shape: "web_editor/Floats/11", label: "Float 11", animated: true },
+ { shape: "web_editor/Floats/12", label: "Float 12", animated: true },
+ { shape: "web_editor/Floats/13", label: "Float 13", animated: true },
+ { shape: "web_editor/Floats/14", label: "Float 14", animated: true },
+];
+
+export class BackgroundShapeComponent extends Component {
+ static template = "html_builder.BackgroundShapeComponent";
+ static components = {
+ ...defaultBuilderComponents,
+ };
+ static props = {};
+
+ setup() {
+ this.basicEl = useRef("basic");
+ this.linearEl = useRef("linear");
+ this.creativeEl = useRef("creative");
+ this.shapeBackgroundImagePerClass = {};
+ for (const styleSheet of this.env.iframeEl.contentWindow.document.styleSheets) {
+ if (styleSheet.href && new URL(styleSheet.href).host !== location.host) {
+ // In some browsers, if a stylesheet is loaded from a different domain
+ // accessing cssRules results in a SecurityError.
+ continue;
+ }
+ for (const rule of [...styleSheet.cssRules]) {
+ if (rule.selectorText && rule.selectorText.startsWith(".o_we_shape.")) {
+ this.shapeBackgroundImagePerClass[rule.selectorText] =
+ rule.style.backgroundImage;
+ }
+ }
+ }
+ this.connectionShapes = connectionShapes;
+ this.originShapes = originShapes;
+ this.boldShapes = boldShapes;
+ this.blobShapes = blobShapes;
+ this.airyAndZigShapes = airyAndZigShapes;
+ this.wavyShapes = wavyShapes;
+ this.blockAndRainyShapes = blockAndRainyShapes;
+ this.floatingShapes = floatingShapes;
+ }
+ getShapeUrl(shapeName) {
+ const shapeClassName = `o_${shapeName.replace(/\//g, "_")}`;
+ // Match current palette.
+ return this.shapeBackgroundImagePerClass[`.o_we_shape.${shapeClassName}`];
+ }
+ scrollToShapes(sectionName) {
+ const sectionElementMap = {
+ basic: this.basicEl.el,
+ linear: this.linearEl.el,
+ creative: this.creativeEl.el,
+ };
+ sectionElementMap[sectionName].scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ hideComponent() {
+ this.env.hideComponent();
+ }
+}
diff --git a/addons/html_builder/static/src/components/background_shape_component.xml b/addons/html_builder/static/src/components/background_shape_component.xml
new file mode 100644
index 0000000000000..11b40e3062b91
--- /dev/null
+++ b/addons/html_builder/static/src/components/background_shape_component.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/html_builder/static/src/components/global_sidebar_component.js b/addons/html_builder/static/src/components/global_sidebar_component.js
new file mode 100644
index 0000000000000..1537c2cd1acd2
--- /dev/null
+++ b/addons/html_builder/static/src/components/global_sidebar_component.js
@@ -0,0 +1,5 @@
+import { BackgroundShapeComponent } from "./background_shape_component";
+
+export const globalSidebarComponent = {
+ BackgroundShapeComponent,
+};
diff --git a/addons/html_builder/static/src/components/option_container.js b/addons/html_builder/static/src/components/option_container.js
index 88fc262803e23..8449d2b8e0c77 100644
--- a/addons/html_builder/static/src/components/option_container.js
+++ b/addons/html_builder/static/src/components/option_container.js
@@ -1,4 +1,4 @@
-import { Component, useSubEnv, markup } from "@odoo/owl";
+import { Component, useState, useSubEnv, markup } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { defaultBuilderComponents } from "../builder_components/default_builder_components";
@@ -6,10 +6,15 @@ import { globalBuilderOptions } from "../builder_components/global_builder_optio
import { useVisibilityObserver, useApplyVisibility } from "../builder_components/utils";
import { DependencyManager } from "../plugins/dependency_manager";
import { getSnippetName } from "@html_builder/utils/utils";
+import { globalSidebarComponent } from "@html_builder/components/global_sidebar_component";
export class OptionsContainer extends Component {
static template = "html_builder.OptionsContainer";
- static components = { ...defaultBuilderComponents, ...globalBuilderOptions };
+ static components = {
+ ...defaultBuilderComponents,
+ ...globalBuilderOptions,
+ ...globalSidebarComponent,
+ };
static props = {
snippetModel: { type: Object },
options: { type: Array },
@@ -21,11 +26,17 @@ export class OptionsContainer extends Component {
setup() {
this.notification = useService("notification");
+ this.state = useState({
+ componentToShow: undefined,
+ });
+
useSubEnv({
dependencyManager: new DependencyManager(),
getEditingElement: () => this.props.editingElement,
getEditingElements: () => [this.props.editingElement],
weContext: {},
+ showComponent: this.showComponent.bind(this),
+ hideComponent: this.hideComponent.bind(this),
});
useVisibilityObserver("content", useApplyVisibility("root"));
}
@@ -47,6 +58,14 @@ export class OptionsContainer extends Component {
this.env.editor.shared["builder-options"].updateContainers(this.props.editingElement);
}
+ showComponent(componentName) {
+ this.state.componentToShow = componentName;
+ }
+
+ hideComponent() {
+ this.state.componentToShow = undefined;
+ }
+
toggleOverlayPreview(el, show) {
if (show) {
this.env.editor.shared.overlayButtons.hideOverlayButtons();
diff --git a/addons/html_builder/static/src/components/option_container.xml b/addons/html_builder/static/src/components/option_container.xml
index 3d5924d9f49f6..47f0d60cb7b0a 100644
--- a/addons/html_builder/static/src/components/option_container.xml
+++ b/addons/html_builder/static/src/components/option_container.xml
@@ -33,7 +33,8 @@
-
+
+
diff --git a/addons/html_builder/static/src/img/options/bg_shape.svg b/addons/html_builder/static/src/img/options/bg_shape.svg
new file mode 100644
index 0000000000000..838ddc5320334
--- /dev/null
+++ b/addons/html_builder/static/src/img/options/bg_shape.svg
@@ -0,0 +1,11 @@
+
diff --git a/addons/html_builder/static/src/options/background_component.js b/addons/html_builder/static/src/options/background_component.js
new file mode 100644
index 0000000000000..cc8411fe862cd
--- /dev/null
+++ b/addons/html_builder/static/src/options/background_component.js
@@ -0,0 +1,32 @@
+import { Component } from "@odoo/owl";
+import { defaultBuilderComponents } from "../builder_components/default_builder_components";
+import { getBgImageURLFromEl } from "@html_builder/utils/utils_css";
+import { BackgroundShapeComponent } from "@html_builder/components/background_shape_component";
+
+export class BackgroundComponent extends Component {
+ static template = "html_builder.BackgroundComponent";
+ static components = { ...defaultBuilderComponents };
+ static props = {
+ withColors: { type: Boolean },
+ withImages: { type: Boolean },
+ withColorCombinations: { type: Boolean },
+ withGradient: { type: Boolean },
+ withShapes: { type: Boolean, optional: true },
+ };
+ static defaultProps = {
+ withShapes: false,
+ };
+ showWebShapeColorpicker() {
+ // TODO: double check the getBgImageURLFromEl(editingEl)
+ const editingEl = this.env.getEditingElement();
+ const src = new URL(getBgImageURLFromEl(editingEl), window.location.origin);
+ return (
+ src.origin === window.location.origin &&
+ (src.pathname.startsWith("/html_editor/shape/") ||
+ src.pathname.startsWith("/web_editor/shape/"))
+ );
+ }
+ onClick() {
+ this.env.showComponent(BackgroundShapeComponent);
+ }
+}
diff --git a/addons/html_builder/static/src/options/background_component.xml b/addons/html_builder/static/src/options/background_component.xml
new file mode 100644
index 0000000000000..f4140d8c9726c
--- /dev/null
+++ b/addons/html_builder/static/src/options/background_component.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
+
+
+
+
+
+
+ Cover
+ Repeat pattern
+
+
+
+
+
+
+
diff --git a/addons/html_builder/static/src/options/background_option.js b/addons/html_builder/static/src/options/background_option.js
new file mode 100644
index 0000000000000..7fc7762b07eed
--- /dev/null
+++ b/addons/html_builder/static/src/options/background_option.js
@@ -0,0 +1,396 @@
+import { Plugin } from "@html_editor/plugin";
+import { registry } from "@web/core/registry";
+import { pick } from "@web/core/utils/objects";
+import { removeOnImageChangeAttrs } from "@html_editor/utils/image_processing";
+import {
+ backgroundImageCssToParts,
+ backgroundImagePartsToCss,
+ getBgImageURLFromEl,
+ getBgImageURLFromURL,
+} from "@html_builder/utils/utils_css";
+import { coreBuilderActions } from "../core_builder_action_plugin";
+// import { isMobileView } from "@html_builder/builder/utils/utils";
+
+// const connectionShapes = [
+// { shape: "web_editor/Connections/01", label: "Connections 01" },
+// { shape: "web_editor/Connections/02", label: "Connections 02" },
+// { shape: "web_editor/Connections/03", label: "Connections 03" },
+// { shape: "web_editor/Connections/04", label: "Connections 04" },
+// { shape: "web_editor/Connections/05", label: "Connections 05" },
+// { shape: "web_editor/Connections/06", label: "Connections 06" },
+// ];
+
+const replaceImage = {
+ load: function () {
+ return new Promise((resolve) => {
+ this.dependencies.media.openMediaDialog({
+ onlyImages: true,
+ noDocuments: true,
+ noVideos: true,
+ save: (imageEl) => {
+ resolve(imageEl.getAttribute("src"));
+ },
+ });
+ });
+ },
+ apply: ({ editingElement, loadResult: imageSrc }) => {
+ // What was in background method of BackgroundImage option
+ setBackground(editingElement, imageSrc);
+ for (const attr of removeOnImageChangeAttrs) {
+ delete editingElement.dataset[attr];
+ }
+ // TODO: handle the _onBackgroundChanged of BackgroundOptimize (previoulsy a trigger check if inherit or something)
+ },
+};
+
+class BackgroundOptionPlugin extends Plugin {
+ static id = "BackgroundOption";
+ static dependencies = ["media"];
+ resources = {
+ builder_actions: this.getActions(),
+ };
+ getActions() {
+ return {
+ applyShape: {
+ apply: ({ editingElement, param }) => {
+ const shapeData = this.getShapeData(editingElement);
+ const applyShapeParams = {
+ shape: param.shape,
+ colors: this.getImplicitColors(editingElement, shapeData.colors, param.url),
+ flip: [],
+ animated: param.animated,
+ shapeAnimationSpeed: shapeData.shapeAnimationSpeed,
+ };
+ this.applyShape(editingElement, () => applyShapeParams);
+ },
+ isApplied: ({ editingElement, param }) => {
+ const currentShapeApplied = this.getShapeData(editingElement).shape;
+ return currentShapeApplied === param.shape;
+ },
+ },
+ replaceImage: {
+ load: replaceImage.load.bind(this),
+ apply: replaceImage.apply.bind(this),
+ },
+ toggleBgImage: {
+ load: replaceImage.load.bind(this),
+ apply: replaceImage.apply.bind(this),
+ clean: ({ editingElement }) => {
+ editingElement.querySelector(".o_we_bg_filter")?.remove();
+ // TODO: use setWidgetValue instead of calling background directly when possible
+ replaceImage.apply.bind(this)({
+ editingElement: editingElement,
+ loadResult: "",
+ });
+ },
+ isApplied: ({ editingElement }) => !!editingElement.style["background-image"],
+ },
+ // toggleBgShape todo
+ toggleBgShape: {
+ apply: ({ editingElement }) => {
+ //TODO
+ },
+ },
+ toggleShape: {
+ apply: ({ editingElement }) => {
+ // // TODO: continue
+ // const previousSibling = editingElement.previousElementSibling;
+ // const possibleShapes = connectionShapes.map(
+ // (connectionShape) => connectionShape.key
+ // );
+ // let shapeToSelect;
+ // if (previousSibling) {
+ // const previousShape = this.getShapeData(previousSibling).shape;
+ // shapeToSelect = possibleShapes.find(
+ // (shape, i) => possibleShapes[i - 1] === previousShape
+ // );
+ // }
+ // // If there is no previous sibling, if the previous sibling had the
+ // // last shape selected or if the previous shape could not be found
+ // // in the possible shapes, default to the first shape. ([0] being no
+ // // shapes selected.)
+ // if (!shapeToSelect) {
+ // shapeToSelect = possibleShapes[1];
+ // }
+ // // Only show on mobile by default if toggled from mobile view
+ // const showOnMobile = isMobileView(editingElement);
+ // this.trigger_up("snippet_edition_request", {exec: () => {
+ // // options for shape will only be available after _toggleShape() returned
+ // this._requestUserValueWidgets('bg_shape_opt')[0].enable();
+ // }});
+ // this.createShapeContainer(shapeToSelect);
+ // return this.applyShape(editingElement, () => ({
+ // shape: shapeToSelect,
+ // colors: this._getImplicitColors(shapeToSelect),
+ // showOnMobile,
+ // }));
+ },
+ clean: ({ editingElement }) => {
+ this.applyShape(editingElement, () => ({ shape: "" }));
+ },
+ },
+ backgroundType: {
+ apply: ({ editingElement }) => {
+ // TODO
+ },
+ },
+ backgroundPositionOverlay: {
+ apply: ({ editingElement }) => {
+ // TODO
+ },
+ },
+ };
+ }
+ /**
+ * Creates and inserts a container for the shape with the right classes.
+ *
+ * @param {HTMLElement} editingElement
+ * @param {string} shape the shape name for which to create a container
+ */
+ createShapeContainer(editingElement, shape) {
+ const shapeContainer = this.insertShapeContainer(
+ editingElement,
+ document.createElement("div")
+ );
+ editingElement.style.setProperty("position", "relative");
+ shapeContainer.className = `o_we_shape o_${shape.replace(/\//g, "_")}`;
+ return shapeContainer;
+ }
+
+ /**
+ * Inserts or removes the given container at the right position in the
+ * document.
+ *
+ * @param {HTMLElement} editingElement
+ * @param {HTMLElement} [newContainer] container to insert, null to remove
+ */
+ insertShapeContainer(editingElement, newContainer) {
+ const shapeContainerEl = editingElement.querySelector(":scope > .o_we_shape");
+ if (shapeContainerEl) {
+ shapeContainerEl.remove();
+ }
+ if (newContainer) {
+ const preShapeLayerElement = this.getLastPreShapeLayerElement(editingElement);
+ if (preShapeLayerElement) {
+ preShapeLayerElement.insertAdjacentElement("afterend", newContainer);
+ } else {
+ editingElement.prepend(newContainer);
+ }
+ }
+ return newContainer;
+ }
+
+ getLastPreShapeLayerElement(editingElement) {
+ return editingElement.querySelector(":scope > .o_we_bg_filter");
+ }
+
+ /**
+ * Handles everything related to saving state before preview and restoring
+ * it after a preview or locking in the changes when not in preview.
+ *
+ * @param {HTMLElement} editingElement
+ * @param {function} computeShapeData function to compute the new shape data.
+ */
+ applyShape(editingElement, computeShapeData) {
+ const curShapeData = editingElement.dataset.oeShapeData || {};
+ const newShapeData = computeShapeData();
+ // TODO: check if really work as curShapeData seems to be of form {"shape": blabla}
+ const { shape: curShape } = curShapeData;
+ const changedShape = newShapeData.shape !== curShape;
+ this.markShape(editingElement, newShapeData);
+ if (changedShape) {
+ // Need to rerender for correct number of colorpickers
+ // TODO check how to do
+ // this.rerender = true;
+ }
+
+ // Updates/removes the shape container as needed and gives it the
+ // correct background shape
+ const json = editingElement.dataset.oeShapeData;
+ const {
+ shape,
+ colors,
+ flip = [],
+ animated = "false",
+ showOnMobile,
+ shapeAnimationSpeed,
+ } = json ? JSON.parse(json) : {};
+ let shapeContainerEl = editingElement.querySelector(":scope > .o_we_shape");
+ if (!shape) {
+ return this.insertShapeContainer(editingElement, null);
+ }
+ // When changing shape we want to reset the shape container (for transparency color)
+ if (changedShape) {
+ shapeContainerEl = this.createShapeContainer(editingElement, shape);
+ }
+ // Compat: remove old flip classes as flipping is now done inside the svg
+ shapeContainerEl.classList.remove("o_we_flip_x", "o_we_flip_y");
+
+ shapeContainerEl.classList.toggle("o_we_animated", animated === "true");
+ if (colors || flip.length || parseFloat(shapeAnimationSpeed) !== 0) {
+ // Custom colors/flip/speed, overwrite shape that is set by the class
+ shapeContainerEl.style.setProperty(
+ "background-image",
+ `url("${this.getShapeSrc(editingElement)}")`
+ );
+ shapeContainerEl.style.backgroundPosition = "";
+ if (flip.length) {
+ let [xPos, yPos] = getComputedStyle(shapeContainerEl)
+ .backgroundPosition.split(" ")
+ .map((p) => parseFloat(p));
+ // -X + 2*Y is a symmetry of X around Y, this is a symmetry around 50%
+ xPos = flip.includes("x") ? -xPos + 100 : xPos;
+ yPos = flip.includes("y") ? -yPos + 100 : yPos;
+ shapeContainerEl.style.backgroundPosition = `${xPos}% ${yPos}%`;
+ }
+ } else {
+ // Remove custom bg image and let the shape class set the bg shape
+ shapeContainerEl.style.setProperty("background-image", "");
+ shapeContainerEl.style.setProperty("background-position", "");
+ }
+ shapeContainerEl.classList.toggle("o_shape_show_mobile", !!showOnMobile);
+ }
+ /**
+ * Overwrites shape properties with the specified data.
+ *
+ * @param {HTMLElement} editingElement
+ * @param {Object} newData an object with the new data
+ */
+ markShape(editingElement, newData) {
+ const defaultColors = this.getDefaultColors(editingElement);
+ const shapeData = Object.assign(this.getShapeData(editingElement), newData);
+ const areColorsDefault = Object.entries(shapeData.colors).every(
+ ([colorName, colorValue]) =>
+ defaultColors[colorName] &&
+ colorValue.toLowerCase() === defaultColors[colorName].toLowerCase()
+ );
+ if (areColorsDefault) {
+ delete shapeData.colors;
+ }
+ if (!shapeData.shape) {
+ delete editingElement.dataset.oeShapeData;
+ } else {
+ editingElement.dataset.oeShapeData = JSON.stringify(shapeData);
+ }
+ }
+ /**
+ * Returns the src of the shape corresponding to the current parameters.
+ *
+ * @param {HTMLElement} editingElement
+ */
+ getShapeSrc(editingElement) {
+ const { shape, colors, flip, shapeAnimationSpeed } = this.getShapeData(editingElement);
+ if (!shape) {
+ return "";
+ }
+ const searchParams = Object.entries(colors).map(([colorName, colorValue]) => {
+ const encodedCol = encodeURIComponent(colorValue);
+ return `${colorName}=${encodedCol}`;
+ });
+ if (flip.length) {
+ searchParams.push(`flip=${encodeURIComponent(flip.sort().join(""))}`);
+ }
+ if (Number(shapeAnimationSpeed)) {
+ searchParams.push(`shapeAnimationSpeed=${encodeURIComponent(shapeAnimationSpeed)}`);
+ }
+ return `/web_editor/shape/${encodeURIComponent(shape)}.svg?${searchParams.join("&")}`;
+ }
+ /**
+ * Returns the implicit colors for the currently selected shape.
+ *
+ * The implicit colors are use upon shape selection. They are computed as:
+ * - the default colors
+ * - patched with each set of colors of previous siblings shape
+ * - patched with the colors of the previously selected shape
+ * - filtered to only keep the colors involved in the current shape
+ *
+ * @param {HTMLElement} editingElement
+ * @param {String} shape identifier of the selected shape
+ * @param {Object} previousColors colors of the shape before its replacement
+ */
+ getImplicitColors(editingElement, previousColors, selectedBackgroundUrl) {
+ const defaultColors = this.getShapeDefaultColors(selectedBackgroundUrl);
+ let colors = previousColors || {};
+ let sibling = editingElement.previousElementSibling;
+ while (sibling) {
+ colors = Object.assign(this.getShapeData(sibling).colors || {}, colors);
+ sibling = sibling.previousElementSibling;
+ }
+ const defaultKeys = Object.keys(defaultColors);
+ colors = Object.assign(defaultColors, colors);
+ return pick(colors, ...defaultKeys);
+ }
+ /**
+ * Returns the default colors for the a shape in the selector.
+ *
+ * @param {String} selectedBackgroundUrl
+ */
+ getShapeDefaultColors(selectedBackgroundUrl) {
+ const shapeSrc = selectedBackgroundUrl && getBgImageURLFromURL(selectedBackgroundUrl);
+ const url = new URL(shapeSrc, window.location.origin);
+ return Object.fromEntries(url.searchParams.entries());
+ }
+ /**
+ * Returns the default colors for the currently selected shape.
+ *
+ * @param {HTMLElement} editingElement the element on which to read the shape
+ * data.
+ */
+ getDefaultColors(editingElement) {
+ const shapeContainer = editingElement.querySelector(":scope > .o_we_shape").cloneNode(true);
+ shapeContainer.classList.add("d-none");
+ // Needs to be in document for bg-image class to take effect
+ editingElement.ownerDocument.body.appendChild(shapeContainer);
+ shapeContainer.style.setProperty("background-image", "");
+ const shapeSrc = shapeContainer && getBgImageURLFromEl(shapeContainer);
+ shapeContainer.remove();
+ if (!shapeSrc) {
+ return {};
+ }
+ const url = new URL(shapeSrc, window.location.origin);
+ return Object.fromEntries(url.searchParams.entries());
+ }
+
+ /**
+ * Retrieves current shape data from the target's dataset.
+ *
+ * @param {HTMLElement} editingElement the target on which to read the shape
+ * data.
+ */
+ getShapeData(editingElement) {
+ const defaultData = {
+ shape: "",
+ colors: this.getDefaultColors(editingElement),
+ flip: [],
+ showOnMobile: false,
+ shapeAnimationSpeed: "0",
+ };
+ const json = editingElement.dataset.oeShapeData;
+ return json ? Object.assign(defaultData, JSON.parse(json.replace(/'/g, '"'))) : defaultData;
+ }
+}
+registry.category("website-plugins").add(BackgroundOptionPlugin.id, BackgroundOptionPlugin);
+
+function setBackground(editingElement, backgroundURL) {
+ const parts = backgroundImageCssToParts(editingElement.style["background-image"]);
+ if (backgroundURL) {
+ parts.url = `url('${backgroundURL}')`;
+ editingElement.classList.add("oe_img_bg", "o_bg_img_center");
+ } else {
+ delete parts.url;
+ editingElement.classList.remove("oe_img_bg", "o_bg_img_center", "o_modified_image_to_save");
+ }
+ const combined = backgroundImagePartsToCss(parts);
+ // TODO: check this comment
+ // We use selectStyle so that if when a background image is removed the
+ // remaining image matches the o_cc's gradient background, it can be
+ // removed too.
+ // -> styleAction
+
+ coreBuilderActions.styleAction.apply({
+ editingElement: editingElement,
+ param: "background-image",
+ value: combined,
+ });
+ // Check if really needed this.options.wysiwyg.odooEditor.editable.focus();
+}
diff --git a/addons/html_builder/static/src/options/image_gallery_option.js b/addons/html_builder/static/src/options/image_gallery_option.js
index 370172757cb75..1e12f27ddc3c6 100644
--- a/addons/html_builder/static/src/options/image_gallery_option.js
+++ b/addons/html_builder/static/src/options/image_gallery_option.js
@@ -29,6 +29,7 @@ class ImageGalleryOption extends Plugin {
}),
apply: ({ editingElement, loadResult: images }) => {
addImages(images, editingElement);
+ // TODO: check if really needed
this.dependencies.history.addStep();
},
},
diff --git a/addons/html_builder/static/src/options/process_steps_option.js b/addons/html_builder/static/src/options/process_steps_option.js
index 7916ebbd08755..8e103149af0ed 100644
--- a/addons/html_builder/static/src/options/process_steps_option.js
+++ b/addons/html_builder/static/src/options/process_steps_option.js
@@ -1,4 +1,5 @@
import { defaultBuilderComponents } from "../builder_components/default_builder_components";
+import { BackgroundComponent } from "./background_component";
import { coreBuilderActions } from "@html_builder/core_builder_action_plugin";
import { applyFunDependOnSelectorAndExclude } from "@html_builder/options/utils";
import { Plugin } from "@html_editor/plugin";
@@ -7,12 +8,25 @@ import { registry } from "@web/core/registry";
class ProcessStepsOptionPlugin extends Plugin {
static id = "ProcessStepsOption";
+ static dependencies = ["media"];
selector = ".s_process_steps";
resources = {
- builder_options: {
- OptionComponent: ProcessStepsOption,
- selector: this.selector,
- },
+ builder_options: [
+ {
+ OptionComponent: ProcessStepsOption,
+ selector: this.selector,
+ },
+ {
+ selector: ".s_process_step .s_process_step_number",
+ OptionComponent: BackgroundComponent,
+ props: {
+ withColors: true,
+ withImages: false,
+ withColorCombinations: false,
+ withGradient: true,
+ },
+ },
+ ],
builder_actions: this.getActions(),
content_updated_handlers: (rootEl) =>
applyFunDependOnSelectorAndExclude(reloadConnectors, rootEl, this.selector),
diff --git a/addons/html_builder/static/src/options/section_background_option.js b/addons/html_builder/static/src/options/section_background_option.js
index 602b2350933c5..50f496f467ae4 100644
--- a/addons/html_builder/static/src/options/section_background_option.js
+++ b/addons/html_builder/static/src/options/section_background_option.js
@@ -1,3 +1,4 @@
+import { BackgroundComponent } from "./background_component";
import { registry } from "@web/core/registry";
import { Plugin } from "@html_editor/plugin";
@@ -8,9 +9,21 @@ class SectionBackgroundOptionPlugin extends Plugin {
static id = "SectionBackgroundOption";
resources = {
builder_options: [
+ // {
+ // template: "html_builder.SectionBackgroundOption",
+ // selector: "section",
+ // },
{
- template: "html_builder.SectionBackgroundOption",
selector: "section",
+ OptionComponent: BackgroundComponent,
+ props: {
+ withColors: true,
+ withImages: true,
+ // todo: handle with_videos
+ withShapes: true,
+ withGradient: true,
+ withColorCombinations: true,
+ },
},
],
};
diff --git a/addons/html_builder/static/src/utils/utils_css.js b/addons/html_builder/static/src/utils/utils_css.js
index 25151a0f350c5..b2df41427da11 100644
--- a/addons/html_builder/static/src/utils/utils_css.js
+++ b/addons/html_builder/static/src/utils/utils_css.js
@@ -371,10 +371,19 @@ export function normalizeColor(color) {
* @param {string} string a css value in the form 'url("...")'
* @returns {string|false} the src of the image or false if not parsable
*/
-export function getBgImageURL(el) {
+export function getBgImageURLFromEl(el) {
const parts = backgroundImageCssToParts(window.getComputedStyle(el).backgroundImage);
const string = parts.url || "";
- const match = string.match(/^url\((['"])(.*?)\1\)$/);
+ return getBgImageURLFromURL(string);
+}
+/**
+ * Parse an element's background-image's url.
+ *
+ * @param {string} string a css value in the form 'url("...")'
+ * @returns {string|false} the src of the image or false if not parsable
+ */
+export function getBgImageURLFromURL(url) {
+ const match = url.match(/^url\((['"])(.*?)\1\)$/);
if (!match) {
return "";
}
diff --git a/addons/html_editor/static/src/utils/image_processing.js b/addons/html_editor/static/src/utils/image_processing.js
index 40f04ab202397..f4fc7e8d497db 100644
--- a/addons/html_editor/static/src/utils/image_processing.js
+++ b/addons/html_editor/static/src/utils/image_processing.js
@@ -6,6 +6,20 @@ import { getAffineApproximation, getProjective } from "./perspective_utils";
// initializing the cropper to reuse the previous crop.
export const cropperDataFields = ["x", "y", "width", "height", "rotate", "scaleX", "scaleY"];
export const isGif = (mimetype) => mimetype === "image/gif";
+const modifierFields = [
+ 'filter',
+ 'quality',
+ 'mimetype',
+ 'glFilter',
+ 'originalId',
+ 'originalSrc',
+ 'resizeWidth',
+ 'aspectRatio',
+ "bgSrc",
+ "mimetypeBeforeConversion",
+];
+
+export const removeOnImageChangeAttrs = [...cropperDataFields, ...modifierFields];
// webgl color filters
const _applyAll = (result, filter, filters) => {
diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml
index 6066afe4e7d1a..99fd5681e3be2 100644
--- a/addons/web_editor/views/snippets.xml
+++ b/addons/web_editor/views/snippets.xml
@@ -107,6 +107,7 @@
t-att-data-css-compatible="css_compatible and 'true' or None"/>
+