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/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..540b085c1 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -1,7 +1,7 @@ import UpdateCart from "../Helper/UpdateCart"; import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler"; -import {hide, show, setVisible} from "../Helper/Hiding"; -import ButtonsToggleListener from "../Helper/ButtonsToggleListener"; +import {hide, show} from "../Helper/Hiding"; +import {disable, enable} from "../Helper/ButtonDisabler"; class SingleProductBootstap { constructor(gateway, renderer, messages, errorHandler) { @@ -10,56 +10,93 @@ class SingleProductBootstap { this.messages = messages; 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() { + return document.querySelector(this.formSelector); + } handleChange() { - const shouldRender = this.shouldRender(); - setVisible(this.gateway.button.wrapper, shouldRender); - setVisible(this.gateway.messages.wrapper, shouldRender); - if (!shouldRender) { - return; - } - - this.render(); - } - - init() { - const form = document.querySelector('form.cart'); - if (!form) { - return; - } - - form.addEventListener('change', this.handleChange.bind(this)); - 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(); - if (!this.shouldRender()) { - hide(this.gateway.button.wrapper); + this.renderer.disableSmartButtons(); + hide(this.gateway.button.wrapper, this.formSelector); hide(this.gateway.messages.wrapper); return; } this.render(); + + this.renderer.enableSmartButtons(); + show(this.gateway.button.wrapper); + show(this.gateway.messages.wrapper); + + this.handleButtonStatus(); + } + + 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); + } + + init() { + const form = this.form(); + + if (!form) { + return; + } + + 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'); + + if (addToCartButton) { + (new MutationObserver(this.handleButtonStatus.bind(this))) + .observe(addToCartButton, { attributes : true }); + } + + if (!this.shouldRender()) { + return; + } + + this.render(); + this.handleChange(); } shouldRender() { - return document.querySelector('form.cart') !== null + return this.form() !== null + && !this.isWcsattSubscriptionMode(); + } + + 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 +130,7 @@ class SingleProductBootstap { return !price || price === 0; } - isSubscriptionMode() { + 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 @@ -106,7 +143,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..9e4bf51ee --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js @@ -0,0 +1,53 @@ +/** + * @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': '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; diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js index a07e81d21..80c90c0ef 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,38 +10,58 @@ 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.optionsEqual(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); }); } renderWithAmount(amount) { - if (! this.shouldRender()) { return; } + const options = { + amount, + placement: this.config.placement, + style: this.config.style + }; + + if (this.optionsEqual(options)) { + 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)); + const oldWrapper = document.querySelector(this.config.wrapper); + const sibling = oldWrapper.nextSibling; + oldWrapper.parentElement.removeChild(oldWrapper); sibling.parentElement.insertBefore(newWrapper, sibling); - paypal.Messages({ - amount, - placement: this.config.placement, - style: this.config.style - }).render(this.config.wrapper); + + paypal.Messages(options).render(this.config.wrapper); + } + + optionsEqual(options) { + const fingerprint = JSON.stringify(options); + + if (this.optionsFingerprint === fingerprint) { + return true; + } + + this.optionsFingerprint = fingerprint; + return false; } shouldRender() { 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;