Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<t t-name="html_builder.BuilderRow">
<BuilderComponent dependencies="props.dependencies">
<div class="d-flex p-1 px-2 hb-row" t-att-class="props.extraClassName or ''" t-ref="root">
<div class="d-flex p-1 px-2 hb-row" t-att-class="props.extraClassName or ''" t-ref="root" t-att-data-label="props.label">
Copy link
Author

@loco-odoo loco-odoo Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only to target the desired option more easily in the tests

<div class="d-flex" style="flex-grow: 0.4; flex-basis: 0; min-width: 0;" t-att-data-tooltip="props.tooltip">
<span class="text-nowrap text-truncate" t-out="props.label"/>
</div>
Expand Down
145 changes: 145 additions & 0 deletions addons/html_builder/static/src/builder/options/rating_option.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Plugin } from "@html_editor/plugin";
import { registry } from "@web/core/registry";

class RatingOptionPlugin extends Plugin {
static id = "RatingOption";
static dependencies = ["history", "media"];
selector = ".s_rating";
resources = {
builder_options: {
template: "html_builder.RatingOption",
selector: ".s_rating",
},
builder_actions: this.getActions(),
};
getActions() {
return {
setIcons: {
apply: ({ editingElement, param: iconParam }) => {
editingElement.dataset.icon = iconParam;
renderIcons(editingElement);
delete editingElement.dataset.activeCustomIcon;
delete editingElement.dataset.inactiveCustomIcon;
},
isApplied: ({ editingElement, param: iconParam }) =>
getIconType(editingElement) === iconParam,
},
customIcon: {
load: async ({ editingElement, param: customParam }) =>
new Promise((resolve) => {
const isCustomActive = customParam === "customActiveIcon";
const media = document.createElement("i");
media.className = isCustomActive
? getActiveCustomIcons(editingElement)
: getInactiveCustomIcons(editingElement);
const mediaDialogParams = {
noImages: true,
noDocuments: true,
noVideos: true,
media,
save: (icon) => {
resolve(icon);
},
};
this.dependencies.media.openMediaDialog(mediaDialogParams, this.editable);
}),
apply: ({ editingElement, loadResult: savedIconEl, param: customParam }) => {
const isCustomActive = customParam === "customActiveIcon";
const customClass = savedIconEl.className;
const activeIconEls = getActiveIcons(editingElement);
const inactiveIconEls = getInactiveIcons(editingElement);
const iconEls = isCustomActive ? activeIconEls : inactiveIconEls;
iconEls.forEach((iconEl) => (iconEl.className = customClass));
const faClassActiveCustomIcons =
activeIconEls.length > 0
? activeIconEls[0].getAttribute("class")
: customClass;
const faClassInactiveCustomIcons =
inactiveIconEls.length > 0
? inactiveIconEls[0].getAttribute("class")
: customClass;
editingElement.dataset.activeCustomIcon = faClassActiveCustomIcons;
editingElement.dataset.inactiveCustomIcon = faClassInactiveCustomIcons;
editingElement.dataset.icon = "custom";
},
},
activeIconsNumber: {
apply: ({ editingElement, value }) => {
const nbActiveIcons = parseInt(value);
const nbTotalIcons = getAllIcons(editingElement).length;
createIcons({
editingElement: editingElement,
nbActiveIcons: nbActiveIcons,
nbTotalIcons: nbTotalIcons,
});
},
getValue: ({ editingElement }) => getActiveIcons(editingElement).length,
},
totalIconsNumber: {
apply: ({ editingElement, value }) => {
const nbTotalIcons = Math.max(parseInt(value), 1);
const nbActiveIcons = getActiveIcons(editingElement).length;
createIcons({
editingElement: editingElement,
nbActiveIcons: nbActiveIcons,
nbTotalIcons: nbTotalIcons,
});
},
getValue: ({ editingElement }) => getAllIcons(editingElement).length,
},
};
}
}

registry.category("website-plugins").add(RatingOptionPlugin.id, RatingOptionPlugin);

