From 162e6d47fc13996d18bf19ccc372d1688836858f Mon Sep 17 00:00:00 2001 From: Oliver Bartsch <bo@cedev.de> Date: Sat, 21 Aug 2021 02:43:04 +0200 Subject: [PATCH] [TASK] Allow components to manually change checkbox state Components, using the multi record selection, introduced in #94906, usually do not have to change any checkbox state manually. The multi record selection component is taking care of it. However, there might be cases when a consuming component needs to change checkbox state on its own. An example is the workspaces module, which may changes checkbox state of child records when their parent record got changed. See: #94404. To prevent the components from overriding each other, the multi record selection now respects a special data attribute `manually-changed`. If set on a checkbox element, the multi record selection component will not touch this checkbox, while executing its (mass) actions. Resolves: #94948 Releases: master Change-Id: I35b07ca45cab4b96d3eea93ff45426702a1532d7 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70713 Tested-by: core-ci <typo3@b13.com> Tested-by: Jochen <rothjochen@gmail.com> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Jochen <rothjochen@gmail.com> Reviewed-by: Benni Mack <benni@typo3.org> --- .../Public/TypeScript/MultiRecordSelection.ts | 27 +++++++++++++++++-- .../Public/JavaScript/MultiRecordSelection.js | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts index 033ab846143a..a636382a3554 100644 --- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts +++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts @@ -61,8 +61,8 @@ class MultiRecordSelection { } private static changeCheckboxState(checkbox: HTMLInputElement, check: boolean): void { - if (checkbox.checked === check) { - // Return if state did not change + if (checkbox.checked === check || checkbox.dataset.manuallyChanged) { + // Return in case state did not change or another component has already changed it return; } checkbox.checked = check; @@ -146,6 +146,21 @@ class MultiRecordSelection { }); } + /** + * The manually changed attribute can be set by components, using + * this module while implementing custom logic to change checkbox + * state. To not cancel each others action, all actions in this + * module respect this attribute before changing checkbox state. + * Therefore, this method is called prior to every action in + * this module, which changes checkbox states. Otherwise old + * state would may led to misbehaviour. + */ + private static unsetManuallyChangedAttribute(): void { + MultiRecordSelection.getCheckboxes().forEach((checkbox: HTMLInputElement): void => { + checkbox.removeAttribute('data-manually-changed'); + }); + } + constructor() { DocumentService.ready().then((): void => { MultiRecordSelection.restoreTemporaryState(); @@ -229,6 +244,11 @@ class MultiRecordSelection { return; } + // Unset manually changed attribute so we can be sure, in case this is + // set on a checkbox, while executing the requested action, the checkbox + // was already changed by another component. + MultiRecordSelection.unsetManuallyChangedAttribute(); + // Perform requested action switch (target.dataset.multiRecordSelectionCheckAction) { case CheckboxActions.checkAll: @@ -250,6 +270,9 @@ class MultiRecordSelection { // Unknown action Notification.warning('Unknown checkbox action'); } + + // To prevent possible side effects we simply clean up and unset the attribute here again + MultiRecordSelection.unsetManuallyChangedAttribute(); }).delegateTo(document, [Selectors.checkboxActionsSelector, Buttons.checkboxActionButton].join(' ')); } diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js b/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js index 96d85fda4332..689be3c37b15 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -define(["require","exports","TYPO3/CMS/Backend/Notification","TYPO3/CMS/Core/DocumentService","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,c,o,n){"use strict";var i,s,a,l,r;!function(e){e.actionsSelector=".t3js-multi-record-selection-actions",e.checkboxSelector=".t3js-multi-record-selection-check",e.checkboxActionsSelector="#multi-record-selection-check-actions"}(i||(i={})),function(e){e.actionButton="button[data-multi-record-selection-action]",e.checkboxActionButton="button[data-multi-record-selection-check-action]",e.checkboxActionsToggleButton='button[data-bs-target="multi-record-selection-check-actions"]'}(s||(s={})),function(e){e.edit="edit"}(a||(a={})),function(e){e.checkAll="check-all",e.checkNone="check-none",e.toggle="toggle"}(l||(l={})),function(e){e.any="",e.checked=":checked",e.unchecked=":not(:checked)"}(r||(r={}));class d{static getCheckboxes(e=r.any){return document.querySelectorAll(i.checkboxSelector+e)}static changeCheckboxState(e,t){e.checked!==t&&(e.checked=t,e.dispatchEvent(new Event("checkbox:state:changed",{bubbles:!0,cancelable:!1})))}static getReturnUrl(e){return""===e&&(e=top.list_frame.document.location.pathname+top.list_frame.document.location.search),encodeURIComponent(e)}static restoreTemporaryState(){const e=d.getCheckboxes(r.checked);if(!e.length)return;e.forEach(e=>{e.closest("tr").classList.add("success")});const t=document.querySelectorAll(i.actionsSelector);t.length&&t.forEach(e=>e.classList.remove("hidden"))}static toggleActionsState(){const e=document.querySelectorAll(i.actionsSelector);if(!e.length)return;if(!d.getCheckboxes(r.checked).length)return void e.forEach(e=>e.classList.add("hidden"));e.forEach(e=>e.classList.remove("hidden"));const t=document.querySelectorAll([i.actionsSelector,s.actionButton].join(" "));t.length&&t.forEach(e=>{if(!e.dataset.multiRecordSelectionActionConfig)return;const t=JSON.parse(e.dataset.multiRecordSelectionActionConfig);if(!t.idField)return;e.classList.add("disabled");const c=d.getCheckboxes(r.checked);for(let o=0;o<c.length;o++)if(c[o].closest("tr").dataset[t.idField]){e.classList.remove("disabled");break}})}constructor(){o.ready().then(()=>{d.restoreTemporaryState(),this.registerActions(),this.registerActionsEventHandlers(),this.registerCheckboxActions(),this.registerToggleCheckboxActions(),this.registerDispatchCheckboxStateChangedEvent(),this.registerCheckboxStateChangedEventHandler()})}registerActions(){new n("click",(e,t)=>{const o=d.getCheckboxes(r.checked);if(t.dataset.multiRecordSelectionAction&&o.length)switch(t.dataset.multiRecordSelectionAction){case a.edit:e.preventDefault();const n=JSON.parse(t.dataset.multiRecordSelectionActionConfig||"");if(!n||!n.idField||!n.table)break;const i=[];o.forEach(e=>{const t=e.closest("tr");null!==t&&t.dataset[n.idField]&&i.push(t.dataset[n.idField])}),i.length?window.location.href=top.TYPO3.settings.FormEngine.moduleUrl+"&edit["+n.table+"]["+i.join(",")+"]=edit&returnUrl="+d.getReturnUrl(n.returnUrl||""):c.warning("The selected elements can not be edited.");break;default:t.dispatchEvent(new Event("multiRecordSelection:action:"+t.dataset.multiRecordSelectionAction,{bubbles:!0,cancelable:!1}))}}).delegateTo(document,[i.actionsSelector,s.actionButton].join(" ")),d.toggleActionsState()}registerActionsEventHandlers(){new n("multiRecordSelection:actions:show",()=>{const e=document.querySelectorAll(i.actionsSelector);e&&e.forEach(e=>e.classList.remove("hidden"))}).bindTo(document),new n("multiRecordSelection:actions:hide",()=>{const e=document.querySelectorAll(i.actionsSelector);e&&e.forEach(e=>e.classList.add("hidden"))}).bindTo(document)}registerCheckboxActions(){new n("click",(e,t)=>{e.preventDefault();const o=d.getCheckboxes();if(t.dataset.multiRecordSelectionCheckAction&&o.length)switch(t.dataset.multiRecordSelectionCheckAction){case l.checkAll:o.forEach(e=>{d.changeCheckboxState(e,!0)});break;case l.checkNone:o.forEach(e=>{d.changeCheckboxState(e,!1)});break;case l.toggle:o.forEach(e=>{d.changeCheckboxState(e,!e.checked)});break;default:c.warning("Unknown checkbox action")}}).delegateTo(document,[i.checkboxActionsSelector,s.checkboxActionButton].join(" "))}registerDispatchCheckboxStateChangedEvent(){new n("change",(e,t)=>{t.dispatchEvent(new Event("checkbox:state:changed",{bubbles:!0,cancelable:!1}))}).delegateTo(document,i.checkboxSelector)}registerCheckboxStateChangedEventHandler(){new n("checkbox:state:changed",e=>{const t=e.target;t.checked?t.closest("tr").classList.add("success"):t.closest("tr").classList.remove("success"),d.toggleActionsState()}).bindTo(document)}registerToggleCheckboxActions(){new n("click",()=>{const e=document.querySelector('button[data-multi-record-selection-check-action="'+l.checkAll+'"]');null!==e&&e.classList.toggle("disabled",!d.getCheckboxes(r.unchecked).length);const t=document.querySelector('button[data-multi-record-selection-check-action="'+l.checkNone+'"]');null!==t&&t.classList.toggle("disabled",!d.getCheckboxes(r.checked).length)}).delegateTo(document,s.checkboxActionsToggleButton)}}return new d})); \ No newline at end of file +define(["require","exports","TYPO3/CMS/Backend/Notification","TYPO3/CMS/Core/DocumentService","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,c,o,n){"use strict";var i,a,s,l,r;!function(e){e.actionsSelector=".t3js-multi-record-selection-actions",e.checkboxSelector=".t3js-multi-record-selection-check",e.checkboxActionsSelector="#multi-record-selection-check-actions"}(i||(i={})),function(e){e.actionButton="button[data-multi-record-selection-action]",e.checkboxActionButton="button[data-multi-record-selection-check-action]",e.checkboxActionsToggleButton='button[data-bs-target="multi-record-selection-check-actions"]'}(a||(a={})),function(e){e.edit="edit"}(s||(s={})),function(e){e.checkAll="check-all",e.checkNone="check-none",e.toggle="toggle"}(l||(l={})),function(e){e.any="",e.checked=":checked",e.unchecked=":not(:checked)"}(r||(r={}));class d{static getCheckboxes(e=r.any){return document.querySelectorAll(i.checkboxSelector+e)}static changeCheckboxState(e,t){e.checked===t||e.dataset.manuallyChanged||(e.checked=t,e.dispatchEvent(new Event("checkbox:state:changed",{bubbles:!0,cancelable:!1})))}static getReturnUrl(e){return""===e&&(e=top.list_frame.document.location.pathname+top.list_frame.document.location.search),encodeURIComponent(e)}static restoreTemporaryState(){const e=d.getCheckboxes(r.checked);if(!e.length)return;e.forEach(e=>{e.closest("tr").classList.add("success")});const t=document.querySelectorAll(i.actionsSelector);t.length&&t.forEach(e=>e.classList.remove("hidden"))}static toggleActionsState(){const e=document.querySelectorAll(i.actionsSelector);if(!e.length)return;if(!d.getCheckboxes(r.checked).length)return void e.forEach(e=>e.classList.add("hidden"));e.forEach(e=>e.classList.remove("hidden"));const t=document.querySelectorAll([i.actionsSelector,a.actionButton].join(" "));t.length&&t.forEach(e=>{if(!e.dataset.multiRecordSelectionActionConfig)return;const t=JSON.parse(e.dataset.multiRecordSelectionActionConfig);if(!t.idField)return;e.classList.add("disabled");const c=d.getCheckboxes(r.checked);for(let o=0;o<c.length;o++)if(c[o].closest("tr").dataset[t.idField]){e.classList.remove("disabled");break}})}static unsetManuallyChangedAttribute(){d.getCheckboxes().forEach(e=>{e.removeAttribute("data-manually-changed")})}constructor(){o.ready().then(()=>{d.restoreTemporaryState(),this.registerActions(),this.registerActionsEventHandlers(),this.registerCheckboxActions(),this.registerToggleCheckboxActions(),this.registerDispatchCheckboxStateChangedEvent(),this.registerCheckboxStateChangedEventHandler()})}registerActions(){new n("click",(e,t)=>{const o=d.getCheckboxes(r.checked);if(t.dataset.multiRecordSelectionAction&&o.length)switch(t.dataset.multiRecordSelectionAction){case s.edit:e.preventDefault();const n=JSON.parse(t.dataset.multiRecordSelectionActionConfig||"");if(!n||!n.idField||!n.table)break;const i=[];o.forEach(e=>{const t=e.closest("tr");null!==t&&t.dataset[n.idField]&&i.push(t.dataset[n.idField])}),i.length?window.location.href=top.TYPO3.settings.FormEngine.moduleUrl+"&edit["+n.table+"]["+i.join(",")+"]=edit&returnUrl="+d.getReturnUrl(n.returnUrl||""):c.warning("The selected elements can not be edited.");break;default:t.dispatchEvent(new Event("multiRecordSelection:action:"+t.dataset.multiRecordSelectionAction,{bubbles:!0,cancelable:!1}))}}).delegateTo(document,[i.actionsSelector,a.actionButton].join(" ")),d.toggleActionsState()}registerActionsEventHandlers(){new n("multiRecordSelection:actions:show",()=>{const e=document.querySelectorAll(i.actionsSelector);e&&e.forEach(e=>e.classList.remove("hidden"))}).bindTo(document),new n("multiRecordSelection:actions:hide",()=>{const e=document.querySelectorAll(i.actionsSelector);e&&e.forEach(e=>e.classList.add("hidden"))}).bindTo(document)}registerCheckboxActions(){new n("click",(e,t)=>{e.preventDefault();const o=d.getCheckboxes();if(t.dataset.multiRecordSelectionCheckAction&&o.length){switch(d.unsetManuallyChangedAttribute(),t.dataset.multiRecordSelectionCheckAction){case l.checkAll:o.forEach(e=>{d.changeCheckboxState(e,!0)});break;case l.checkNone:o.forEach(e=>{d.changeCheckboxState(e,!1)});break;case l.toggle:o.forEach(e=>{d.changeCheckboxState(e,!e.checked)});break;default:c.warning("Unknown checkbox action")}d.unsetManuallyChangedAttribute()}}).delegateTo(document,[i.checkboxActionsSelector,a.checkboxActionButton].join(" "))}registerDispatchCheckboxStateChangedEvent(){new n("change",(e,t)=>{t.dispatchEvent(new Event("checkbox:state:changed",{bubbles:!0,cancelable:!1}))}).delegateTo(document,i.checkboxSelector)}registerCheckboxStateChangedEventHandler(){new n("checkbox:state:changed",e=>{const t=e.target;t.checked?t.closest("tr").classList.add("success"):t.closest("tr").classList.remove("success"),d.toggleActionsState()}).bindTo(document)}registerToggleCheckboxActions(){new n("click",()=>{const e=document.querySelector('button[data-multi-record-selection-check-action="'+l.checkAll+'"]');null!==e&&e.classList.toggle("disabled",!d.getCheckboxes(r.unchecked).length);const t=document.querySelector('button[data-multi-record-selection-check-action="'+l.checkNone+'"]');null!==t&&t.classList.toggle("disabled",!d.getCheckboxes(r.checked).length)}).delegateTo(document,a.checkboxActionsToggleButton)}}return new d})); \ No newline at end of file -- GitLab