diff --git a/Build/Sources/TypeScript/backend/ajax-data-handler.ts b/Build/Sources/TypeScript/backend/ajax-data-handler.ts index e47b38f100b7..2614ac620040 100644 --- a/Build/Sources/TypeScript/backend/ajax-data-handler.ts +++ b/Build/Sources/TypeScript/backend/ajax-data-handler.ts @@ -24,7 +24,7 @@ import Notification from './notification'; import RegularEvent from '@typo3/core/event/regular-event'; enum Identifiers { - hide = '.t3js-record-hide', + hide = 'button[data-datahandler-action="visibility"]', delete = '.t3js-record-delete', icon = '.t3js-icon', } @@ -106,23 +106,9 @@ class AjaxDataHandler { // @todo: Many extensions rely on this behavior but it's misplaced in AjaxDataHandler. Move into recordlist.ts and deprecate in v11. private initialize(): void { // HIDE/UNHIDE: click events for all action icons to hide/unhide - new RegularEvent('click', (e: Event, anchorElement: HTMLElement): void => { + new RegularEvent('click', (e: Event, element: HTMLButtonElement): void => { e.preventDefault(); - - const iconElement = anchorElement.querySelector(Identifiers.icon); - const rowElement = anchorElement.closest('tr[data-uid]'); - const params = anchorElement.dataset.params; - - // add a spinner - this._showSpinnerIcon(iconElement); - - // make the AJAX call to toggle the visibility - this.process(params).then((result: ResponseInterface): void => { - if (!result.hasErrors) { - // adjust overlay icon - this.toggleRow(rowElement); - } - }); + this.handleVisibilityToggle(element); }).delegateTo(document, Identifiers.hide); // DELETE: click events for all action icons to delete @@ -153,55 +139,80 @@ class AjaxDataHandler { }).delegateTo(document, Identifiers.delete); } - /** - * Toggle row visibility after record has been changed - */ - private toggleRow(rowElement: Element): void { - const anchorElement = rowElement.querySelector(Identifiers.hide) as HTMLElement; - const table = (anchorElement.closest('table[data-table]') as HTMLTableElement).dataset.table; - const params = anchorElement.dataset.params; - let nextParams; - let nextState; - let iconName; - - if (anchorElement.dataset.state === 'hidden') { - nextState = 'visible'; - nextParams = params.replace('=0', '=1'); - iconName = 'actions-edit-hide'; - } else { - nextState = 'hidden'; - nextParams = params.replace('=1', '=0'); - iconName = 'actions-edit-unhide'; - } - anchorElement.dataset.state = nextState; - anchorElement.dataset.params = nextParams; + private handleVisibilityToggle(element: HTMLButtonElement): void + { + const rowElement = element.closest('tr[data-uid]'); - const iconElement = anchorElement.querySelector(Identifiers.icon); - Icons.getIcon(iconName, Icons.sizes.small).then((icon: string): void => { - iconElement.replaceWith(document.createRange().createContextualFragment(icon)); - }); + // Show spinner + const iconElement = element.querySelector(Identifiers.icon); + this._showSpinnerIcon(iconElement); - // Set overlay for the record icon - const recordIcon = rowElement.querySelector('.col-icon ' + Identifiers.icon); - if (nextState === 'hidden') { - Icons.getIcon('miscellaneous-placeholder', Icons.sizes.small, 'overlay-hidden').then((icon: string): void => { - const iconFragment = document.createRange().createContextualFragment(icon); - recordIcon.append(iconFragment.querySelector('.icon-overlay')); - }); - } else { - recordIcon.querySelector('.icon-overlay').remove(); - } + // Get Settings from element + const settings = { + table: element.dataset.datahandlerTable, + uid: element.dataset.datahandlerUid, + field: element.dataset.datahandlerField, + visible: (element.dataset.datahandlerStatus === 'visible') + }; + + const params = { + data: { + [settings.table]: { + [settings.uid]: { + [settings.field]: settings.visible + ? element.dataset.datahandlerHiddenValue + : element.dataset.datahandlerVisibleValue + } + } + } + }; - const animationEvent = new RegularEvent('animationend', (): void => { - rowElement.classList.remove('record-pulse'); - animationEvent.release(); - }); - animationEvent.bindTo(rowElement); - rowElement.classList.add('record-pulse'); + // Submit Data + this.process(params).then((result: ResponseInterface): void => { + if (!result.hasErrors) { + // Inverse current state + settings.visible = !(settings.visible); + element.setAttribute('data-datahandler-status', settings.visible ? 'visible' : 'hidden'); + + const elementLabel = settings.visible + ? element.dataset.datahandlerVisibleLabel + : element.dataset.datahandlerHiddenLabel; + element.setAttribute('title', elementLabel); + + const elementIconIdentifier = settings.visible + ? element.dataset.datahandlerVisibleIcon + : element.dataset.datahandlerHiddenIcon; + const iconElement = element.querySelector(Identifiers.icon); + Icons.getIcon(elementIconIdentifier, Icons.sizes.small).then((icon: string): void => { + iconElement.replaceWith(document.createRange().createContextualFragment(icon)); + }); - if (table === 'pages') { - AjaxDataHandler.refreshPageTree(); - } + // Set overlay for the record icon + const recordIcon = rowElement.querySelector('.col-icon ' + Identifiers.icon); + if (settings.visible) { + recordIcon.querySelector('.icon-overlay').remove(); + } else { + + Icons.getIcon('miscellaneous-placeholder', Icons.sizes.small, 'overlay-hidden').then((icon: string): void => { + const iconFragment = document.createRange().createContextualFragment(icon); + recordIcon.append(iconFragment.querySelector('.icon-overlay')); + }); + } + + // Animate row + const animationEvent = new RegularEvent('animationend', (): void => { + rowElement.classList.remove('record-pulse'); + animationEvent.release(); + }); + animationEvent.bindTo(rowElement); + rowElement.classList.add('record-pulse'); + + // Refresh Pagetree + if (settings.table === 'pages') { + AjaxDataHandler.refreshPageTree(); + } + } + }); } /** diff --git a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php index 5d5285ccd952..d1892e2b0de1 100644 --- a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php +++ b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php @@ -1531,29 +1531,43 @@ public function makeControl($table, $row) if (!$permsEdit || $isDeletePlaceHolder || $this->isRecordCurrentBackendUser($table, $row)) { $hideAction = $this->spaceIcon; } else { - $hideTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($table === 'pages' ? 'Page' : ''))); - $unhideTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($table === 'pages' ? 'Page' : ''))); + $visibleTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($table === 'pages' ? 'Page' : '')); + $visibleIcon = 'actions-edit-hide'; + $visibleValue = '0'; + $hiddenTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($table === 'pages' ? 'Page' : '')); + $hiddenIcon = 'actions-edit-unhide'; + $hiddenValue = '1'; if ($row[$hiddenField] ?? false) { - $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=0'; - $hideAction = ''; + $titleLabel = $hiddenTitle; + $iconIdentifier = $hiddenIcon; + $status = 'hidden'; } else { - $params = 'data[' . $table . '][' . $rowUid . '][' . $hiddenField . ']=1'; - $hideAction = ''; + $titleLabel = $visibleTitle; + $iconIdentifier = $visibleIcon; + $status = 'visible'; } + $attributesString = GeneralUtility::implodeAttributes( + [ + 'class' => 'btn btn-default', + 'type' => 'button', + 'title' => $titleLabel, + 'data-datahandler-action' => 'visibility', + 'data-datahandler-table' => $table, + 'data-datahandler-uid' => $rowUid, + 'data-datahandler-field' => $hiddenField, + 'data-datahandler-status' => $status, + 'data-datahandler-visible-label' => $visibleTitle, + 'data-datahandler-visible-value' => $visibleValue, + 'data-datahandler-visible-icon' => $visibleIcon, + 'data-datahandler-hidden-label' => $hiddenTitle, + 'data-datahandler-hidden-value' => $hiddenValue, + 'data-datahandler-hidden-icon' => $hiddenIcon, + ], + true + ); + $hideAction = ''; } $this->addActionToCellGroup($cells, $hideAction, 'hide'); } diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ajax-data-handler.js b/typo3/sysext/backend/Resources/Public/JavaScript/ajax-data-handler.js index 9cea0d2ecf95..38493eb72b36 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/ajax-data-handler.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/ajax-data-handler.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import{BroadcastMessage}from"@typo3/backend/broadcast-message.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import DocumentService from"@typo3/core/document-service.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import BroadcastService from"@typo3/backend/broadcast-service.js";import Icons from"@typo3/backend/icons.js";import Modal from"@typo3/backend/modal.js";import Notification from"@typo3/backend/notification.js";import RegularEvent from"@typo3/core/event/regular-event.js";var Identifiers;!function(e){e.hide=".t3js-record-hide",e.delete=".t3js-record-delete",e.icon=".t3js-icon"}(Identifiers||(Identifiers={}));class AjaxDataHandler{constructor(){DocumentService.ready().then((()=>{this.initialize()}))}static refreshPageTree(){top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))}static call(e){return new AjaxRequest(TYPO3.settings.ajaxUrls.record_process).withQueryArguments(e).get().then((async e=>await e.resolve()))}process(e,t){return AjaxDataHandler.call(e).then((e=>{if(e.hasErrors&&this.handleErrors(e),t){const a={...t,hasErrors:e.hasErrors},r=new BroadcastMessage("datahandler","process",a);BroadcastService.post(r);const n=new CustomEvent("typo3:datahandler:process",{detail:{payload:a}});document.dispatchEvent(n)}return e}))}initialize(){new RegularEvent("click",((e,t)=>{e.preventDefault();const a=t.querySelector(Identifiers.icon),r=t.closest("tr[data-uid]"),n=t.dataset.params;this._showSpinnerIcon(a),this.process(n).then((e=>{e.hasErrors||this.toggleRow(r)}))})).delegateTo(document,Identifiers.hide),new RegularEvent("click",((e,t)=>{e.preventDefault();const a=Modal.confirm(t.dataset.title,t.dataset.message,SeverityEnum.warning,[{text:t.dataset.buttonCloseText||TYPO3.lang["button.cancel"]||"Cancel",active:!0,btnClass:"btn-default",name:"cancel"},{text:t.dataset.buttonOkText||TYPO3.lang["button.delete"]||"Delete",btnClass:"btn-warning",name:"delete"}]);a.addEventListener("button.clicked",(e=>{"cancel"===e.target.getAttribute("name")?a.hideModal():"delete"===e.target.getAttribute("name")&&(a.hideModal(),this.deleteRecord(t))}))})).delegateTo(document,Identifiers.delete)}toggleRow(e){const t=e.querySelector(Identifiers.hide),a=t.closest("table[data-table]").dataset.table,r=t.dataset.params;let n,o,s;"hidden"===t.dataset.state?(o="visible",n=r.replace("=0","=1"),s="actions-edit-hide"):(o="hidden",n=r.replace("=1","=0"),s="actions-edit-unhide"),t.dataset.state=o,t.dataset.params=n;const c=t.querySelector(Identifiers.icon);Icons.getIcon(s,Icons.sizes.small).then((e=>{c.replaceWith(document.createRange().createContextualFragment(e))}));const i=e.querySelector(".col-icon "+Identifiers.icon);"hidden"===o?Icons.getIcon("miscellaneous-placeholder",Icons.sizes.small,"overlay-hidden").then((e=>{const t=document.createRange().createContextualFragment(e);i.append(t.querySelector(".icon-overlay"))})):i.querySelector(".icon-overlay").remove();const d=new RegularEvent("animationend",(()=>{e.classList.remove("record-pulse"),d.release()}));d.bindTo(e),e.classList.add("record-pulse"),"pages"===a&&AjaxDataHandler.refreshPageTree()}deleteRecord(e){const t=e.dataset.params;let a=e.querySelector(Identifiers.icon);this._showSpinnerIcon(a);const r=e.closest("table[data-table]"),n=r.dataset.table,o=e.closest("tr[data-uid]"),s=parseInt(o.dataset.uid,10),c={component:"datahandler",action:"delete",table:n,uid:s};this.process(t,c).then((t=>{if(Icons.getIcon("actions-edit-delete",Icons.sizes.small).then((t=>{a=e.querySelector(Identifiers.icon),a.replaceWith(document.createRange().createContextualFragment(t))})),!t.hasErrors){const t=e.closest(".recordlist"),a=t.querySelector(".recordlist-heading-title"),c=r.querySelector('[data-l10nparent="'+s+'"]')?.closest("tr[data-uid]");if(void 0!==c&&(new RegularEvent("transitionend",(()=>{c.remove()})).bindTo(c),c.classList.add("record-deleted")),new RegularEvent("transitionend",(()=>{o.remove(),0===r.querySelectorAll("tbody tr").length&&t.remove()})).bindTo(o),o.classList.add("record-deleted"),"0"===e.dataset.l10parent||""===e.dataset.l10parent){const e=parseInt(a.querySelector(".t3js-table-total-items").textContent,10);a.querySelector(".t3js-table-total-items").textContent=(e-1).toString()}"pages"===n&&AjaxDataHandler.refreshPageTree()}}))}handleErrors(e){for(const t of e.messages)Notification.error(t.title,t.message)}_showSpinnerIcon(e){Icons.getIcon("spinner-circle-dark",Icons.sizes.small).then((t=>{e.replaceWith(document.createRange().createContextualFragment(t))}))}}export default new AjaxDataHandler; \ No newline at end of file +import{BroadcastMessage}from"@typo3/backend/broadcast-message.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import DocumentService from"@typo3/core/document-service.js";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import BroadcastService from"@typo3/backend/broadcast-service.js";import Icons from"@typo3/backend/icons.js";import Modal from"@typo3/backend/modal.js";import Notification from"@typo3/backend/notification.js";import RegularEvent from"@typo3/core/event/regular-event.js";var Identifiers;!function(e){e.hide='button[data-datahandler-action="visibility"]',e.delete=".t3js-record-delete",e.icon=".t3js-icon"}(Identifiers||(Identifiers={}));class AjaxDataHandler{constructor(){DocumentService.ready().then((()=>{this.initialize()}))}static refreshPageTree(){top.document.dispatchEvent(new CustomEvent("typo3:pagetree:refresh"))}static call(e){return new AjaxRequest(TYPO3.settings.ajaxUrls.record_process).withQueryArguments(e).get().then((async e=>await e.resolve()))}process(e,t){return AjaxDataHandler.call(e).then((e=>{if(e.hasErrors&&this.handleErrors(e),t){const a={...t,hasErrors:e.hasErrors},r=new BroadcastMessage("datahandler","process",a);BroadcastService.post(r);const n=new CustomEvent("typo3:datahandler:process",{detail:{payload:a}});document.dispatchEvent(n)}return e}))}initialize(){new RegularEvent("click",((e,t)=>{e.preventDefault(),this.handleVisibilityToggle(t)})).delegateTo(document,Identifiers.hide),new RegularEvent("click",((e,t)=>{e.preventDefault();const a=Modal.confirm(t.dataset.title,t.dataset.message,SeverityEnum.warning,[{text:t.dataset.buttonCloseText||TYPO3.lang["button.cancel"]||"Cancel",active:!0,btnClass:"btn-default",name:"cancel"},{text:t.dataset.buttonOkText||TYPO3.lang["button.delete"]||"Delete",btnClass:"btn-warning",name:"delete"}]);a.addEventListener("button.clicked",(e=>{"cancel"===e.target.getAttribute("name")?a.hideModal():"delete"===e.target.getAttribute("name")&&(a.hideModal(),this.deleteRecord(t))}))})).delegateTo(document,Identifiers.delete)}handleVisibilityToggle(e){const t=e.closest("tr[data-uid]"),a=e.querySelector(Identifiers.icon);this._showSpinnerIcon(a);const r={table:e.dataset.datahandlerTable,uid:e.dataset.datahandlerUid,field:e.dataset.datahandlerField,visible:"visible"===e.dataset.datahandlerStatus},n={data:{[r.table]:{[r.uid]:{[r.field]:r.visible?e.dataset.datahandlerHiddenValue:e.dataset.datahandlerVisibleValue}}}};this.process(n).then((a=>{if(!a.hasErrors){r.visible=!r.visible,e.setAttribute("data-datahandler-status",r.visible?"visible":"hidden");const a=r.visible?e.dataset.datahandlerVisibleLabel:e.dataset.datahandlerHiddenLabel;e.setAttribute("title",a);const n=r.visible?e.dataset.datahandlerVisibleIcon:e.dataset.datahandlerHiddenIcon,s=e.querySelector(Identifiers.icon);Icons.getIcon(n,Icons.sizes.small).then((e=>{s.replaceWith(document.createRange().createContextualFragment(e))}));const o=t.querySelector(".col-icon "+Identifiers.icon);r.visible?o.querySelector(".icon-overlay").remove():Icons.getIcon("miscellaneous-placeholder",Icons.sizes.small,"overlay-hidden").then((e=>{const t=document.createRange().createContextualFragment(e);o.append(t.querySelector(".icon-overlay"))}));const i=new RegularEvent("animationend",(()=>{t.classList.remove("record-pulse"),i.release()}));i.bindTo(t),t.classList.add("record-pulse"),"pages"===r.table&&AjaxDataHandler.refreshPageTree()}}))}deleteRecord(e){const t=e.dataset.params;let a=e.querySelector(Identifiers.icon);this._showSpinnerIcon(a);const r=e.closest("table[data-table]"),n=r.dataset.table,s=e.closest("tr[data-uid]"),o=parseInt(s.dataset.uid,10),i={component:"datahandler",action:"delete",table:n,uid:o};this.process(t,i).then((t=>{if(Icons.getIcon("actions-edit-delete",Icons.sizes.small).then((t=>{a=e.querySelector(Identifiers.icon),a.replaceWith(document.createRange().createContextualFragment(t))})),!t.hasErrors){const t=e.closest(".recordlist"),a=t.querySelector(".recordlist-heading-title"),i=r.querySelector('[data-l10nparent="'+o+'"]')?.closest("tr[data-uid]");if(void 0!==i&&(new RegularEvent("transitionend",(()=>{i.remove()})).bindTo(i),i.classList.add("record-deleted")),new RegularEvent("transitionend",(()=>{s.remove(),0===r.querySelectorAll("tbody tr").length&&t.remove()})).bindTo(s),s.classList.add("record-deleted"),"0"===e.dataset.l10parent||""===e.dataset.l10parent){const e=parseInt(a.querySelector(".t3js-table-total-items").textContent,10);a.querySelector(".t3js-table-total-items").textContent=(e-1).toString()}"pages"===n&&AjaxDataHandler.refreshPageTree()}}))}handleErrors(e){for(const t of e.messages)Notification.error(t.title,t.message)}_showSpinnerIcon(e){Icons.getIcon("spinner-circle-dark",Icons.sizes.small).then((t=>{e.replaceWith(document.createRange().createContextualFragment(t))}))}}export default new AjaxDataHandler; \ No newline at end of file