[IMP] website: target popup as a link

This commit introduces a new feature to the popup snippet.
Up until now, the popup could be displayed after a delay or on exit.
This commit enables the popup to be displayed on click.

To make use of this feature, the user will need to:

- Set the "Display" option of the popup to "On Click (via link)".
- Paste the anchor that has been copied to the clipboard into the
URL input of any link.

This commit also adds a test for this new feature.

task-2172312

closes odoo/odoo#76442

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Benjamin Vray
2023-07-11 22:33:36 +02:00
parent 858943c47d
commit b043f0671a
10 changed files with 207 additions and 35 deletions
@@ -2671,27 +2671,18 @@ options.registry.HideFooter = VisibilityPageOptionUpdate.extend({
options.registry.anchor = options.Class.extend({
isTopOption: true,
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
start: function () {
start() {
// Generate anchor and copy it to clipboard on click, show the tooltip on success
this.$button = this.$el.find('we-button');
const clipboard = new ClipboardJS(this.$button[0], {text: () => this._getAnchorLink()});
clipboard.on('success', () => {
const message = sprintf(Markup(_t("Anchor copied to clipboard<br>Link: %s")), this._getAnchorLink());
this.displayNotification({
type: 'success',
message: message,
buttons: [{text: _t("Edit"), click: () => this.openAnchorDialog(), primary: true}],
});
});
const buttonEl = this.el.querySelector("we-button");
this.isModal = this.$target[0].classList.contains("modal");
if (buttonEl && !this.isModal) {
this._buildClipboard(buttonEl);
}
return this._super.apply(this, arguments);
return this._super(...arguments);
},
/**
* @override
@@ -2701,13 +2692,45 @@ options.registry.anchor = options.Class.extend({
this.$target.filter(':not(.carousel)').removeAttr('id');
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
notify(name, data) {
this._super(...arguments);
if (name === "modalAnchor") {
this._buildClipboard(data.buttonEl);
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @see this.selectClass for parameters
* @private
* @param {Element} buttonEl
*/
openAnchorDialog: function (previewMode, widgetValue, params) {
_buildClipboard(buttonEl) {
const clipboard = new ClipboardJS(buttonEl, {text: () => this._getAnchorLink()});
clipboard.on("success", () => {
const message = sprintf(Markup(_t("Anchor copied to clipboard<br>Link: %s")), this._getAnchorLink());
this.displayNotification({
type: "success",
message: message,
buttons: [{text: _t("Edit"), click: () => this._openAnchorDialog(buttonEl), primary: true}],
});
});
},
/**
* @private
* @param {Element} buttonEl
*/
_openAnchorDialog(buttonEl) {
var self = this;
var buttons = [{
text: _t("Save & copy"),
@@ -2728,7 +2751,7 @@ options.registry.anchor = options.Class.extend({
if (!alreadyExists) {
self._setAnchorName(anchorName);
this.close();
self.$button[0].click();
buttonEl.click();
}
},
}, {
@@ -2760,10 +2783,10 @@ options.registry.anchor = options.Class.extend({
*/
_setAnchorName: function (value) {
if (value) {
this.$target.attr({
'id': value,
'data-anchor': true,
});
this.$target[0].id = value;
if (!this.isModal) {
this.$target[0].dataset.anchor = true;
}
} else {
this.$target.removeAttr('id data-anchor');
}
@@ -2786,7 +2809,8 @@ options.registry.anchor = options.Class.extend({
}
this._setAnchorName(anchorName + n);
}
return `${this.ownerDocument.location.pathname}#${this.$target[0].id}`;
const pathName = this.isModal ? "" : this.ownerDocument.location.pathname;
return `${pathName}#${this.$target[0].id}`;
},
/**
* Creates a safe id/anchor from text.
@@ -108,6 +108,7 @@ function changeOption(optionName, weName = '', optionTooltipLabel = '', position
trigger: `${option_block} ${weName}, ${option_block} [title='${weName}']`,
content: Markup(sprintf(_t("<b>Click</b> on this option to change the %s of the block."), optionTooltipLabel)),
position: position,
in_modal: false,
run: "click",
};
}
+1 -1
View File
@@ -22,7 +22,7 @@ function loadAnchors(url, body) {
resolve();
}
}).then(function (response) {
const anchors = $(response).find('[id][data-anchor=true]').toArray().map((el) => {
const anchors = $(response).find('[id][data-anchor=true], .modal[id][data-display="onClick"]').toArray().map((el) => {
return '#' + el.id;
});
// Always suggest the top and the bottom of the page as internal link
@@ -81,9 +81,21 @@ const PopupWidget = publicWidget.Widget.extend({
* @override
*/
start: function () {
this._popupAlreadyShown = !!getCookie(this.$el.attr('id'));
if (!this._popupAlreadyShown) {
this._bindPopup();
this.modalShownOnClickEl = this.el.querySelector(".modal[data-display='onClick']");
if (this.modalShownOnClickEl) {
// We add a "hashchange" listener in case a button to open a popup
// is clicked.
this.__onHashChange = this._onHashChange.bind(this);
window.addEventListener('hashchange', this.__onHashChange);
// Check if a hash exists and if the modal needs to be opened when
// the page loads (e.g. The user has clicked a button on the
// "Contact us" page to open a popup on the homepage).
this._showPopupOnClick();
} else {
this._popupAlreadyShown = !!getCookie(this.$el.attr('id'));
if (!this._popupAlreadyShown) {
this._bindPopup();
}
}
return this._super(...arguments);
},
@@ -95,6 +107,9 @@ const PopupWidget = publicWidget.Widget.extend({
$(document).off('mouseleave.open_popup');
this.$el.find('.modal').modal('hide');
clearTimeout(this.timeout);
if (this.modalShownOnClickEl) {
window.removeEventListener('hashchange', this.__onHashChange);
}
},
//--------------------------------------------------------------------------
@@ -119,7 +134,7 @@ const PopupWidget = publicWidget.Widget.extend({
if (display === 'afterDelay') {
this.timeout = setTimeout(() => this._showPopup(), delay);
} else {
} else if (display === "mouseExit") {
$(document).on('mouseleave.open_popup', () => this._showPopup());
}
},
@@ -144,6 +159,21 @@ const PopupWidget = publicWidget.Widget.extend({
}
this.$el.find('.modal').modal('show');
},
/**
* @private
*/
_showPopupOnClick() {
const hash = window.location.hash;
// If a hash exists in the URL and it corresponds to the ID of the modal,
// then we open the modal.
if (hash && hash.substring(1) === this.modalShownOnClickEl.id) {
// We remove the hash from the URL because otherwise the popup
// cannot open again after being closed.
const urlWithoutHash = window.location.href.replace(hash, '');
window.history.replaceState(null, null, urlWithoutHash);
this._showPopup();
}
},
//--------------------------------------------------------------------------
// Handlers
@@ -161,7 +191,7 @@ const PopupWidget = publicWidget.Widget.extend({
_onHideModal: function () {
const nbDays = this.$el.find('.modal').data('consentsDuration');
setCookie(this.el.id, this.cookieValue, nbDays * 24 * 60 * 60, 'required');
this._popupAlreadyShown = true;
this._popupAlreadyShown = true && !this.modalShownOnClickEl;
this.$el.find('.media_iframe_video iframe').each((i, iframe) => {
iframe.src = '';
@@ -176,6 +206,12 @@ const PopupWidget = publicWidget.Widget.extend({
iframe.src = media.dataset.oeExpression || media.dataset.src; // TODO still oeExpression to remove someday
});
},
/**
* @private
*/
_onHashChange() {
this._showPopupOnClick();
},
});
publicWidget.registry.popup = PopupWidget;
@@ -31,6 +31,15 @@ options.registry.SnippetPopup = options.Class.extend({
// sound). Stop the video, as the user can't do it (no button on video
// in edit mode).
this._removeIframeSrc();
if (!this.$target[0].parentElement.matches("#website_cookies_bar")) {
this.trigger_up("option_update", {
optionName: "anchor",
name: "modalAnchor",
data: {
buttonEl: this._requestUserValueWidgets("onclick_opt")[0].el,
},
});
}
return this._super(...arguments);
},
/**
@@ -401,10 +401,18 @@
if (successPage.charAt(0) === "#") {
const successAnchorEl = document.getElementById(successPage.substring(1));
if (successAnchorEl) {
await dom.scrollTo(successAnchorEl, {
duration: 500,
extraOffset: 0,
});
// Check if the target of the link is a modal.
if (successAnchorEl.classList.contains("modal")) {
// Trigger a "hashChange" event to
// notify the popup widget to show the
// popup.
window.location.href = successPage;
} else {
await dom.scrollTo(successAnchorEl, {
duration: 500,
extraOffset: 0,
});
}
}
break;
}
@@ -0,0 +1,85 @@
/** @odoo-module */
import wTourUtils from "website.tour_utils";
wTourUtils.registerWebsitePreviewTour("snippet_popup_display_on_click", {
test: true,
url: "/",
edition: true,
}, [
wTourUtils.dragNDrop({id: "s_text_image", name: "Image - Text"}),
wTourUtils.dragNDrop({id: "s_popup", name: "Popup"}),
{
content: "Click inside the popup to access its options menu.",
in_modal: false,
trigger: "iframe .s_popup .s_banner",
},
wTourUtils.changeOption("SnippetPopup", 'we-select[data-attribute-name="display"] we-toggler'),
{
content: "Click on the display 'On Click' option",
trigger: "#oe_snippets we-button[data-name='onclick_opt']",
in_modal: false,
run() {
// The clipboard cannot be accessed from a script.
// https://w3c.github.io/editing/docs/execCommand/#dfn-the-copy-command
// The execCommand is patched for that step so that ClipboardJS still
// sends the "success" event.
const oldExecCommand = document.execCommand;
document.execCommand = () => true;
this.$anchor[0].click();
document.execCommand = oldExecCommand;
}
},
{
content: "Check the copied anchor from the notification toast",
trigger: ".o_notification_manager .o_notification_content",
run() {
const notificationContent = this.$anchor[0].innerText;
const anchor = notificationContent.substring(notificationContent.indexOf("#"));
if (anchor !== "#Win-%2420") {
console.error("The popup anchor is not '#Win-%2420' as expected.");
}
},
},
wTourUtils.clickOnElement("button to close the popup", "iframe .s_popup_close"),
wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"),
{
content: "Paste the popup anchor in the URL input",
trigger: "#o_link_dialog_url_input",
run: "text #Win-%2420"
},
...wTourUtils.clickOnSave(),
wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"),
{
content: "Verify that the popup opens after clicked the button.",
in_modal: false,
trigger: "iframe .s_popup .modal[id='Win-%2420'].show",
},
wTourUtils.clickOnElement("button to close the popup", "iframe .s_popup_close"),
{
content: "Go to the 'contactus' page.",
trigger: "iframe a[href='/contactus']",
},
{
content: "wait for the page to be loaded",
trigger: ".o_website_preview[data-view-xmlid='website.contactus']",
run: () => null, // it"s a check
},
...wTourUtils.clickOnEditAndWaitEditMode(),
wTourUtils.dragNDrop({id: "s_text_image", name: "Image - Text"}),
wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"),
{
content: "Add a link to the homepage popup in the URL input",
trigger: "#o_link_dialog_url_input",
run: "text /#Win-%2420"
},
...wTourUtils.clickOnSave(),
wTourUtils.clickOnElement("text image snippet button", "iframe .s_text_image .btn-primary"),
{
content: "Verify that the popup opens when the homepage page loads.",
in_modal: false,
extra_trigger: ".o_website_preview[data-view-xmlid='website.homepage']",
trigger: "iframe .s_popup .modal[id='Win-%2420'].show",
},
]);
+3
View File
@@ -74,3 +74,6 @@ class TestSnippets(HttpCase):
def test_10_parallax(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'test_parallax', login='admin')
def test_11_snippet_popup_display_on_click(self):
self.start_tour(self.env['website'].get_client_action_url('/'), 'snippet_popup_display_on_click', login='admin')
+2 -1
View File
@@ -53,10 +53,11 @@
<we-select string="Display" data-attribute-name="display" data-attribute-default-value="always">
<we-button data-select-data-attribute="afterDelay" data-name="show_delay">Delay</we-button>
<we-button data-select-data-attribute="mouseExit">On Exit</we-button>
<we-button data-select-data-attribute="onClick" data-name="onclick_opt">On Click (via link)</we-button>
</we-select>
<we-input string="&#8985; Delay" title="Automatically opens the pop-up if the user stays on a page longer than the specified time." data-select-data-attribute="" data-attribute-name="showAfter" data-unit="s" data-save-unit="ms" data-dependencies="show_delay"/>
<t t-set="unit_popup_duration">days</t>
<we-input string="Hide For" title="Once the user closes the popup, it won't be shown again for that period of time." t-attf-data-select-data-attribute="7#{unit_popup_duration}" data-attribute-name="consentsDuration" t-att-data-unit="unit_popup_duration"/>
<we-input string="Hide For" title="Once the user closes the popup, it won't be shown again for that period of time." t-attf-data-select-data-attribute="7#{unit_popup_duration}" data-attribute-name="consentsDuration" t-att-data-unit="unit_popup_duration" data-dependencies="!onclick_opt"/>
<we-select string="Show on" data-no-preview="true">
<we-button data-move-block="currentPage">This page</we-button>
<we-button data-move-block="allPages">All pages</we-button>
@@ -1120,6 +1120,11 @@
data-no-preview="true"/>
</div>
<div data-js="anchor"
data-selector=".s_popup"
data-target=".modal">
</div>
<!-- Mega Menu settings -->
<div data-js="MegaMenuLayout" data-selector=".o_mega_menu">
<we-select string="Template" data-name="mega_menu_template_opt">