From 791df5a7547da5569cb059a6fcb2a8ba220129c3 Mon Sep 17 00:00:00 2001 From: "Louis (loco)" Date: Wed, 15 Jan 2025 09:29:10 +0100 Subject: [PATCH] Rating option --- .../builder_components/builder_row.xml | 2 +- .../src/builder/options/rating_option.js | 145 ++++++++++++++++++ .../src/builder/options/rating_option.xml | 44 ++++++ .../static/src/img/options/size_large.svg | 10 ++ .../static/src/img/options/size_medium.svg | 10 ++ .../static/src/img/options/size_small.svg | 10 ++ .../tests/options/rating_option.test.js | 73 +++++++++ 7 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 addons/html_builder/static/src/builder/options/rating_option.js create mode 100644 addons/html_builder/static/src/builder/options/rating_option.xml create mode 100644 addons/html_builder/static/src/img/options/size_large.svg create mode 100644 addons/html_builder/static/src/img/options/size_medium.svg create mode 100644 addons/html_builder/static/src/img/options/size_small.svg create mode 100644 addons/html_builder/static/tests/options/rating_option.test.js diff --git a/addons/html_builder/static/src/builder/builder_components/builder_row.xml b/addons/html_builder/static/src/builder/builder_components/builder_row.xml index d5702f7dbd235..54ec51be14b95 100644 --- a/addons/html_builder/static/src/builder/builder_components/builder_row.xml +++ b/addons/html_builder/static/src/builder/builder_components/builder_row.xml @@ -3,7 +3,7 @@ -
+
diff --git a/addons/html_builder/static/src/builder/options/rating_option.js b/addons/html_builder/static/src/builder/options/rating_option.js new file mode 100644 index 0000000000000..78cae5209d748 --- /dev/null +++ b/addons/html_builder/static/src/builder/options/rating_option.js @@ -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)); +} diff --git a/addons/html_builder/static/src/builder/options/rating_option.xml b/addons/html_builder/static/src/builder/options/rating_option.xml new file mode 100644 index 0000000000000..dbd76bfded9c5 --- /dev/null +++ b/addons/html_builder/static/src/builder/options/rating_option.xml @@ -0,0 +1,44 @@ + + + + + + + Stars + Thumbs + Circles + Squares + Hearts + Custom + + + + + Replace Icon + + + + Replace Icon + + + + / + + + + + + + + + + + + Top + Left + None + + + + + diff --git a/addons/html_builder/static/src/img/options/size_large.svg b/addons/html_builder/static/src/img/options/size_large.svg new file mode 100644 index 0000000000000..1354178068994 --- /dev/null +++ b/addons/html_builder/static/src/img/options/size_large.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/html_builder/static/src/img/options/size_medium.svg b/addons/html_builder/static/src/img/options/size_medium.svg new file mode 100644 index 0000000000000..00b3a3d43d0f8 --- /dev/null +++ b/addons/html_builder/static/src/img/options/size_medium.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/html_builder/static/src/img/options/size_small.svg b/addons/html_builder/static/src/img/options/size_small.svg new file mode 100644 index 0000000000000..aaa36a673855b --- /dev/null +++ b/addons/html_builder/static/src/img/options/size_small.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/html_builder/static/tests/options/rating_option.test.js b/addons/html_builder/static/tests/options/rating_option.test.js new file mode 100644 index 0000000000000..0f73c29a61aab --- /dev/null +++ b/addons/html_builder/static/tests/options/rating_option.test.js @@ -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( + `
+

Quality

+
+ + + + + + + + + +
+
` + ); + 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( + `
+

Quality

+
+ + + + + + + + + +
+
` + ); + 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"); +});