[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:
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]);
|
||||
@@ -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')
|
||||
|
||||
@@ -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="⌙ 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">
|
||||
|
||||
Reference in New Issue
Block a user