From 4468b55d2d787ba4b4e3215a225e2197b43392c1 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 18 Jan 2023 12:01:45 +0000 Subject: [PATCH] Enforce max_num on MultipleChooserPanel Enable / disable the open-modal button on reaching the limit, as we do for InlinePanel's standard add button; and when handling the response from the modal, stop adding new items when max_num is reached --- client/src/components/InlinePanel/index.js | 36 ++++++++++--------- .../components/MultipleChooserPanel/index.js | 24 +++++++++++++ .../src/entrypoints/admin/modal-workflow.js | 8 +++-- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/client/src/components/InlinePanel/index.js b/client/src/components/InlinePanel/index.js index e600e4f6f0..4418b541e7 100644 --- a/client/src/components/InlinePanel/index.js +++ b/client/src/components/InlinePanel/index.js @@ -27,6 +27,12 @@ export class InlinePanel extends ExpandingFormset { this.initChildControls(childPrefix); } + this.updateControlStates(); + } + + updateControlStates() { + /* Update states of listing controls in response to a change of state such as + adding, deleting or moving an element */ this.updateChildCount(); this.updateMoveButtonDisabledStates(); this.updateAddButtonState(); @@ -43,9 +49,7 @@ export class InlinePanel extends ExpandingFormset { /* set 'deleted' form field to true */ $('#' + deleteInputId).val('1'); currentChild.addClass('deleted').slideUp(() => { - this.updateChildCount(); - this.updateMoveButtonDisabledStates(); - this.updateAddButtonState(); + this.updateControlStates(); }); }); @@ -72,8 +76,7 @@ export class InlinePanel extends ExpandingFormset { currentChildOrderElem.val(prevChildOrder); prevChildOrderElem.val(currentChildOrder); - this.updateChildCount(); - this.updateMoveButtonDisabledStates(); + this.updateControlStates(); }); $down.on('click', () => { @@ -98,8 +101,7 @@ export class InlinePanel extends ExpandingFormset { currentChildOrderElem.val(nextChildOrder); nextChildOrderElem.val(currentChildOrder); - this.updateChildCount(); - this.updateMoveButtonDisabledStates(); + this.updateControlStates(); }); } @@ -110,9 +112,7 @@ export class InlinePanel extends ExpandingFormset { $('#' + childId) .addClass('deleted') .hide(0, () => { - this.updateChildCount(); - this.updateMoveButtonDisabledStates(); - this.updateAddButtonState(); + this.updateControlStates(); }); $('#' + childId) @@ -145,14 +145,18 @@ export class InlinePanel extends ExpandingFormset { }); } + getChildCount() { + const forms = $('> [data-inline-panel-child]', this.formsElt).not( + '.deleted', + ); + return forms.length; + } + updateAddButtonState() { if (this.opts.maxForms) { - const forms = $('> [data-inline-panel-child]', this.formsElt).not( - '.deleted', - ); const addButton = $('#' + this.opts.formsetPrefix + '-ADD'); - if (forms.length >= this.opts.maxForms) { + if (this.getChildCount() >= this.opts.maxForms) { addButton.prop('disabled', true); } else { addButton.prop('disabled', false); @@ -228,9 +232,7 @@ export class InlinePanel extends ExpandingFormset { $('#id_' + newChildPrefix + '-ORDER').val(formIndex + 1); } - this.updateChildCount(); - this.updateMoveButtonDisabledStates(); - this.updateAddButtonState(); + this.updateControlStates(); initCollapsiblePanels( document.querySelectorAll( `#inline_child_${newChildPrefix} [data-panel-toggle]`, diff --git a/client/src/components/MultipleChooserPanel/index.js b/client/src/components/MultipleChooserPanel/index.js index 292ec91a33..74ca3028fe 100644 --- a/client/src/components/MultipleChooserPanel/index.js +++ b/client/src/components/MultipleChooserPanel/index.js @@ -18,6 +18,7 @@ export class MultipleChooserPanel extends InlinePanel { this.chooserWidgetFactory.openModal( (result) => { result.forEach((item) => { + if (opts.maxForms && this.getChildCount() >= opts.maxForms) return; this.addForm(); const formIndex = this.formCount - 1; const formPrefix = `${opts.formsetPrefix}-${formIndex}`; @@ -31,4 +32,27 @@ export class MultipleChooserPanel extends InlinePanel { ); }); } + + updateOpenModalButtonState() { + if (this.opts.maxForms) { + const openModalButton = document.getElementById( + `${this.opts.formsetPrefix}-OPEN_MODAL`, + ); + if (this.getChildCount() >= this.opts.maxForms) { + // need to set the data-force-disabled attribute to override the standard modal-workflow + // behaviour of re-enabling the button after the modal closes (which potentially happens + // after this code has run) + openModalButton.setAttribute('disabled', 'true'); + openModalButton.setAttribute('data-force-disabled', 'true'); + } else { + openModalButton.removeAttribute('disabled'); + openModalButton.removeAttribute('data-force-disabled'); + } + } + } + + updateControlStates() { + super.updateControlStates(); + this.updateOpenModalButtonState(); + } } diff --git a/client/src/entrypoints/admin/modal-workflow.js b/client/src/entrypoints/admin/modal-workflow.js index 93ce7fb63a..22c072f5c7 100644 --- a/client/src/entrypoints/admin/modal-workflow.js +++ b/client/src/entrypoints/admin/modal-workflow.js @@ -55,9 +55,13 @@ function ModalWorkflow(opts) { $('body').append(self.container); self.container.modal('hide'); - // add listener - once modal is about to be hidden, re-enable the trigger + // add listener - once modal is about to be hidden, re-enable the trigger unless it's been forcibly + // disabled by adding a `data-force-disabled` attribute; this mechanism is necessary to accommodate + // response handlers that disable the trigger to prevent it from reopening self.container.on('hide.bs.modal', () => { - self.triggerElement.removeAttribute('disabled'); + if (!self.triggerElement.hasAttribute('data-force-disabled')) { + self.triggerElement.removeAttribute('disabled'); + } }); // add listener - once modal is fully hidden (closed & css transitions end) - re-focus on trigger and remove from DOM