Skip to content

Commit 5440606

Browse files
Goamanloco-odoo
authored andcommitted
filter for background
1 parent b7f96a4 commit 5440606

30 files changed

+491
-362
lines changed

addons/html_builder/static/src/core/core_builder_action_plugin.js

-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Plugin } from "@html_editor/plugin";
22
import { CSS_SHORTHANDS, applyNeededCss, areCssValuesEqual } from "@html_builder/utils/utils_css";
3-
import { backgroundImageCssToParts, backgroundImagePartsToCss } from "@html_editor/utils/image";
43

54
export function withoutTransition(editingElement, callback) {
65
if (editingElement.classList.contains("o_we_force_no_transition")) {
@@ -48,23 +47,6 @@ export class CoreBuilderActionPlugin extends Plugin {
4847

4948
getStyleActions() {
5049
const styleActions = {
51-
"background-image-url": {
52-
getValue: (el) => {
53-
const value = getStyleValue(el, "background-image");
54-
const match = value.match(/url\(([^)]+)\)/);
55-
return match ? match[1] : "";
56-
},
57-
apply: (el, value, params) => {
58-
const parts = backgroundImageCssToParts(el.style["background-image"]);
59-
if (value) {
60-
parts.url = `url('${value}')`;
61-
} else {
62-
delete parts.url;
63-
}
64-
// todo: deal with the gradients
65-
setStyle(el, "background-image", backgroundImagePartsToCss(parts), params);
66-
},
67-
},
6850
"box-shadow": {
6951
getValue: (el, styleName) => {
7052
const value = getStyleValue(el, styleName);

addons/html_builder/static/src/plugins/background_option/background_image_option_plugin.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getBackgroundImageColor } from "./background_image_option";
1010

1111
export class BackgroundImageOptionPlugin extends Plugin {
1212
static id = "backgroundImageOption";
13-
static dependencies = ["builderActions", "media", "coreBuilderAction"];
13+
static dependencies = ["builderActions", "media", "style"];
1414
static shared = ["changeEditingEl"];
1515
resources = {
1616
builder_actions: this.getActions(),
@@ -175,7 +175,7 @@ export class BackgroundImageOptionPlugin extends Plugin {
175175
// We use selectStyle so that if when a background image is removed the
176176
// remaining image matches the o_cc's gradient background, it can be
177177
// removed too.
178-
this.dependencies.coreBuilderAction.setStyle(el, "background-image-url", backgroundURL);
178+
this.dependencies.style.setBackgroundImageUrl(el, backgroundURL);
179179
}
180180
}
181181

addons/html_builder/static/src/plugins/background_option/background_option.js

+7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { BackgroundImageOption } from "./background_image_option";
33
import { BackgroundPositionOption } from "./background_position_option";
44
import { BackgroundShapeOption } from "./background_shape_option";
55
import { useBackgroundOption } from "./background_hook";
6+
import { ImageFilterOption } from "../image/image_filter_option";
7+
import { ImageFormatOption } from "../image/image_format_option";
68

79
export class BackgroundOption extends BaseOptionComponent {
810
static template = "html_builder.BackgroundOption";
911
static components = {
1012
BackgroundImageOption,
1113
BackgroundPositionOption,
1214
BackgroundShapeOption,
15+
ImageFilterOption,
16+
ImageFormatOption,
1317
};
1418
static props = {
1519
withColors: { type: Boolean },
@@ -26,4 +30,7 @@ export class BackgroundOption extends BaseOptionComponent {
2630
const { showColorFilter } = useBackgroundOption(this.isActiveItem);
2731
this.showColorFilter = showColorFilter;
2832
}
33+
computeMaxDisplayWidth() {
34+
return 1920;
35+
}
2936
}

addons/html_builder/static/src/plugins/background_option/background_option.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
<t t-if="props.withImages">
2929
<BackgroundImageOption/>
3030
<BackgroundPositionOption/>
31-
<!-- TODO: BackgroundOptimize-->
31+
<ImageFilterOption level="2"/>
32+
<ImageFormatOption level="2" computeMaxDisplayWidth="this.computeMaxDisplayWidth"/>
3233
<!-- Color filter -->
3334
<BuilderRow t-if="this.showColorFilter()" label.translate="Color Filter" level="2">
3435
<!-- TODO handle all the attributes -->

addons/html_builder/static/src/plugins/background_option/background_shape_option_plugin.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { getValueFromVar, isMobileView } from "@html_builder/utils/utils";
2-
import { getBgImageURLFromURL, normalizeColor } from "@html_builder/utils/utils_css";
2+
import { normalizeColor } from "@html_builder/utils/utils_css";
33
import { Plugin } from "@html_editor/plugin";
44
import { registry } from "@web/core/registry";
55
import { pick } from "@web/core/utils/objects";
66
import { backgroundShapesDefinition } from "./background_shapes_definition";
77
import { ShapeSelector } from "../shape/shape_selector";
88
import { getDefaultColors } from "./background_shape_option";
99
import { withSequence } from "@html_editor/utils/resource";
10+
import { getBgImageURLFromURL } from "@html_editor/utils/image";
1011

1112
export class BackgroundShapeOptionPlugin extends Plugin {
1213
static id = "backgroundShapeOption";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
2+
import { shouldPreventGifTransformation } from "@html_editor/main/media/image_post_process_plugin";
3+
import { loadImageInfo } from "@html_editor/utils/image_processing";
4+
import { KeepLast } from "@web/core/utils/concurrency";
5+
6+
export class ImageFilterOption extends BaseOptionComponent {
7+
static template = "html_builder.ImageFilterOption";
8+
static props = {
9+
level: { type: Number, optional: true },
10+
};
11+
static defaultProps = {
12+
level: 0,
13+
};
14+
setup() {
15+
super.setup();
16+
const keepLast = new KeepLast();
17+
this.state = useDomState((editingElement) => {
18+
keepLast
19+
.add(
20+
loadImageInfo(editingElement).then((data) => ({
21+
...editingElement.dataset,
22+
...data,
23+
}))
24+
)
25+
.then((data) => {
26+
this.state.showFilter =
27+
data.mimetypeBeforeConversion && !shouldPreventGifTransformation(data);
28+
});
29+
return {
30+
isCustomFilter: editingElement.dataset.glFilter === "custom",
31+
showFilter: false,
32+
};
33+
});
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="html_builder.ImageFilterOption">
5+
<BuilderRow label.translate="Filter" level="this.props.level" t-if="state.showFilter">
6+
<BuilderSelect>
7+
<BuilderSelectItem action="'glFilter'" actionParam="''">None</BuilderSelectItem>
8+
<BuilderSelectItem action="'glFilter'" actionParam="'blur'">Blur</BuilderSelectItem>
9+
<BuilderSelectItem action="'glFilter'" actionParam="'1977'">1977</BuilderSelectItem>
10+
<BuilderSelectItem action="'glFilter'" actionParam="'aden'">Aden</BuilderSelectItem>
11+
<BuilderSelectItem action="'glFilter'" actionParam="'brannan'">Brannan</BuilderSelectItem>
12+
<BuilderSelectItem action="'glFilter'" actionParam="'earlybird'">EarlyBird</BuilderSelectItem>
13+
<BuilderSelectItem action="'glFilter'" actionParam="'inkwell'">Inkwell</BuilderSelectItem>
14+
<BuilderSelectItem action="'glFilter'" actionParam="'maven'">Maven</BuilderSelectItem>
15+
<BuilderSelectItem action="'glFilter'" actionParam="'toaster'">Toaster</BuilderSelectItem>
16+
<BuilderSelectItem action="'glFilter'" actionParam="'walden'">Walden</BuilderSelectItem>
17+
<BuilderSelectItem action="'glFilter'" actionParam="'valencia'">Valencia</BuilderSelectItem>
18+
<BuilderSelectItem action="'glFilter'" actionParam="'xpro'">Xpro</BuilderSelectItem>
19+
<BuilderSelectItem action="'glFilter'" actionParam="'custom'" id="custom_glfilter_opt">Custom</BuilderSelectItem>
20+
</BuilderSelect>
21+
</BuilderRow>
22+
<t t-if="state.isCustomFilter">
23+
<BuilderRow level="this.props.level + 1" label.translate="Color">
24+
<BuilderSelect action="'setCustomFilter'" actionParam="'blend'">
25+
<BuilderSelectItem actionValue="'normal'">Normal</BuilderSelectItem>
26+
<BuilderSelectItem actionValue="'overlay'">Overlay</BuilderSelectItem>
27+
<BuilderSelectItem actionValue="'screen'">Screen</BuilderSelectItem>
28+
<BuilderSelectItem actionValue="'multiply'">Multiply</BuilderSelectItem>
29+
<BuilderSelectItem actionValue="'lighter'">Add</BuilderSelectItem>
30+
<BuilderSelectItem actionValue="'exclusion'">Exclusion</BuilderSelectItem>
31+
<BuilderSelectItem actionValue="'darken'">Darken</BuilderSelectItem>
32+
<BuilderSelectItem actionValue="'lighten'">Lighten</BuilderSelectItem>
33+
</BuilderSelect>
34+
<BuilderColorPicker action="'setCustomFilter'" actionParam="'filterColor'" enabledTabs="['custom']" />
35+
</BuilderRow>
36+
<BuilderRow level="this.props.level + 1" label.translate="Saturation">
37+
<BuilderRange
38+
action="'setCustomFilter'"
39+
actionParam="'saturation'"
40+
min="-100"
41+
max="100"
42+
step="10" />
43+
</BuilderRow>
44+
<BuilderRow level="this.props.level + 1" label.translate="Contrast">
45+
<BuilderRange
46+
action="'setCustomFilter'"
47+
actionParam="'contrast'"
48+
min="-100"
49+
max="100"
50+
step="10" />
51+
</BuilderRow>
52+
<BuilderRow level="this.props.level + 1" label.translate="Brightness">
53+
<BuilderRange
54+
action="'setCustomFilter'"
55+
actionParam="'brightness'"
56+
min="-100"
57+
max="100"
58+
step="10" />
59+
</BuilderRow>
60+
<BuilderRow level="this.props.level + 1" label.translate="Sepia">
61+
<BuilderRange
62+
action="'setCustomFilter'"
63+
actionParam="'sepia'"
64+
min="0"
65+
max="100"
66+
step="5" />
67+
</BuilderRow>
68+
<BuilderRow level="this.props.level + 1" label.translate="Blur">
69+
<BuilderRange
70+
action="'setCustomFilter'"
71+
actionParam="'blur'"
72+
min="0"
73+
max="2000"
74+
step="100" />
75+
</BuilderRow>
76+
</t>
77+
</t>
78+
79+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { normalizeColor } from "@html_builder/utils/utils_css";
2+
import { defaultImageFilterOptions } from "@html_editor/main/media/image_post_process_plugin";
3+
import { Plugin } from "@html_editor/plugin";
4+
import { registry } from "@web/core/registry";
5+
6+
class ImageFilterOptionPlugin extends Plugin {
7+
static id = "ImageFilterOption";
8+
static dependencies = ["imagePostProcess"];
9+
resources = {
10+
builder_actions: this.getActions(),
11+
};
12+
getActions() {
13+
return {
14+
glFilter: {
15+
isApplied: ({ editingElement, params: { mainParam: glFilterName } }) => {
16+
if (glFilterName) {
17+
return editingElement.dataset.glFilter === glFilterName;
18+
} else {
19+
return !editingElement.dataset.glFilter;
20+
}
21+
},
22+
load: async ({ editingElement: img, params: { mainParam: glFilterName } }) =>
23+
await this.dependencies.imagePostProcess.processImage(img, {
24+
glFilter: glFilterName,
25+
}),
26+
apply: ({ loadResult: updateImageAttributes }) => {
27+
updateImageAttributes();
28+
},
29+
},
30+
setCustomFilter: {
31+
getValue: ({ editingElement, params: { mainParam: filterProperty } }) => {
32+
const filterOptions = JSON.parse(editingElement.dataset.filterOptions || "{}");
33+
return (
34+
filterOptions[filterProperty] || defaultImageFilterOptions[filterProperty]
35+
);
36+
},
37+
isApplied: ({
38+
editingElement,
39+
params: { mainParam: filterProperty },
40+
value: filterValue,
41+
}) => {
42+
const filterOptions = JSON.parse(editingElement.dataset.filterOptions || "{}");
43+
return (
44+
filterValue ===
45+
(filterOptions[filterProperty] || defaultImageFilterOptions[filterProperty])
46+
);
47+
},
48+
load: async ({
49+
editingElement: img,
50+
params: { mainParam: filterProperty },
51+
value,
52+
}) => {
53+
const filterOptions = JSON.parse(img.dataset.filterOptions || "{}");
54+
filterOptions[filterProperty] =
55+
filterProperty === "filterColor" ? normalizeColor(value) : value;
56+
return this.dependencies.imagePostProcess.processImage(img, {
57+
filterOptions: JSON.stringify(filterOptions),
58+
});
59+
},
60+
apply: ({ loadResult: updateImageAttributes }) => {
61+
updateImageAttributes();
62+
},
63+
},
64+
};
65+
}
66+
}
67+
registry.category("website-plugins").add(ImageFilterOptionPlugin.id, ImageFilterOptionPlugin);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { BaseOptionComponent, useDomState } from "@html_builder/core/utils";
2+
import { KeepLast } from "@web/core/utils/concurrency";
3+
import { getImageSrc, getMimetype } from "@html_editor/utils/image";
4+
import { clamp } from "@web/core/utils/numbers";
5+
6+
export class ImageFormatOption extends BaseOptionComponent {
7+
static template = "html_builder.ImageFormat";
8+
static props = {
9+
level: { type: Number, optional: true },
10+
computeMaxDisplayWidth: { type: Function, optional: true },
11+
};
12+
static defaultProps = {
13+
level: 0,
14+
};
15+
MAX_SUGGESTED_WIDTH = 1920;
16+
setup() {
17+
super.setup();
18+
const keepLast = new KeepLast();
19+
this.state = useDomState((editingElement) => {
20+
keepLast
21+
.add(
22+
this.env.editor.shared.imageFormatOption.computeAvailableFormats(
23+
editingElement,
24+
this.computeMaxDisplayWidth.bind(this)
25+
)
26+
)
27+
.then((formats) => {
28+
const hasSrc = !!getImageSrc(editingElement);
29+
this.state.formats = hasSrc ? formats : [];
30+
});
31+
return {
32+
showQuality: ["image/jpeg", "image/webp"].includes(getMimetype(editingElement)),
33+
formats: [],
34+
};
35+
});
36+
}
37+
computeMaxDisplayWidth(img) {
38+
if (this.props.computeMaxDisplayWidth) {
39+
return this.props.computeMaxDisplayWidth(img);
40+
}
41+
const window = img.ownerDocument.defaultView;
42+
if (!window) {
43+
return;
44+
}
45+
const computedStyles = window.getComputedStyle(img);
46+
const displayWidth = parseFloat(computedStyles.getPropertyValue("width"));
47+
const gutterWidth =
48+
parseFloat(computedStyles.getPropertyValue("--o-grid-gutter-width")) || 30;
49+
50+
// For the logos we don't want to suggest a width too small.
51+
if (img.closest("nav")) {
52+
return Math.round(Math.min(displayWidth * 3, this.MAX_SUGGESTED_WIDTH));
53+
// If the image is in a container(-small), it might get bigger on
54+
// smaller screens. So we suggest the width of the current image unless
55+
// it is smaller than the size of the container on the md breapoint
56+
// (which is where our bootstrap columns fallback to full container
57+
// width since we only use col-lg-* in Odoo).
58+
} else if (img.closest(".container, .o_container_small")) {
59+
const mdContainerMaxWidth =
60+
parseFloat(computedStyles.getPropertyValue("--o-md-container-max-width")) || 720;
61+
const mdContainerInnerWidth = mdContainerMaxWidth - gutterWidth;
62+
return Math.round(clamp(displayWidth, mdContainerInnerWidth, this.MAX_SUGGESTED_WIDTH));
63+
// If the image is displayed in a container-fluid, it might also get
64+
// bigger on smaller screens. The same way, we suggest the width of the
65+
// current image unless it is smaller than the max size of the container
66+
// on the md breakpoint (which is the LG breakpoint since the container
67+
// fluid is full-width).
68+
} else if (img.closest(".container-fluid")) {
69+
const lgBp = parseFloat(computedStyles.getPropertyValue("--breakpoint-lg")) || 992;
70+
const mdContainerFluidMaxInnerWidth = lgBp - gutterWidth;
71+
return Math.round(
72+
clamp(displayWidth, mdContainerFluidMaxInnerWidth, this.MAX_SUGGESTED_WIDTH)
73+
);
74+
}
75+
// If it's not in a container, it's probably not going to change size
76+
// depending on breakpoints. We still keep a margin safety.
77+
return Math.round(Math.min(displayWidth * 1.5, this.MAX_SUGGESTED_WIDTH));
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="html_builder.ImageFormat">
5+
<BuilderRow label.translate="Format" level="this.props.level">
6+
<BuilderSelect>
7+
<t t-foreach="state.formats" t-as="format" t-key="format.id">
8+
<BuilderSelectItem className="'o_we_badge_at_end'" action="'setImageFormat'" actionParam="format">
9+
<t t-esc="format.label"/>
10+
<span class="badge rounded-pill text-bg-dark" t-out="format.mimetype.split('/')[1]"/>
11+
</BuilderSelectItem>
12+
</t>
13+
</BuilderSelect>
14+
</BuilderRow>
15+
<BuilderRow label.translate="Quality" t-if="state.showQuality" level="this.props.level">
16+
<BuilderRange
17+
action="'setImageQuality'"
18+
min="0"
19+
max="100" />
20+
</BuilderRow>
21+
</t>
22+
23+
24+
25+
</templates>

0 commit comments

Comments
 (0)