function createIcons({ editingElement, nbActiveIcons, nbTotalIcons }) {
const activeIconEl = editingElement.querySelector(".s_rating_active_icons");
const inactiveIconEl = editingElement.querySelector(".s_rating_inactive_icons");
const iconEls = getAllIcons(editingElement);
[...iconEls].forEach((iconEl) => iconEl.remove());
for (let i = 0; i < nbTotalIcons; i++) {
if (i < nbActiveIcons) {
activeIconEl.appendChild(document.createElement("i"));
} else {
inactiveIconEl.append(document.createElement("i"));
}
}
renderIcons(editingElement);
}
function getActiveCustomIcons(editingElement) {
return editingElement.dataset.activeCustomIcon || "";
}
function getActiveIcons(editingElement) {
return editingElement.querySelectorAll(".s_rating_active_icons > i");
}
function getAllIcons(editingElement) {
return editingElement.querySelectorAll(".s_rating_icons i");
}
function getIconType(editingElement) {
return editingElement.dataset.icon;
}
function getInactiveCustomIcons(editingElement) {
return editingElement.dataset.inactiveCustomIcon || "";
}
function getInactiveIcons(editingElement) {
return editingElement.querySelectorAll(".s_rating_inactive_icons > i");
}
function renderIcons(editingElement) {
const iconType = getIconType(editingElement);
const icons = {
"fa-star": "fa-star-o",
"fa-thumbs-up": "fa-thumbs-o-up",
"fa-circle": "fa-circle-o",
"fa-square": "fa-square-o",
"fa-heart": "fa-heart-o",
};
const faClassActiveIcons =
iconType === "custom" ? getActiveCustomIcons(editingElement) : "fa " + iconType;
const faClassInactiveIcons =
iconType === "custom" ? getInactiveCustomIcons(editingElement) : "fa " + icons[iconType];
const activeIconEls = getActiveIcons(editingElement);
const inactiveIconEls = getInactiveIcons(editingElement);
activeIconEls.forEach((activeIconEl) => (activeIconEl.className = faClassActiveIcons));
inactiveIconEls.forEach((inactiveIconEl) => (inactiveIconEl.className = faClassInactiveIcons));
}
44 changes: 44 additions & 0 deletions addons/html_builder/static/src/builder/options/rating_option.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">

<t t-name="html_builder.RatingOption">
<BuilderRow label.translate="Icon">
<BuilderSelect>
<BuilderSelectItem action="'setIcons'" actionParam="'fa-star'">Stars</BuilderSelectItem>
<BuilderSelectItem action="'setIcons'" actionParam="'fa-thumbs-up'">Thumbs</BuilderSelectItem>
<BuilderSelectItem action="'setIcons'" actionParam="'fa-circle'">Circles</BuilderSelectItem>
<BuilderSelectItem action="'setIcons'" actionParam="'fa-square'">Squares</BuilderSelectItem>
<BuilderSelectItem action="'setIcons'" actionParam="'fa-heart'">Hearts</BuilderSelectItem>
<BuilderSelectItem action="'setIcons'" actionParam="'custom'">Custom</BuilderSelectItem>
</BuilderSelect>
</BuilderRow>
<BuilderRow label.translate="&#8985; Active">
<BuilderColorPicker applyTo="'.s_rating_active_icons'" styleAction="'color'"/>
<BuilderButton action="'customIcon'" actionParam="'customActiveIcon'" preview="false"><i class="fa fa-fw fa-refresh me-1"/> Replace Icon</BuilderButton>
</BuilderRow>
<BuilderRow label.translate="&#8985; Inactive">
<BuilderColorPicker applyTo="'.s_rating_inactive_icons'" styleAction="'color'"/>
<BuilderButton action="'customIcon'" actionParam="'customInactiveIcon'" preview="false"><i class="fa fa-fw fa-refresh me-1"/> Replace Icon</BuilderButton>
</BuilderRow>
<BuilderRow label.translate="Score">
<BuilderNumberInput action="'activeIconsNumber'"/>
<span class="mx-2">/</span>
<BuilderNumberInput action="'totalIconsNumber'"/>
</BuilderRow>
<BuilderRow label.translate="Size">
<BuilderButtonGroup applyTo="'.s_rating_icons'">
<BuilderButton classAction="''" title="'Small'"><img src="/html_builder/static/src/img/options/size_small.svg" /></BuilderButton>
<BuilderButton classAction="'fa-2x'" title="'Medium'"><img src="/html_builder/static/src/img/options/size_medium.svg" /></BuilderButton>
<BuilderButton classAction="'fa-3x'" title="'Large'"><img src="/html_builder/static/src/img/options/size_large.svg" /></BuilderButton>
</BuilderButtonGroup>
</BuilderRow>
<BuilderRow label.translate="Title Position">
<BuilderSelect>
<BuilderSelectItem classAction="''">Top</BuilderSelectItem>
<BuilderSelectItem classAction="'s_rating_inline'">Left</BuilderSelectItem>
<BuilderSelectItem classAction="'s_rating_no_title'">None</BuilderSelectItem>
</BuilderSelect>
</BuilderRow>
</t>

