From 587e065fba83e0270f359ea36797974101b9fd53 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 23 Jun 2023 08:23:11 +0100 Subject: [PATCH 1/4] Refactored button display logic Replaced show / hide buttons mode with enable / disable buttons mode --- .../SingleProductActionHandler.js | 1 - .../ContextBootstrap/SingleProductBootstap.js | 67 +++++++++++-------- .../js/modules/Helper/ButtonDisabler.js | 58 ++++++++++++++++ .../modules/Helper/ButtonsToggleListener.js | 39 ----------- 4 files changed, 98 insertions(+), 67 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js delete mode 100644 modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index 3c3371542..146fd7e94 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -1,4 +1,3 @@ -import ButtonsToggleListener from '../Helper/ButtonsToggleListener'; import Product from '../Entity/Product'; import onApprove from '../OnApproveHandler/onApproveForContinue'; import {payerData} from "../Helper/PayerData"; diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index 376962772..c6b86657c 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -1,7 +1,6 @@ import UpdateCart from "../Helper/UpdateCart"; import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler"; -import {hide, show, setVisible} from "../Helper/Hiding"; -import ButtonsToggleListener from "../Helper/ButtonsToggleListener"; +import {disable, enable} from "../Helper/ButtonDisabler"; class SingleProductBootstap { constructor(gateway, renderer, messages, errorHandler) { @@ -10,56 +9,70 @@ class SingleProductBootstap { this.messages = messages; this.errorHandler = errorHandler; this.mutationObserver = new MutationObserver(this.handleChange.bind(this)); + this.formSelector = 'form.cart'; } + form() { + return document.querySelector(this.formSelector); + } handleChange() { - const shouldRender = this.shouldRender(); - setVisible(this.gateway.button.wrapper, shouldRender); - setVisible(this.gateway.messages.wrapper, shouldRender); - if (!shouldRender) { + if (!this.shouldRender()) { return; } this.render(); + this.handleButtonStatus(); + } + + handleButtonStatus() { + if (!this.shouldEnable()) { + disable(this.gateway.button.wrapper, this.formSelector); + disable(this.gateway.messages.wrapper); + return; + } + enable(this.gateway.button.wrapper); + enable(this.gateway.messages.wrapper); + this.messages.renderWithAmount(this.priceAmount()) } init() { - const form = document.querySelector('form.cart'); + const form = this.form(); + if (!form) { return; } form.addEventListener('change', this.handleChange.bind(this)); - this.mutationObserver.observe(form, {childList: true, subtree: true}); + this.mutationObserver.observe(form, { childList: true, subtree: true }); - const buttonObserver = new ButtonsToggleListener( - form.querySelector('.single_add_to_cart_button'), - () => { - show(this.gateway.button.wrapper); - show(this.gateway.messages.wrapper); - this.messages.renderWithAmount(this.priceAmount()) - }, - () => { - hide(this.gateway.button.wrapper); - hide(this.gateway.messages.wrapper); - }, - ); - buttonObserver.init(); + const addToCartButton = form.querySelector('.single_add_to_cart_button'); + + if (addToCartButton) { + (new MutationObserver(this.handleButtonStatus.bind(this))) + .observe(addToCartButton, { attributes : true }); + } if (!this.shouldRender()) { - hide(this.gateway.button.wrapper); - hide(this.gateway.messages.wrapper); return; } this.render(); + this.handleButtonStatus(); } shouldRender() { - return document.querySelector('form.cart') !== null + return this.form() !== null + && !this.isDisabledReasonExternalPlugins(); + } + + shouldEnable() { + const form = this.form(); + const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null; + + return this.shouldRender() && !this.priceAmountIsZero() - && !this.isSubscriptionMode(); + && ((null === addToCartButton) || !addToCartButton.classList.contains('disabled')); } priceAmount() { @@ -93,7 +106,7 @@ class SingleProductBootstap { return !price || price === 0; } - isSubscriptionMode() { + isDisabledReasonExternalPlugins() { // Check "All products for subscriptions" plugin. return document.querySelector('.wcsatt-options-product:not(.wcsatt-options-product--hidden) .subscription-option input[type="radio"]:checked') !== null || document.querySelector('.wcsatt-options-prompt-label-subscription input[type="radio"]:checked') !== null; // grouped @@ -106,7 +119,7 @@ class SingleProductBootstap { this.gateway.ajax.change_cart.endpoint, this.gateway.ajax.change_cart.nonce, ), - document.querySelector('form.cart'), + this.form(), this.errorHandler, ); diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js new file mode 100644 index 000000000..b9f98cbab --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js @@ -0,0 +1,58 @@ +/** + * @param selectorOrElement + * @returns {Element} + */ +const getElement = (selectorOrElement) => { + if (typeof selectorOrElement === 'string') { + return document.querySelector(selectorOrElement); + } + return selectorOrElement; +} + +export const setEnabled = (selectorOrElement, enable, form = null) => { + const element = getElement(selectorOrElement); + + if (!element) { + return; + } + + if (enable) { + jQuery(element).css({ + 'cursor': '', + '-webkit-filter': '', + 'filter': '', + } ) + .off('mouseup') + .find('> *') + .css('pointer-events', ''); + } else { + jQuery(element).css( { + 'cursor': '', + '-webkit-filter': '', + 'filter': '', + } ) + .css({ + 'cursor': 'not-allowed', + '-webkit-filter': 'grayscale(100%)', + 'filter': 'grayscale(100%)', + }) + .on('mouseup', function(event) { + event.stopImmediatePropagation(); + + if (form) { + // Trigger form submit to show the error message + jQuery(form).find(':submit').trigger('click'); + } + }) + .find('> *') + .css('pointer-events', 'none'); + } +}; + +export const disable = (selectorOrElement, form = null) => { + setEnabled(selectorOrElement, false, form); +}; + +export const enable = (selectorOrElement) => { + setEnabled(selectorOrElement, true); +}; diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js deleted file mode 100644 index add1ee28a..000000000 --- a/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * When you can't add something to the cart, the PayPal buttons should not show. - * Therefore we listen for changes on the add to cart button and show/hide the buttons accordingly. - */ - -class ButtonsToggleListener { - constructor(element, showCallback, hideCallback) - { - this.element = element; - this.showCallback = showCallback; - this.hideCallback = hideCallback; - this.observer = null; - } - - init() - { - if (!this.element) { - return; - } - const config = { attributes : true }; - const callback = () => { - if (this.element.classList.contains('disabled')) { - this.hideCallback(); - return; - } - this.showCallback(); - } - this.observer = new MutationObserver(callback); - this.observer.observe(this.element, config); - callback(); - } - - disconnect() - { - this.observer.disconnect(); - } -} - -export default ButtonsToggleListener; From 75bf98c1748459ba821884b68b1627743aa68766 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 23 Jun 2023 15:49:08 +0100 Subject: [PATCH 2/4] Add hide / show conditions on SingleProduct Buttons for when they shouldn't be rendered. Refactor MessageRenderer not to reload when it has no changes. --- .../ContextBootstrap/SingleProductBootstap.js | 7 +++ .../js/modules/Renderer/MessageRenderer.js | 52 +++++++++++++------ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index c6b86657c..8d6f61cb1 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -1,5 +1,6 @@ import UpdateCart from "../Helper/UpdateCart"; import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler"; +import {hide, show} from "../Helper/Hiding"; import {disable, enable} from "../Helper/ButtonDisabler"; class SingleProductBootstap { @@ -18,10 +19,16 @@ class SingleProductBootstap { handleChange() { if (!this.shouldRender()) { + hide(this.gateway.button.wrapper, this.formSelector); + hide(this.gateway.messages.wrapper); return; } this.render(); + + show(this.gateway.button.wrapper, this.formSelector); + show(this.gateway.messages.wrapper); + this.handleButtonStatus(); } diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js index a07e81d21..82c10769f 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js @@ -2,6 +2,7 @@ class MessageRenderer { constructor(config) { this.config = config; + this.optionsFingerprint = null; } render() { @@ -9,18 +10,20 @@ class MessageRenderer { return; } - paypal.Messages({ + const options = { amount: this.config.amount, placement: this.config.placement, style: this.config.style - }).render(this.config.wrapper); + }; + + if (this.isOptionsFingerprintEqual(options)) { + return; + } + + paypal.Messages(options).render(this.config.wrapper); jQuery(document.body).on('updated_cart_totals', () => { - paypal.Messages({ - amount: this.config.amount, - placement: this.config.placement, - style: this.config.style - }).render(this.config.wrapper); + paypal.Messages(options).render(this.config.wrapper); }); } @@ -30,17 +33,36 @@ class MessageRenderer { return; } - const newWrapper = document.createElement('div'); - newWrapper.setAttribute('id', this.config.wrapper.replace('#', '')); - - const sibling = document.querySelector(this.config.wrapper).nextSibling; - document.querySelector(this.config.wrapper).parentElement.removeChild(document.querySelector(this.config.wrapper)); - sibling.parentElement.insertBefore(newWrapper, sibling); - paypal.Messages({ + const options = { amount, placement: this.config.placement, style: this.config.style - }).render(this.config.wrapper); + }; + + if (this.isOptionsFingerprintEqual(options)) { + return; + } + + const newWrapper = document.createElement('div'); + newWrapper.setAttribute('id', this.config.wrapper.replace('#', '')); + + const oldWrapper = document.querySelector(this.config.wrapper); + const sibling = oldWrapper.nextSibling; + oldWrapper.parentElement.removeChild(oldWrapper); + sibling.parentElement.insertBefore(newWrapper, sibling); + + paypal.Messages(options).render(this.config.wrapper); + } + + isOptionsFingerprintEqual(options) { + const fingerprint = JSON.stringify(options); + + if (this.optionsFingerprint === fingerprint) { + return true; + } + + this.optionsFingerprint = fingerprint; + return false; } shouldRender() { From adf7a2e2977e8fc55e810c4e217f4b56d57a4d7a Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 26 Jun 2023 11:55:30 +0100 Subject: [PATCH 3/4] Remove unnecessary code --- .../resources/js/modules/Helper/ButtonDisabler.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js index b9f98cbab..9e4bf51ee 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js +++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js @@ -18,20 +18,15 @@ export const setEnabled = (selectorOrElement, enable, form = null) => { if (enable) { jQuery(element).css({ - 'cursor': '', - '-webkit-filter': '', - 'filter': '', - } ) + 'cursor': '', + '-webkit-filter': '', + 'filter': '', + } ) .off('mouseup') .find('> *') .css('pointer-events', ''); } else { - jQuery(element).css( { - 'cursor': '', - '-webkit-filter': '', - 'filter': '', - } ) - .css({ + jQuery(element).css({ 'cursor': 'not-allowed', '-webkit-filter': 'grayscale(100%)', 'filter': 'grayscale(100%)', From 82828c299123fe7f40421deb9a356cae6b7ea545 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 26 Jun 2023 18:14:41 +0100 Subject: [PATCH 4/4] Add support for PayPal SmartButtons enable / disable Rename functions Remove invalid function arguments --- modules/ppcp-button/resources/js/button.js | 18 ++++++++++-- .../ContextBootstrap/SingleProductBootstap.js | 29 +++++++++++++++---- .../js/modules/Renderer/MessageRenderer.js | 7 ++--- .../resources/js/modules/Renderer/Renderer.js | 17 ++++++++++- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 6708b0275..0fbccd095 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -121,10 +121,22 @@ const bootstrap = () => { return actions.reject(); } }; - const onSmartButtonsInit = () => { - buttonsSpinner.unblock(); + + let smartButtonsOptions = { + onInit: null, + init: function (actions) { + this.actions = actions; + if (typeof this.onInit === 'function') { + this.onInit(); + } + } }; - const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit); + + const onSmartButtonsInit = (data, actions) => { + buttonsSpinner.unblock(); + smartButtonsOptions.init(actions); + }; + const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit, smartButtonsOptions); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const context = PayPalCommerceGateway.context; if (context === 'mini-cart' || context === 'product') { diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index 8d6f61cb1..540b085c1 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -11,6 +11,12 @@ class SingleProductBootstap { this.errorHandler = errorHandler; this.mutationObserver = new MutationObserver(this.handleChange.bind(this)); this.formSelector = 'form.cart'; + + if (this.renderer && this.renderer.smartButtonsOptions) { + this.renderer.smartButtonsOptions.onInit = () => { + this.handleChange(); + }; + } } form() { @@ -19,6 +25,7 @@ class SingleProductBootstap { handleChange() { if (!this.shouldRender()) { + this.renderer.disableSmartButtons(); hide(this.gateway.button.wrapper, this.formSelector); hide(this.gateway.messages.wrapper); return; @@ -26,7 +33,8 @@ class SingleProductBootstap { this.render(); - show(this.gateway.button.wrapper, this.formSelector); + this.renderer.enableSmartButtons(); + show(this.gateway.button.wrapper); show(this.gateway.messages.wrapper); this.handleButtonStatus(); @@ -34,13 +42,14 @@ class SingleProductBootstap { handleButtonStatus() { if (!this.shouldEnable()) { + this.renderer.disableSmartButtons(); disable(this.gateway.button.wrapper, this.formSelector); disable(this.gateway.messages.wrapper); return; } + this.renderer.enableSmartButtons(); enable(this.gateway.button.wrapper); enable(this.gateway.messages.wrapper); - this.messages.renderWithAmount(this.priceAmount()) } init() { @@ -50,7 +59,15 @@ class SingleProductBootstap { return; } - form.addEventListener('change', this.handleChange.bind(this)); + form.addEventListener('change', () => { + this.handleChange(); + + setTimeout(() => { // Wait for the DOM to be fully updated + // For the moment renderWithAmount should only be done here to prevent undesired side effects due to priceAmount() + // not being correctly formatted in some cases, can be moved to handleButtonStatus() once this issue is fixed + this.messages.renderWithAmount(this.priceAmount()); + }, 100); + }); this.mutationObserver.observe(form, { childList: true, subtree: true }); const addToCartButton = form.querySelector('.single_add_to_cart_button'); @@ -65,12 +82,12 @@ class SingleProductBootstap { } this.render(); - this.handleButtonStatus(); + this.handleChange(); } shouldRender() { return this.form() !== null - && !this.isDisabledReasonExternalPlugins(); + && !this.isWcsattSubscriptionMode(); } shouldEnable() { @@ -113,7 +130,7 @@ class SingleProductBootstap { return !price || price === 0; } - isDisabledReasonExternalPlugins() { + isWcsattSubscriptionMode() { // Check "All products for subscriptions" plugin. return document.querySelector('.wcsatt-options-product:not(.wcsatt-options-product--hidden) .subscription-option input[type="radio"]:checked') !== null || document.querySelector('.wcsatt-options-prompt-label-subscription input[type="radio"]:checked') !== null; // grouped diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js index 82c10769f..80c90c0ef 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js @@ -16,7 +16,7 @@ class MessageRenderer { style: this.config.style }; - if (this.isOptionsFingerprintEqual(options)) { + if (this.optionsEqual(options)) { return; } @@ -28,7 +28,6 @@ class MessageRenderer { } renderWithAmount(amount) { - if (! this.shouldRender()) { return; } @@ -39,7 +38,7 @@ class MessageRenderer { style: this.config.style }; - if (this.isOptionsFingerprintEqual(options)) { + if (this.optionsEqual(options)) { return; } @@ -54,7 +53,7 @@ class MessageRenderer { paypal.Messages(options).render(this.config.wrapper); } - isOptionsFingerprintEqual(options) { + optionsEqual(options) { const fingerprint = JSON.stringify(options); if (this.optionsFingerprint === fingerprint) { diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 17f58aff3..efd3c0bfa 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -1,11 +1,12 @@ import merge from "deepmerge"; class Renderer { - constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) { + constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit, smartButtonsOptions) { this.defaultSettings = defaultSettings; this.creditCardRenderer = creditCardRenderer; this.onSmartButtonClick = onSmartButtonClick; this.onSmartButtonsInit = onSmartButtonsInit; + this.smartButtonsOptions = smartButtonsOptions; this.renderedSources = new Set(); } @@ -106,6 +107,20 @@ class Renderer { enableCreditCardFields() { this.creditCardRenderer.enableFields(); } + + disableSmartButtons() { + if (!this.smartButtonsOptions || !this.smartButtonsOptions.actions) { + return; + } + this.smartButtonsOptions.actions.disable(); + } + + enableSmartButtons() { + if (!this.smartButtonsOptions || !this.smartButtonsOptions.actions) { + return; + } + this.smartButtonsOptions.actions.enable(); + } } export default Renderer;