</templates>
10 changes: 10 additions & 0 deletions addons/html_builder/static/src/img/options/size_large.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions addons/html_builder/static/src/img/options/size_medium.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions addons/html_builder/static/src/img/options/size_small.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions addons/html_builder/static/tests/options/rating_option.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { defineWebsiteModels, setupWebsiteBuilder } from "../helpers";
import { expect, test } from "@odoo/hoot";
import { animationFrame, clear, click, fill } from "@odoo/hoot-dom";
import { contains } from "@web/../tests/web_test_helpers";

defineWebsiteModels();

test("change rating score", async () => {
await setupWebsiteBuilder(
`<div class="s_rating pt16 pb16" data-icon="fa-star" data-snippet="s_rating" data-name="Rating">
<h4 class="s_rating_title">Quality</h4>
<div class="s_rating_icons">
<span class="s_rating_active_icons">
<i class="fa fa-star"></i>
<i class="fa fa-star"></i>
<i class="fa fa-star"></i>
</span>
<span class="s_rating_inactive_icons">
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
</span>
</div>
</div>`
);
expect(":iframe .s_rating .s_rating_active_icons i").toHaveCount(3);
expect(":iframe .s_rating .s_rating_inactive_icons i").toHaveCount(2);
await contains(":iframe .s_rating").click();
await contains(".options-container [data-action-id='activeIconsNumber'] input").click();
await clear();
await fill("1");
expect(":iframe .s_rating .s_rating_active_icons i").toHaveCount(1);
await contains(".options-container [data-action-id='totalIconsNumber'] input").click();
await clear();
await fill("4");
expect(":iframe .s_rating .s_rating_inactive_icons i").toHaveCount(3);
});
test("Ensure order of operations when clicking very fast on two options", async () => {
await setupWebsiteBuilder(
`<div class="s_rating pt16 pb16" data-icon="fa-star" data-snippet="s_rating" data-name="Rating">
<h4 class="s_rating_title">Quality</h4>
<div class="s_rating_icons">
<span class="s_rating_active_icons">
<i class="fa fa-star"></i>
<i class="fa fa-star"></i>
<i class="fa fa-star"></i>
</span>
<span class="s_rating_inactive_icons">
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
</span>
</div>
</div>`
);
await contains(":iframe .s_rating").click();
expect("[data-label='Icon'] .btn-primary.dropdown-toggle").toHaveText("Stars");
expect(":iframe .s_rating").not.toHaveAttribute("data-active-custom-icon");
await click(".options-container [data-action-id='customIcon']");
await click(".options-container [data-class-action='fa-2x']");
await animationFrame();
expect(":iframe .s_rating_icons").not.toHaveClass("fa-2x");
await contains(".modal-dialog .fa-glass").click();
expect(":iframe .s_rating").toHaveAttribute("data-active-custom-icon", "fa fa-glass");
expect("[data-label='Icon'] .btn-primary.dropdown-toggle").toHaveText("Custom");
expect(":iframe .s_rating_icons").toHaveClass("fa-2x");
await contains(".o-snippets-top-actions .fa-undo").click();
expect("[data-label='Icon'] .btn-primary.dropdown-toggle").toHaveText("Custom");
expect(":iframe .s_rating").toHaveAttribute("data-active-custom-icon", "fa fa-glass");
expect(":iframe .s_rating_icons").not.toHaveClass("fa-2x");
await contains(".o-snippets-top-actions .fa-undo").click();
expect("[data-label='Icon'] .btn-primary.dropdown-toggle").toHaveText("Stars");
expect(":iframe .s_rating").not.toHaveAttribute("data-active-custom-icon");
expect(":iframe .s_rating_icons").not.toHaveClass("fa-2x");
});