From 77a8007857742fe22239fedbb9404482f994afb0 Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Thu, 12 Aug 2021 20:18:10 +0200
Subject: [PATCH] [FEATURE] Introduce multi record selection in filelist

A new component "MultiRecordSelection" is added. It's
used first for the filelist module as well as the file
selector. It simply adds a consistent way of selecting
multiple elements (records) and to perform  actions on
this selection. Therefore, the checkboxes are always
displayed at the beginning of each record row. The
checkbox actions, such as "check all" are available
in the header row of the same column via a dropdown
menu. The actions to perform on the selection, e.g.
"delete" or "import", appear once a record is selected.

For the filelist module, this means those actions are
decoupled from the clipboard, making it easier for
editors to work with multiple records.

This also introduces the "Edit marked" action to the
filelist module, which allows to edit the metadata of
all selected files at once.

Resolves: #94906
Releases: master
Change-Id: Id9e2915203049235fc4b503d33a4f1a77646c133
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70515
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 | 271 +++++++++++++++
 .../Public/TypeScript/BrowseFiles.ts          | 119 ++-----
 .../Public/JavaScript/MultiRecordSelection.js |  13 +
 ...e-94906-MultiRecordSelectionInFilelist.rst |  42 +++
 .../Backend/FileList/FileClipboardCest.php    |   5 +-
 .../Classes/Controller/FileListController.php |  20 +-
 typo3/sysext/filelist/Classes/FileList.php    | 315 +++++++++---------
 .../Language/locallang_mod_file_list.xlf      |   9 +-
 .../Private/Templates/File/List.html          |  50 ++-
 .../Classes/Browser/FileBrowser.php           |  80 ++---
 .../Public/JavaScript/BrowseFiles.js          |   2 +-
 11 files changed, 616 insertions(+), 310 deletions(-)
 create mode 100644 Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts
 create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-94906-MultiRecordSelectionInFilelist.rst

diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts
new file mode 100644
index 000000000000..c85854a0ff83
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/MultiRecordSelection.ts
@@ -0,0 +1,271 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import Notification = require('TYPO3/CMS/Backend/Notification');
+import DocumentService = require('TYPO3/CMS/Core/DocumentService');
+import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
+
+enum Selectors {
+  actionsSelector = '.t3js-multi-record-selection-actions',
+  checkboxSelector = '.t3js-multi-record-selection-check',
+  checkboxActionsSelector = '#multi-record-selection-check-actions',
+}
+
+enum Buttons {
+  actionButton = 'button[data-multi-record-selection-action]',
+  checkboxActionButton = 'button[data-multi-record-selection-check-action]',
+  checkboxActionsToggleButton = 'button[data-bs-target="multi-record-selection-check-actions"]'
+}
+
+enum Actions {
+  edit = 'edit'
+}
+
+enum CheckboxActions {
+  checkAll = 'check-all',
+  checkNone = 'check-none',
+  toggle = 'toggle'
+}
+
+enum CheckboxState {
+  any = '',
+  checked = ':checked',
+  unchecked = ':not(:checked)'
+}
+
+interface ActionConfiguration {
+  idField: string;
+}
+
+interface EditActionConfiguration extends ActionConfiguration{
+  table: string;
+  returnUrl: string;
+}
+
+/**
+ * Module: TYPO3/CMS/Backend/MultiRecordSelection
+ */
+class MultiRecordSelection {
+  private static getCheckboxes(state: CheckboxState = CheckboxState.any): NodeListOf<HTMLInputElement> {
+    return document.querySelectorAll(Selectors.checkboxSelector + state);
+  }
+
+  private static changeCheckboxState(checkbox: HTMLInputElement, check: boolean): void {
+    if (checkbox.checked === check) {
+      // Return if state did not change
+      return;
+    }
+    checkbox.checked = check;
+    checkbox.dispatchEvent(new Event('checkbox:state:changed', {bubbles: true, cancelable: false}));
+  }
+
+  private static getReturnUrl(returnUrl: string): string {
+    if (returnUrl === '') {
+      returnUrl = top.list_frame.document.location.pathname + top.list_frame.document.location.search;
+    }
+    return encodeURIComponent(returnUrl);
+  }
+
+  /**
+   * This restores (initializes) a temporary state, which is required in case
+   * the user returns to the listing using the browsers' history back feature,
+   * which will not result in a new request.
+   */
+  private static restoreTemporaryState(): void {
+    const checked: NodeListOf<HTMLInputElement> = MultiRecordSelection.getCheckboxes(CheckboxState.checked);
+    // In case nothing is checked we don't have to do anything here
+    if (!checked.length) {
+      return;
+    }
+    checked.forEach((checkbox: HTMLInputElement) => {
+      checkbox.closest('tr').classList.add('success');
+    });
+    const actionsContainer: HTMLElement = document.querySelector(Selectors.actionsSelector);
+    if (actionsContainer !== null) {
+      actionsContainer.classList.remove('hidden');
+    }
+  }
+
+  /**
+   * Toggles the state of the actions, depending on the
+   * currently selected elements and their nature.
+   */
+  private static toggleActionsState(): void {
+    const actions: NodeListOf<HTMLButtonElement> = document.querySelectorAll([Selectors.actionsSelector, Buttons.actionButton].join(' '));
+    if (!actions.length) {
+      // Early return in case no action is defined
+      return;
+    }
+
+    actions.forEach((action: HTMLButtonElement): void => {
+      if (!action.dataset.multiRecordSelectionActionConfig) {
+        // In case the action does not define any configuration, no toggling is possible
+        return;
+      }
+      const configuration: ActionConfiguration = JSON.parse(action.dataset.multiRecordSelectionActionConfig);
+      if (!configuration.idField) {
+        // Return in case the idField (where to find the id on selected elements) is not defined
+        return;
+      }
+      // Start the evaluation by disabling the action
+      action.classList.add('disabled');
+      // Get all currently checked elements
+      const checked: NodeListOf<HTMLInputElement> = MultiRecordSelection.getCheckboxes(CheckboxState.checked);
+      for (let i=0; i < checked.length; i++) {
+        // Evaluate each checked element if it contains the specified idField
+        if (checked[i].closest('tr').dataset[configuration.idField]) {
+          // If a checked element contains the idField, remove the "disabled"
+          // state and end the search since the action can be performed.
+          action.classList.remove('disabled');
+          break;
+        }
+      }
+    });
+  }
+
+  constructor() {
+    DocumentService.ready().then((): void => {
+      MultiRecordSelection.restoreTemporaryState();
+      this.registerActions();
+      this.registerCheckboxActions();
+      this.registerToggleCheckboxActions();
+      this.registerDispatchCheckboxStateChangedEvent();
+      this.registerCheckboxStateChangedEventHandler();
+    });
+  }
+
+  private registerActions(): void {
+    new RegularEvent('click', (e: Event, target: HTMLButtonElement): void => {
+      const checked: NodeListOf<HTMLInputElement> = MultiRecordSelection.getCheckboxes(CheckboxState.checked);
+
+      if (!target.dataset.multiRecordSelectionAction || !checked.length) {
+        // Return if we don't deal with a valid action or in case there is
+        // currently no element checked to perform the action on.
+        return;
+      }
+
+      // Perform requested action
+      switch (target.dataset.multiRecordSelectionAction) {
+        case Actions.edit:
+          e.preventDefault();
+          const configuration: EditActionConfiguration = JSON.parse(target.dataset.multiRecordSelectionActionConfig || '');
+          if (!configuration || !configuration.idField || !configuration.table) {
+            break;
+          }
+          const list: Array<string> = [];
+          checked.forEach((checkbox: HTMLInputElement) => {
+            const checkboxContainer: HTMLElement = checkbox.closest('tr');
+            if (checkboxContainer !== null && checkboxContainer.dataset[configuration.idField]) {
+              list.push(checkboxContainer.dataset[configuration.idField]);
+            }
+          });
+          if (list.length) {
+            window.location.href = top.TYPO3.settings.FormEngine.moduleUrl
+              + '&edit[' + configuration.table + '][' + list.join(',') + ']=edit'
+              + '&returnUrl=' + MultiRecordSelection.getReturnUrl(configuration.returnUrl || '');
+          } else {
+            Notification.warning('The selected elements can not be edited.');
+          }
+          break;
+        default:
+          // Not all actions are handled here. Therefore we simply skip them and just
+          // dispatch an event so those components can react on the triggered action.
+          target.dispatchEvent(new Event('multiRecordSelection:action:' + target.dataset.multiRecordSelectionAction, {bubbles: true, cancelable: false}));
+          break;
+      }
+    }).delegateTo(document, [Selectors.actionsSelector, Buttons.actionButton].join(' '));
+
+    // After registering the event, toggle their state
+    MultiRecordSelection.toggleActionsState();
+  }
+
+  private registerCheckboxActions(): void {
+    new RegularEvent('click', (e: Event, target: HTMLButtonElement): void => {
+      e.preventDefault();
+
+      const checkboxes: NodeListOf<HTMLInputElement> = MultiRecordSelection.getCheckboxes();
+      if (!target.dataset.multiRecordSelectionCheckAction || !checkboxes.length) {
+        // Return if we don't deal with a valid action or in case there
+        // are no checkboxes (elements) to perform the action on.
+        return;
+      }
+
+      // Perform requested action
+      switch (target.dataset.multiRecordSelectionCheckAction) {
+        case CheckboxActions.checkAll:
+          checkboxes.forEach((checkbox: HTMLInputElement) => {
+            MultiRecordSelection.changeCheckboxState(checkbox, true);
+          });
+          break;
+        case CheckboxActions.checkNone:
+          checkboxes.forEach((checkbox: HTMLInputElement) => {
+            MultiRecordSelection.changeCheckboxState(checkbox, false);
+          });
+          break;
+        case CheckboxActions.toggle:
+          checkboxes.forEach((checkbox: HTMLInputElement) => {
+            MultiRecordSelection.changeCheckboxState(checkbox, !checkbox.checked);
+          });
+          break;
+        default:
+          // Unknown action
+          Notification.warning('Unknown checkbox action');
+      }
+    }).delegateTo(document, [Selectors.checkboxActionsSelector, Buttons.checkboxActionButton].join(' '));
+  }
+
+  private registerDispatchCheckboxStateChangedEvent(): void {
+    new RegularEvent('change', (e: Event, target: HTMLInputElement): void => {
+      target.dispatchEvent(new Event('checkbox:state:changed', {bubbles: true, cancelable: false}));
+    }).delegateTo(document, Selectors.checkboxSelector);
+  }
+
+  private registerCheckboxStateChangedEventHandler(): void {
+    new RegularEvent('checkbox:state:changed', (e: Event): void => {
+      const checkbox: HTMLInputElement = <HTMLInputElement>e.target;
+      if (checkbox.checked) {
+        checkbox.closest('tr').classList.add('success');
+      } else {
+        checkbox.closest('tr').classList.remove('success');
+      }
+
+      const actionsContainer: HTMLElement = document.querySelector(Selectors.actionsSelector);
+      if (actionsContainer !== null) {
+        if (MultiRecordSelection.getCheckboxes(CheckboxState.checked).length) {
+          actionsContainer.classList.remove('hidden');
+        } else {
+          actionsContainer.classList.add('hidden');
+        }
+      }
+
+      // Toggle actions for changed checkbox state
+      MultiRecordSelection.toggleActionsState();
+    }).bindTo(document);
+  }
+
+  private registerToggleCheckboxActions(): void {
+    new RegularEvent('click', (): void => {
+      const checkAll: HTMLButtonElement = document.querySelector('button[data-multi-record-selection-check-action="' + CheckboxActions.checkAll + '"]');
+      if (checkAll !== null) {
+        checkAll.classList.toggle('disabled', !MultiRecordSelection.getCheckboxes(CheckboxState.unchecked).length)
+      }
+
+      const checkNone: HTMLButtonElement = document.querySelector('button[data-multi-record-selection-check-action="' + CheckboxActions.checkNone + '"]');
+      if (checkNone !== null) {
+        checkNone.classList.toggle('disabled', !MultiRecordSelection.getCheckboxes(CheckboxState.checked).length);
+      }
+    }).delegateTo(document, Buttons.checkboxActionsToggleButton);
+  }
+}
+
+export = new MultiRecordSelection();
diff --git a/Build/Sources/TypeScript/recordlist/Resources/Public/TypeScript/BrowseFiles.ts b/Build/Sources/TypeScript/recordlist/Resources/Public/TypeScript/BrowseFiles.ts
index 2c6b731b934c..edbb8da2e71d 100644
--- a/Build/Sources/TypeScript/recordlist/Resources/Public/TypeScript/BrowseFiles.ts
+++ b/Build/Sources/TypeScript/recordlist/Resources/Public/TypeScript/BrowseFiles.ts
@@ -17,21 +17,12 @@ import NProgress = require('nprogress');
 import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
 import Icons = TYPO3.Icons;
 
-enum Selectors {
-  bulkItemSelector = '.typo3-list-check',
-  importSelectionSelector = 'button[data-action="import"]',
-  selectionToggleSelector = '.typo3-selection-toggle',
-  listContainer = '[data-list-container="files"]'
-}
-
 interface LinkElement {
   fileName: string;
   uid: string;
 }
 
 class BrowseFiles {
-  public static Selector: Selector;
-
   public static insertElement(fileName: string, fileUid: number, close?: boolean): boolean {
     return ElementBrowser.insertElement(
       'sys_file',
@@ -42,9 +33,14 @@ class BrowseFiles {
     );
   }
 
-  constructor() {
-    BrowseFiles.Selector = new Selector();
+  private static handleNext(items: LinkElement[]): void {
+    if (items.length > 0) {
+      const item = items.pop();
+      BrowseFiles.insertElement(item.fileName, Number(item.uid));
+    }
+  }
 
+  constructor() {
     new RegularEvent('click', (evt: MouseEvent, targetEl: HTMLElement): void => {
       evt.preventDefault();
       BrowseFiles.insertElement(
@@ -54,87 +50,33 @@ class BrowseFiles {
       );
     }).delegateTo(document, '[data-close]');
 
-    new RegularEvent('change', BrowseFiles.Selector.toggleImportButton).delegateTo(document, Selectors.bulkItemSelector);
-    new RegularEvent('click', BrowseFiles.Selector.handle).delegateTo(document, Selectors.importSelectionSelector);
-    new RegularEvent('click', BrowseFiles.Selector.toggle).delegateTo(document, Selectors.selectionToggleSelector);
-    new RegularEvent('change', BrowseFiles.Selector.toggle).delegateTo(document, Selectors.bulkItemSelector);
+    // Handle import selection event, dispatched from MultiRecordSelection
+    new RegularEvent('multiRecordSelection:action:import', this.importSelection).bindTo(document);
   }
 
-}
-
-class Selector {
-  /**
-   * Either a toggle button (all/none/toggle) button was pressed, or a checkbox was switched
-   */
-  public toggle = (e: MouseEvent): void => {
+  private importSelection = (e: Event): void => {
     e.preventDefault();
-    const element = e.target as HTMLInputElement;
-    const action = element.dataset.action;
-    const items = this.getItems();
-
-    switch (action) {
-      case 'select-toggle':
-        items.forEach((item: HTMLInputElement) => {
-          item.checked = !item.checked;
-          item.closest('tr').classList.toggle('success');
-        });
-        break;
-      case 'select-all':
-        items.forEach((item: HTMLInputElement) => {
-          item.checked = true;
-          item.closest('tr').classList.add('success');
-        });
-        break;
-      case 'select-none':
-        items.forEach((item: HTMLInputElement) => {
-          item.checked = false;
-          item.closest('tr').classList.remove('success');
-        });
-        break;
-      default:
-        // the button itself was checked
-        if (element.classList.contains('typo3-list-check')) {
-          element.closest('tr').classList.toggle('success');
-        }
+    const targetEl: HTMLElement = e.target as HTMLElement;
+    const items: NodeListOf<HTMLInputElement> = document.querySelectorAll('.t3js-multi-record-selection-check');
+    if (!items.length) {
+      return;
     }
-    this.toggleImportButton();
-  }
 
-  /**
-   * Import selection button is pressed
-   */
-  public handle = (e: MouseEvent, targetEl: HTMLElement): void => {
-    e.preventDefault();
-    const items = this.getItems();
     const selectedItems: Array<LinkElement> = [];
-    if (items.length) {
-      items.forEach((item: HTMLInputElement) => {
-        if (item.checked && item.name && item.dataset.fileName && item.dataset.fileUid) {
-          selectedItems.unshift({uid: item.dataset.fileUid, fileName: item.dataset.fileName});
-        }
-      });
-      Icons.getIcon('spinner-circle', Icons.sizes.small, null, null, Icons.markupIdentifiers.inline).then((icon: string): void => {
-        targetEl.classList.add('disabled');
-        targetEl.innerHTML = icon;
-      });
-      this.handleSelection(selectedItems);
-    }
-  }
-
-  public getItems(): NodeList {
-    return document.querySelector(Selectors.listContainer).querySelectorAll(Selectors.bulkItemSelector);
-  }
-
-  public toggleImportButton(): void {
-    const hasCheckedElements = document.querySelector(Selectors.listContainer)?.querySelectorAll(Selectors.bulkItemSelector + ':checked').length > 0;
-    document.querySelector(Selectors.importSelectionSelector).classList.toggle('disabled', !hasCheckedElements);
-  }
+    items.forEach((item: HTMLInputElement) => {
+      if (item.checked && item.name && item.dataset.fileName && item.dataset.fileUid) {
+        selectedItems.unshift({uid: item.dataset.fileUid, fileName: item.dataset.fileName});
+      }
+    });
 
-  private handleSelection(items: LinkElement[]): void {
+    Icons.getIcon('spinner-circle', Icons.sizes.small, null, null, Icons.markupIdentifiers.inline).then((icon: string): void => {
+      targetEl.classList.add('disabled');
+      targetEl.innerHTML = icon;
+    });
     NProgress.configure({parent: '.element-browser-main-content', showSpinner: false});
     NProgress.start();
-    const stepping = 1 / items.length;
-    this.handleNext(items);
+    const stepping = 1 / selectedItems.length;
+    BrowseFiles.handleNext(selectedItems);
 
     new RegularEvent('message', (e: MessageEvent): void => {
       if (!MessageUtility.verifyOrigin(e.origin)) {
@@ -142,9 +84,9 @@ class Selector {
       }
 
       if (e.data.actionName === 'typo3:foreignRelation:inserted') {
-        if (items.length > 0) {
+        if (selectedItems.length > 0) {
           NProgress.inc(stepping);
-          this.handleNext(items);
+          BrowseFiles.handleNext(selectedItems);
         } else {
           NProgress.done();
           ElementBrowser.focusOpenerAndClose();
@@ -152,13 +94,6 @@ class Selector {
       }
     }).bindTo(window);
   }
-
-  private handleNext(items: LinkElement[]): void {
-    if (items.length > 0) {
-      const item = items.pop();
-      BrowseFiles.insertElement(item.fileName, Number(item.uid));
-    }
-  }
 }
 
 export = new BrowseFiles();
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js b/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js
new file mode 100644
index 000000000000..8bc39188051f
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/MultiRecordSelection.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * 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.querySelector(i.actionsSelector);null!==t&&t.classList.remove("hidden")}static toggleActionsState(){const e=document.querySelectorAll([i.actionsSelector,s.actionButton].join(" "));e.length&&e.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.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()}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");const c=document.querySelector(i.actionsSelector);null!==c&&(d.getCheckboxes(r.checked).length?c.classList.remove("hidden"):c.classList.add("hidden")),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
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-94906-MultiRecordSelectionInFilelist.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-94906-MultiRecordSelectionInFilelist.rst
new file mode 100644
index 000000000000..878630c11997
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-94906-MultiRecordSelectionInFilelist.rst
@@ -0,0 +1,42 @@
+.. include:: ../../Includes.txt
+
+====================================================
+Feature: #94906 - Multi record selection in filelist
+====================================================
+
+See :issue:`94906`
+
+Description
+===========
+
+With :issue:`94452` the file list in the file selector has been improved
+by introducing an optimized way of selecting the files to attach to a record.
+
+Those optimizations have now also been added to the filelist module. The
+checkboxes, previously only used for adding files / folders to the
+clipboard, are now always shown in front of each file / folder and are
+now independant of the current clipboard mode. Furthermore are the
+convenience actions, such as "check all", "uncheck all" and "toggle
+selection" now available in the filelist, too.
+
+By decoupling the selection from the clipboard logic, it's therefore
+now possible to directly work with the current selection without the
+need to transfer it top the clipboard first. This means, editing or
+deleting multiple files is now directly possible without any clipboard
+interaction. The available actions appear, once an element has been
+selected.
+
+As mentioned above, the "Edit marked" action has been added to the
+filelist, which might already be known from the recordlist module.
+This action allows to edit the :sql:`sys_file_metadata` records of
+all selected files at once.
+
+Impact
+======
+
+Selection of files and folder is now quicker to grasp for editors working
+in the filelist module. It's furthermore now also possible to directly
+execute actions, e.g. editing metadata of selected files, without
+transferring them to the clipboard first.
+
+.. index:: Backend, ext:filelist
diff --git a/typo3/sysext/core/Tests/Acceptance/Backend/FileList/FileClipboardCest.php b/typo3/sysext/core/Tests/Acceptance/Backend/FileList/FileClipboardCest.php
index d2e65e498206..2ab430644a34 100644
--- a/typo3/sysext/core/Tests/Acceptance/Backend/FileList/FileClipboardCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Backend/FileList/FileClipboardCest.php
@@ -77,8 +77,9 @@ class FileClipboardCest extends AbstractFileCest
 
         $I->amGoingTo('add multiple elements to clipboard');
         $I->click('Clipboard #1 (multi-selection mode)');
-        $I->click('.t3js-toggle-all-checkboxes');
-        $I->click('span[title="Transfer the selection of files to clipboard"]');
+        $I->click('.dropdown-toggle');
+        $I->click('button[data-multi-record-selection-check-action="check-all"]');
+        $I->click('button[data-multi-record-selection-action="setCB"]');
 
         foreach ($expectedFiles as $file) {
             $I->see($file, '#clipboard_form');
diff --git a/typo3/sysext/filelist/Classes/Controller/FileListController.php b/typo3/sysext/filelist/Classes/Controller/FileListController.php
index 356f398462da..a9c24cacbe54 100644
--- a/typo3/sysext/filelist/Classes/Controller/FileListController.php
+++ b/typo3/sysext/filelist/Classes/Controller/FileListController.php
@@ -269,7 +269,7 @@ class FileListController implements LoggerAwareInterface
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileList');
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileDelete');
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
-        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ClipboardComponent');
+        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/MultiRecordSelection');
         $this->pageRenderer->addInlineLanguageLabelFile(
             'EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf',
             'buttons'
@@ -368,8 +368,7 @@ class FileListController implements LoggerAwareInterface
             $this->folderObject,
             MathUtility::forceIntegerInRange($this->pointer, 0, 100000),
             (string)($this->MOD_SETTINGS['sort'] ?? ''),
-            (bool)($this->MOD_SETTINGS['reverse'] ?? false),
-            (bool)($this->MOD_SETTINGS['clipBoard'] ?? false)
+            (bool)($this->MOD_SETTINGS['reverse'] ?? false)
         );
     }
 
@@ -384,7 +383,10 @@ class FileListController implements LoggerAwareInterface
 
         // Generate the list, if accessible
         if ($this->folderObject->getStorage()->isBrowsable()) {
-            $this->view->assign('listHtml', $this->filelist->getTable($searchDemand));
+            $this->view->assignMultiple([
+                'listHtml' => $this->filelist->getTable($searchDemand),
+                'totalItems' => $this->filelist->totalItems
+            ]);
             if ($this->filelist->totalItems === 0 && $searchDemand !== null) {
                 // In case this is a search and no results were found, add a flash message
                 // @todo This info should in the future also be displayed for folders without any file.
@@ -393,6 +395,15 @@ class FileListController implements LoggerAwareInterface
                     $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang.xlf:flashmessage.no_results')
                 );
             }
+            // Assign meta information for the multi record selection
+            $this->view->assignMultiple([
+                'hasSelectedElements' => $this->filelist->getSelectedElements() !== [],
+                'editActionConfiguration' => json_encode([
+                    'idField' => 'metadataUid',
+                    'table' => 'sys_file_metadata',
+                    'returnUrl' => $this->filelist->listURL()
+                ])
+            ]);
         } else {
             $this->addFlashMessage(
                 $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:storageNotBrowsableMessage'),
@@ -450,6 +461,7 @@ class FileListController implements LoggerAwareInterface
         $this->view->assign('enableClipBoard', [
             'enabled' => $userTsConfig['options.']['file_list.']['enableClipBoard'] === 'selectable',
             'label' => htmlspecialchars($lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clipBoard')),
+            'mode' => $this->filelist->clipObj->current,
             'html' => BackendUtility::getFuncCheck(
                 $this->id,
                 'SET[clipBoard]',
diff --git a/typo3/sysext/filelist/Classes/FileList.php b/typo3/sysext/filelist/Classes/FileList.php
index 8b3390c5c7b0..ea0c28df7a10 100644
--- a/typo3/sysext/filelist/Classes/FileList.php
+++ b/typo3/sysext/filelist/Classes/FileList.php
@@ -135,7 +135,8 @@ class FileList
      */
     public $addElement_tdCssClass = [
         '_CONTROL_' => 'col-control',
-        '_CLIPBOARD_' => 'col-clipboard',
+        '_SELECTOR_' => 'col-selector',
+        'icon' => 'col-icon',
         'file' => 'col-title col-responsive',
         '_LOCALIZATION_' => 'col-localizationa',
     ];
@@ -177,6 +178,8 @@ class FileList
 
     protected ?FileSearchDemand $searchDemand = null;
 
+    protected array $selectedElements = [];
+
     public function __construct(?ServerRequestInterface $request = null)
     {
         // Setting the maximum length of the filenames to the user's settings or minimum 30 (= $this->fixedL)
@@ -204,9 +207,8 @@ class FileList
      * @param int $pointer Pointer
      * @param string $sort Sorting column
      * @param bool $sortRev Sorting direction
-     * @param bool $clipBoard
      */
-    public function start(Folder $folderObject, $pointer, $sort, $sortRev, $clipBoard = false)
+    public function start(Folder $folderObject, $pointer, $sort, $sortRev)
     {
         $this->folderObject = $folderObject;
         $this->counter = 0;
@@ -214,36 +216,9 @@ class FileList
         $this->sort = $sort;
         $this->sortRev = $sortRev;
         $this->firstElementNumber = $pointer;
-        // Cleaning rowlist for duplicates and place the $titleCol as the first column always!
-        $rowlist = 'file,_LOCALIZATION_,_CONTROL_,fileext,tstamp,size,rw,_REF_';
-        if ($clipBoard) {
-            $rowlist = str_replace('_CONTROL_,', '_CONTROL_,_CLIPBOARD_,', $rowlist);
-        }
-        $this->fieldArray = explode(',', $rowlist);
-    }
-
-    /**
-     * Wrapping input string in a link with clipboard command.
-     *
-     * @param string $string String to be linked - must be htmlspecialchar'ed / prepared before.
-     * @param string $cmd "cmd" value
-     * @param string $warning Warning for JS confirm message
-     * @return string Linked string
-     */
-    public function linkClipboardHeaderIcon($string, $cmd, $warning = '')
-    {
-        if ($warning) {
-            $attributes['class'] = 'btn btn-default t3js-modal-trigger';
-            $attributes['data-severity'] = 'warning';
-            $attributes['data-bs-content'] = $warning;
-            $attributes['data-event-name'] = 'filelist:clipboard:cmd';
-            $attributes['data-event-payload'] = $cmd;
-        } else {
-            $attributes['class'] = 'btn btn-default';
-            $attributes['data-filelist-clipboard-cmd'] = $cmd;
-        }
-
-        return '<button type="button" ' . GeneralUtility::implodeAttributes($attributes, true) . '>' . $string . '</button>';
+        $this->fieldArray = [
+            '_SELECTOR_', 'icon', 'file', '_LOCALIZATION_', '_CONTROL_', 'fileext', 'tstamp', 'size', 'rw', '_REF_'
+        ];
     }
 
     /**
@@ -285,7 +260,7 @@ class FileList
             $files = array_slice($files, $this->firstElementNumber, $filesNum);
 
             // Add special "Path" field for the search result
-            array_unshift($this->fieldArray, '_PATH_');
+            array_splice($this->fieldArray, 3, 0, '_PATH_');
         } else {
             // @todo use folder methods directly when they support filters
             $storage = $this->folderObject->getStorage();
@@ -353,14 +328,14 @@ class FileList
         // Header line is drawn
         $theData = [];
         foreach ($this->fieldArray as $v) {
-            if ($v === '_CLIPBOARD_') {
-                $theData[$v] = $this->renderClipboardHeaderRow(!empty($iOut));
+            if ($v === '_SELECTOR_') {
+                $theData[$v] = $this->renderCheckboxActions();
             } elseif ($v === '_REF_') {
                 $theData[$v] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._REF_'));
             } elseif ($v === '_PATH_') {
                 $theData[$v] = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels._PATH_'));
-            } else {
-                // Normal row
+            } elseif ($v !== 'icon') {
+                // Normal row - except "icon", which does not need a table header col
                 $theData[$v]  = $this->linkWrapSort($v);
             }
         }
@@ -369,87 +344,25 @@ class FileList
             <div class="mb-4 mt-2">
                 <div class="table-fit mb-0">
                     <table class="table table-striped table-hover" id="typo3-filelist">
-                        <thead>' . $this->addElement('', $theData, true) . '</thead>
+                        <thead>' . $this->addElement($theData, [], true) . '</thead>
                         <tbody>' . $iOut . '</tbody>
                     </table>
                 </div>
             </div>';
     }
 
-    protected function renderClipboardHeaderRow(bool $hasContent): string
-    {
-        $cells = [];
-        $elFromTable = $this->clipObj->elFromTable('_FILE');
-        if (!empty($elFromTable) && $this->folderObject->checkActionPermission('write')) {
-            $clipboardMode = $this->clipObj->clipData[$this->clipObj->current]['mode'] ?? '';
-            $permission = $clipboardMode === 'copy' ? 'copy' : 'move';
-            $addPasteButton = $this->folderObject->checkActionPermission($permission);
-            $elToConfirm = [];
-            foreach ($elFromTable as $key => $element) {
-                $clipBoardElement = $this->resourceFactory->retrieveFileOrFolderObject($element);
-                if ($clipBoardElement instanceof Folder && $clipBoardElement->getStorage()->isWithinFolder($clipBoardElement, $this->folderObject)) {
-                    $addPasteButton = false;
-                }
-                $elToConfirm[$key] = $clipBoardElement->getName();
-            }
-            if ($addPasteButton) {
-                $cells[] = '<a class="btn btn-default t3js-modal-trigger"' .
-                    ' href="' . htmlspecialchars($this->clipObj->pasteUrl(
-                        '_FILE',
-                        $this->folderObject->getCombinedIdentifier()
-                    )) . '"'
-                    . ' data-bs-content="' . htmlspecialchars($this->clipObj->confirmMsgText(
-                        '_FILE',
-                        $this->folderObject->getReadablePath(),
-                        'into',
-                        $elToConfirm
-                    )) . '"'
-                    . ' data-severity="warning"'
-                    . ' data-title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_paste')) . '"'
-                    . ' title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_paste')) . '">'
-                    . $this->iconFactory->getIcon('actions-document-paste-into', Icon::SIZE_SMALL)
-                        ->render()
-                    . '</a>';
-            } else {
-                $cells[] = $this->spaceIcon;
-            }
-        }
-        if ($this->clipObj->current !== 'normal' && $hasContent) {
-            $cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_selectMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-copy', Icon::SIZE_SMALL)->render() . '</span>', 'setCB');
-            $cells[] = $this->linkClipboardHeaderIcon('<span title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_deleteMarked')) . '">' . $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span>', 'delete', $this->getLanguageService()->getLL('clip_deleteMarkedWarning'));
-            $cells[] = '<a class="btn btn-default t3js-toggle-all-checkboxes" data-checkboxes-names="' . htmlspecialchars(implode(',', $this->CBnames)) . '" rel="" href="#" title="' . htmlspecialchars($this->getLanguageService()->getLL('clip_markRecords')) . '">' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a>';
-        }
-        if (!empty($cells)) {
-            return '<div class="btn-group">' . implode('', $cells) . '</div>';
-        }
-        return '';
-    }
-
     /**
      * Returns a table-row with the content from the fields in the input data array.
      * OBS: $this->fieldArray MUST be set! (represents the list of fields to display)
      *
-     * @param string $icon Is the <img>+<a> of the record. If not supplied the first 'join'-icon will be a 'line' instead
      * @param array $data Is the data array, record with the fields. Notice: These fields are (currently) NOT htmlspecialchar'ed before being wrapped in <td>-tags
+     * @param array $attributes Attributes for the table row. Values will be htmlspecialchar'ed!
      * @param bool $isTableHeader Whether the element to be added is a table header
      *
      * @return string HTML content for the table row
      */
-    public function addElement(string $icon, array $data, bool $isTableHeader = false): string
+    public function addElement(array $data, array $attributes = [], bool $isTableHeader = false): string
     {
-        // Initialize additional data attributes for the row
-        // Note: To be consistent with the other $data values, the additional data attributes
-        // are not htmlspecialchar'ed before being added to the table row. Therefore it
-        // has to be ensured they are properly escaped when applied to the $data array!
-        $dataAttributes = [];
-        foreach (['type', 'file-uid', 'metadata-uid', 'folder-identifier', 'combined-identifier'] as $dataAttribute) {
-            if (isset($data[$dataAttribute])) {
-                $dataAttributes['data-' . $dataAttribute] = $data[$dataAttribute];
-                // Unset as we don't need them anymore, when building the table cells
-                unset($data[$dataAttribute]);
-            }
-        }
-
         // Initialize rendering.
         $cols = [];
         $colType = $isTableHeader ? 'th' : 'td';
@@ -480,10 +393,9 @@ class FileList
 
         // Add the the table row
         return '
-            <tr ' . GeneralUtility::implodeAttributes($dataAttributes) . '>
-                <' . $colType . ' class="col-icon nowrap">' . ($icon ?: '') . '</' . $colType . '>'
-                . implode(PHP_EOL, $cols) .
-            '</tr>';
+            <tr ' . GeneralUtility::implodeAttributes($attributes, true) . '>
+                ' . implode(PHP_EOL, $cols) . '
+            </tr>';
     }
 
     /**
@@ -503,7 +415,7 @@ class FileList
                     'actions-move-up',
                     Icon::SIZE_SMALL
                 )->render() . ' <i>[' . (max(0, $currentItemCount - $this->iLimit) + 1) . ' - ' . $currentItemCount . ']</i></a>';
-                $code = $this->addElement('', $theData);
+                $code = $this->addElement($theData);
             }
             return $code;
         }
@@ -515,7 +427,7 @@ class FileList
                 'actions-move-down',
                 Icon::SIZE_SMALL
             )->render() . ' <i>[' . ($currentItemCount + 1) . ' - ' . $this->totalItems . ']</i></a>';
-            $code = $this->addElement('', $theData);
+            $code = $this->addElement($theData);
         }
         return $code;
     }
@@ -562,22 +474,28 @@ class FileList
             // Initialization
             $this->counter++;
 
-            // The icon with link
-            $theIcon = '<span title="' . htmlspecialchars($folderName) . '">' . $this->iconFactory->getIconForResource($folderObject, Icon::SIZE_SMALL)->render() . '</span>';
-            if (!$isLocked) {
-                $theIcon = (string)BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $folderObject->getCombinedIdentifier());
-            }
+            // The icon - will be linked later on, if not locked
+            $theIcon = $this->getFileOrFolderIcon($folderName, $folderObject);
 
             // Preparing and getting the data-array
-            $theData = [
-                'type' => 'folder',
-                'folder-identifier' => htmlspecialchars($folderObject->getIdentifier()),
-                'combined-identifier' => htmlspecialchars($folderObject->getCombinedIdentifier()),
+            $theData = [];
+
+            // Preparing table row attributes
+            $attributes = [
+                'data-type' => 'folder',
+                'data-folder-identifier' => $folderObject->getIdentifier(),
+                'data-combined-identifier' => $folderObject->getCombinedIdentifier(),
             ];
+            if ($this->clipObj->current !== 'normal'
+                && $this->clipObj->isSelected('_FILE', md5($folderObject->getCombinedIdentifier()))
+            ) {
+                $attributes['class'] = 'success';
+            }
             if ($isLocked) {
                 foreach ($this->fieldArray as $field) {
                     $theData[$field] = '';
                 }
+                $theData['icon'] = $theIcon;
                 $theData['file'] = $displayName;
             } else {
                 foreach ($this->fieldArray as $field) {
@@ -600,14 +518,17 @@ class FileList
                             $tstamp = $folderObject->getModificationTime();
                             $theData[$field] = $tstamp ? BackendUtility::date($tstamp) : '-';
                             break;
+                        case 'icon':
+                            $theData[$field] = (string)BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $folderObject->getCombinedIdentifier());
+                            break;
                         case 'file':
                             $theData[$field] = $this->linkWrapDir($displayName, $folderObject);
                             break;
                         case '_CONTROL_':
                             $theData[$field] = $this->makeEdit($folderObject);
                             break;
-                        case '_CLIPBOARD_':
-                            $theData[$field] = $this->makeClipboardCheckbox($folderObject);
+                        case '_SELECTOR_':
+                            $theData[$field] = $this->makeCheckbox($folderObject);
                             break;
                         case '_REF_':
                             $theData[$field] = $this->makeRef($folderObject);
@@ -620,7 +541,7 @@ class FileList
                     }
                 }
             }
-            $out .= $this->addElement($theIcon, $theData);
+            $out .= $this->addElement($theData, $attributes);
         }
         return $out;
     }
@@ -689,6 +610,11 @@ class FileList
         return (string)$this->uriBuilder->buildUriFromRoute('file_FilelistList', $params);
     }
 
+    public function getSelectedElements(): array
+    {
+        return $this->selectedElements;
+    }
+
     protected function getAvailableSystemLanguages(): array
     {
         // first two keys are "0" (default) and "-1" (multiple), after that comes the "other languages"
@@ -717,19 +643,22 @@ class FileList
             $ext = $fileObject->getExtension();
             $fileUid = $fileObject->getUid();
             $fileName = trim($fileObject->getName());
-            // The icon with link
-            $theIcon = '<span title="' . htmlspecialchars($fileName . ' [' . $fileUid . ']') . '">'
-                . $this->iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render() . '</span>';
-            $theIcon = (string)BackendUtility::wrapClickMenuOnIcon($theIcon, 'sys_file', $fileObject->getCombinedIdentifier());
             // Preparing and getting the data-array
-            $theData = [
-                'type' => 'file',
-                'file-uid' => $fileUid
+            $theData = [];
+            // Preparing table row attributes
+            $attributes = [
+                'data-type' => 'file',
+                'data-file-uid' => $fileUid
             ];
             if ($this->isEditMetadataAllowed($fileObject)
                 && ($metaDataUid = $fileObject->getMetaData()->offsetGet('uid'))
             ) {
-                $theData['metadata-uid'] = htmlspecialchars((string)$metaDataUid);
+                $attributes['data-metadata-uid'] = (string)$metaDataUid;
+            }
+            if ($this->clipObj->current !== 'normal'
+                && $this->clipObj->isSelected('_FILE', md5($fileObject->getCombinedIdentifier()))
+            ) {
+                $attributes['class'] = 'success';
             }
             foreach ($this->fieldArray as $field) {
                 switch ($field) {
@@ -748,8 +677,8 @@ class FileList
                     case '_CONTROL_':
                         $theData[$field] = $this->makeEdit($fileObject);
                         break;
-                    case '_CLIPBOARD_':
-                        $theData[$field] = $this->makeClipboardCheckbox($fileObject);
+                    case '_SELECTOR_':
+                        $theData[$field] = $this->makeCheckbox($fileObject);
                         break;
                     case '_LOCALIZATION_':
                         if (!empty($systemLanguages) && $fileObject->isIndexed() && $fileObject->checkActionPermission('editMeta') && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata') && !empty($GLOBALS['TCA']['sys_file_metadata']['ctrl']['languageField'] ?? null)) {
@@ -803,6 +732,9 @@ class FileList
                     case '_PATH_':
                         $theData[$field] = $this->makePath($fileObject);
                         break;
+                    case 'icon':
+                        $theData[$field] = (string)BackendUtility::wrapClickMenuOnIcon($this->getFileOrFolderIcon($fileName, $fileObject), 'sys_file', $fileObject->getCombinedIdentifier());
+                        break;
                     case 'file':
                         // Edit metadata of file
                         $theData[$field] = $this->linkWrapFile(htmlspecialchars($fileName), $fileObject);
@@ -833,7 +765,7 @@ class FileList
                         }
                 }
             }
-            $out .= $this->addElement($theIcon, $theData);
+            $out .= $this->addElement($theData, $attributes);
         }
         return $out;
     }
@@ -969,28 +901,32 @@ class FileList
     }
 
     /**
-     * Adds the clipboard checkbox for a file/folder in the listing
+     * Adds the checkbox to select a file/folder in the listing
      *
      * @param File|Folder $fileOrFolderObject
      * @return string
      */
-    protected function makeClipboardCheckbox($fileOrFolderObject): string
+    protected function makeCheckbox($fileOrFolderObject): string
     {
-        if ($this->clipObj->current === 'normal' || !$fileOrFolderObject->checkActionPermission('read')) {
+        if (!$fileOrFolderObject->checkActionPermission('read')) {
             return '';
         }
 
         $fullIdentifier = $fileOrFolderObject->getCombinedIdentifier();
         $md5 = md5($fullIdentifier);
         $identifier = '_FILE|' . $md5;
+        $isSelected = $this->clipObj->isSelected('_FILE', $md5) && $this->clipObj->current !== 'normal';
         $this->CBnames[] = $identifier;
 
+        if ($isSelected) {
+            $this->selectedElements[] = $identifier;
+        }
+
         return '
-            <label class="btn btn-default btn-checkbox">
-                <input type="checkbox" name="CBC[' . $identifier . ']" value="' . htmlspecialchars($fullIdentifier) . '" ' . ($this->clipObj->isSelected('_FILE', $md5) ? ' checked="checked"' : '') . ' />
-                <span class="t3-icon fa"></span>
+            <span class="form-check form-toggle">
+                <input class="form-check-input t3js-multi-record-selection-check" type="checkbox" name="CBC[' . $identifier . ']" value="' . htmlspecialchars($fullIdentifier) . '" ' . ($isSelected ? ' checked="checked"' : '') . ' />
                 <input type="hidden" name="CBH[' . $identifier . ']" value="0" />
-            </label>';
+            </span>';
     }
 
     /**
@@ -1245,26 +1181,6 @@ class FileList
         return htmlspecialchars($folder->$method());
     }
 
-    /**
-     * Returns an instance of LanguageService
-     *
-     * @return LanguageService
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
-    }
-
-    /**
-     * Returns the current BE user.
-     *
-     * @return BackendUserAuthentication
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
-
     /**
      * Generates HTML code for a Reference tooltip out of
      * sys_refindex records you hand over
@@ -1297,4 +1213,85 @@ class FileList
             && $file->checkActionPermission('editMeta')
             && $this->getBackendUser()->check('tables_modify', 'sys_file_metadata');
     }
+
+    /**
+     * Get the icon for a file or folder object
+     *
+     * @param string $title The icon title
+     * @param File|Folder $fileOrFolderObject
+     * @return string The wrapped icon for the file or folder
+     */
+    protected function getFileOrFolderIcon(string $title, $fileOrFolderObject): string
+    {
+        return '
+            <span title="' . htmlspecialchars($title) . '">
+                ' . $this->iconFactory->getIconForResource($fileOrFolderObject, Icon::SIZE_SMALL)->render() . '
+            </span>';
+    }
+
+    /**
+     * Render convenience actions, such as "check all"
+     *
+     * @return string HTML markup for the checkbox actions
+     */
+    protected function renderCheckboxActions(): string
+    {
+        // Early return in case there are no items
+        if (!$this->totalItems) {
+            return '';
+        }
+
+        $lang = $this->getLanguageService();
+
+        $dropdownItems['checkAll'] = '
+            <li>
+                <button type="button" class="btn btn-link dropdown-item disabled" data-multi-record-selection-check-action="check-all" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '">
+                    ' . $this->iconFactory->getIcon('actions-check-square', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '
+                </button>
+            </li>';
+
+        $dropdownItems['checkNone'] = '
+            <li>
+                <button type="button" class="btn btn-link dropdown-item disabled" data-multi-record-selection-check-action="check-none" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '">
+                    ' . $this->iconFactory->getIcon('actions-square', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '
+                </button>
+            </li>';
+
+        $dropdownItems['toggleSelection'] = '
+            <li>
+                <button type="button" class="btn btn-link dropdown-item" data-multi-record-selection-check-action="toggle" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '">
+                    ' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . ' ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '
+                </button>
+            </li>';
+
+        return '
+            <div class="btn-group dropdown position-static">
+                 <button type="button" class="btn btn-borderless dropdown-toggle" data-bs-target="multi-record-selection-check-actions" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">
+                    ' . $this->iconFactory->getIcon('content-special-div', Icon::SIZE_SMALL) . '
+                </button>
+                <ul id="multi-record-selection-check-actions" class="dropdown-menu">
+                    ' . implode(PHP_EOL, $dropdownItems) . '
+                </ul>
+            </div>';
+    }
+
+    /**
+     * Returns an instance of LanguageService
+     *
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
+
+    /**
+     * Returns the current BE user.
+     *
+     * @return BackendUserAuthentication
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
 }
diff --git a/typo3/sysext/filelist/Resources/Private/Language/locallang_mod_file_list.xlf b/typo3/sysext/filelist/Resources/Private/Language/locallang_mod_file_list.xlf
index cda657c72030..8749c476b2ce 100644
--- a/typo3/sysext/filelist/Resources/Private/Language/locallang_mod_file_list.xlf
+++ b/typo3/sysext/filelist/Resources/Private/Language/locallang_mod_file_list.xlf
@@ -12,11 +12,14 @@
 			<trans-unit id="clip_pasteInto" resname="clip_pasteInto">
 				<source>Paste into: Clipboard content is inserted into this folder</source>
 			</trans-unit>
-			<trans-unit id="clip_markRecords" resname="clip_markRecords">
-				<source>Mark All/Mark none</source>
+			<trans-unit id="selection" resname="selection">
+				<source>Selection:</source>
+			</trans-unit>
+			<trans-unit id="editMarked" resname="editMarked">
+				<source>Edit Metadata</source>
 			</trans-unit>
 			<trans-unit id="clip_selectMarked" resname="clip_selectMarked">
-				<source>Transfer the selection of files to clipboard</source>
+				<source>Transfer to clipboard</source>
 			</trans-unit>
 			<trans-unit id="clip_deleteMarked" resname="clip_deleteMarked">
 				<source>Delete marked</source>
diff --git a/typo3/sysext/filelist/Resources/Private/Templates/File/List.html b/typo3/sysext/filelist/Resources/Private/Templates/File/List.html
index df703f9a31dc..c926a086ef23 100644
--- a/typo3/sysext/filelist/Resources/Private/Templates/File/List.html
+++ b/typo3/sysext/filelist/Resources/Private/Templates/File/List.html
@@ -29,7 +29,7 @@
 
 <f:section name="content">
     <form method="post" name="fileListForm">
-        <div class="row">
+        <div class="row mb-3">
             <div class="col-6">
                 <div class="input-group">
                     <input type="hidden" name="pointer" value="0" />
@@ -43,17 +43,47 @@
                 </div>
             </div>
         </div>
-        <div class="row justify-content-end">
-            <f:if condition="{listHtml} && {displayThumbs.enabled}">
-                <div class="col-6">
-                    <div class="float-end">
-                        <div class="form-check form-switch">
-                            {displayThumbs.html -> f:format.raw()}
-                            <label for="checkDisplayThumbs" class="form-check-label">
-                                {displayThumbs.label}
-                            </label>
+        <div class="row row-cols-auto justify-content-between">
+            <div class="col-auto">
+                <f:if condition="{listHtml} && {totalItems}">
+                    <div class="row row-cols-auto align-items-center g-2 t3js-multi-record-selection-actions {f:if(condition: '!{hasSelectedElements}', then: 'hidden')}">
+                        <div class="col">
+                            <strong><f:translate key="LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:selection"/></strong>
+                        </div>
+                        <div class="col">
+                            <button type="button" class="btn btn-default btn-sm disabled" data-multi-record-selection-action="edit" data-multi-record-selection-action-config="{editActionConfiguration}">
+                                <span title="{f:translate(key: 'LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:editMarked')}">
+                                    <core:icon identifier="actions-open" size="small" /> <f:translate key="LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:editMarked" />
+                                </span>
+                            </button>
+                        </div>
+                        <f:if condition="{enableClipBoard.enabled}">
+                            <div class="col">
+                                <button type="button" class="btn btn-default btn-sm {f:if(condition: '{enableClipBoard.mode} == normal', then: 'disabled')}" data-multi-record-selection-action="setCB" data-filelist-clipboard-cmd="setCB">
+                                    <span title="{f:translate(key: 'LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clip_selectMarked')}">
+                                        <core:icon identifier="actions-edit-copy" size="small" /> <f:translate key="LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clip_selectMarked" />
+                                    </span>
+                                </button>
+                            </div>
+                        </f:if>
+                        <div class="col">
+                            <button type="button" class="btn btn-default btn-sm t3js-modal-trigger" data-multi-record-selection-action="delete" data-event-name="filelist:clipboard:cmd" data-event-payload="delete" data-severity="warning" data-bs-content="{f:translate(key: 'LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:clip_deleteMarkedWarning')}">
+                                <span title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete')}">
+                                    <core:icon identifier="actions-edit-delete" size="small" /> <f:translate key="LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete" />
+                                </span>
+                            </button>
                         </div>
                     </div>
+                </f:if>
+            </div>
+            <f:if condition="{listHtml} && {displayThumbs.enabled}">
+                <div class="col-auto">
+                    <div class="form-check form-switch">
+                        {displayThumbs.html -> f:format.raw()}
+                        <label for="checkDisplayThumbs" class="form-check-label">
+                            {displayThumbs.label}
+                        </label>
+                    </div>
                 </div>
             </f:if>
         </div>
diff --git a/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php b/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
index ad0b85fd2e56..f20758894ae1 100644
--- a/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
+++ b/typo3/sysext/recordlist/Classes/Browser/FileBrowser.php
@@ -73,6 +73,7 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
         parent::initialize();
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Recordlist/BrowseFiles');
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tree/FileStorageBrowser');
+        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/MultiRecordSelection');
 
         $thumbnailConfig = $this->getBackendUser()->getTSConfig()['options.']['file_list.']['thumbnail.'] ?? [];
         if (isset($thumbnailConfig['width']) && MathUtility::canBeInterpretedAsInteger($thumbnailConfig['width'])) {
@@ -244,30 +245,27 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
                 <tr>
                     <th colspan="3" class="nowrap">
                         <div class="btn-group dropdown position-static me-1">
-                            <button type="button" class="btn btn-borderless dropdown-toggle" data-bs-target="actions_filebrowser" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">' .
+                            <button type="button" class="btn btn-borderless dropdown-toggle" data-bs-target="multi-record-selection-check-actions" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false">' .
                                 $this->iconFactory->getIcon('content-special-div', Icon::SIZE_SMALL) .
                             '</button>
-                            <ul id="actions_filebrowser" class="dropdown-menu">
+                            <ul id="multi-record-selection-check-actions" class="dropdown-menu">
                                 <li>
-                                    <button type="button" class="btn btn-link dropdown-item typo3-selection-toggle" data-action="select-all">' .
+                                    <button type="button" class="btn btn-link dropdown-item disabled" data-multi-record-selection-check-action="check-all" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '">' .
                                         $this->iconFactory->getIcon('actions-check-square', Icon::SIZE_SMALL) . ' ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) .
                                     '</button>
                                 </li>
                                 <li>
-                                    <button type="button" class="btn btn-link dropdown-item typo3-selection-toggle" data-action="select-none">' .
+                                    <button type="button" class="btn btn-link dropdown-item disabled" data-multi-record-selection-check-action="check-none" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '">' .
                                         $this->iconFactory->getIcon('actions-square', Icon::SIZE_SMALL) . ' ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) .
                                     '</button>
                                 </li>
                                 <li>
-                                    <button type="button" class="btn btn-link dropdown-item typo3-selection-toggle" data-action="select-toggle">' .
-                                        $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL) . ' ' . htmlspecialchars($lang->getLL('toggleSelection')) .
+                                    <button type="button" class="btn btn-link dropdown-item" data-multi-record-selection-check-action="toggle" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '">' .
+                                        $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL) . ' ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) .
                                     '</button>
                                 </li>
                             </ul>
                         </div>
-                        <button type="button" class="btn btn-default disabled" data-action="import" title="' . htmlspecialchars($lang->getLL('importSelection')) . '">' .
-                            $this->iconFactory->getIcon('actions-document-import-t3d', Icon::SIZE_SMALL) . ' ' . htmlspecialchars($lang->getLL('importSelection')) .
-                        '</button>
                     </th>
                     <th class="col-control nowrap"></th>
                 </tr>
@@ -304,7 +302,7 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
                 $ATag_e = '</a>';
                 $bulkCheckBox = '
                     <span class="form-check form-toggle">
-                        <input type="checkbox" data-file-name="' . htmlspecialchars($fileObject->getName()) . '" data-file-uid="' . $fileObject->getUid() . '" name="file_' . $fileObject->getUid() . '" value="0" autocomplete="off" class="form-check-input typo3-list-check"  />
+                        <input type="checkbox" data-file-name="' . htmlspecialchars($fileObject->getName()) . '" data-file-uid="' . $fileObject->getUid() . '" name="file_' . $fileObject->getUid() . '" value="0" autocomplete="off" class="form-check-input t3js-multi-record-selection-check"  />
                     </span>';
             } else {
                 $ATag = '';
@@ -343,8 +341,17 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
         $markup = [];
         $markup[] = '<div class="mt-4 mb-4">' . $searchBox . '</div>';
         $markup[] = '<div id="filelist">';
-        $markup[] = '  <div class="list-header">';
-        $markup[] = '   ' . $this->getBulkSelector();
+        $markup[] = '  <div class="row row-cols-auto justify-content-between list-header">';
+        $markup[] = '      <div class="col-auto">';
+        $markup[] = '          <div class="row row-cols-auto align-items-center g-2 t3js-multi-record-selection-actions hidden">';
+        $markup[] = '              <div class="col">';
+        $markup[] = '                  <button type="button" class="btn btn-default btn-sm" data-multi-record-selection-action="import" title="' . htmlspecialchars($lang->getLL('importSelection')) . '">';
+        $markup[] = '                      ' . $this->iconFactory->getIcon('actions-document-import-t3d', Icon::SIZE_SMALL) . ' ' . htmlspecialchars($lang->getLL('importSelection'));
+        $markup[] = '                  </button>';
+        $markup[] = '              </div>';
+        $markup[] = '          </div>';
+        $markup[] = '      </div>';
+        $markup[] = '      ' . $this->getThumbnailSelector();
         $markup[] = '   </div>';
         $markup[] = '   <table class="mt-1 table table-sm table-responsive table-striped table-hover" id="typo3-filelist" data-list-container="files">';
         $markup[] = '       ' . $tableHeader;
@@ -375,38 +382,33 @@ class FileBrowser extends AbstractElementBrowser implements ElementBrowserInterf
     }
 
     /**
-     * Get the HTML data required for a bulk selection of files of the TYPO3 Element Browser.
+     * Get the HTML for the thumbnail selector, if enabled
      *
-     * @return string HTML data required for a bulk selection of files
+     * @return string HTML data required for the thumbnail selector
      */
-    protected function getBulkSelector(): string
+    protected function getThumbnailSelector(): string
     {
-        $lang = $this->getLanguageService();
-        $out = '';
-
         // Getting flag for showing/not showing thumbnails:
-        $noThumbsInEB = $this->getBackendUser()->getTSConfig()['options.']['noThumbsInEB'] ?? false;
-        if (!$noThumbsInEB && $this->selectedFolder) {
-            // MENU-ITEMS, fetching the setting for thumbnails from File>List module:
-            $_MOD_MENU = ['displayThumbs' => ''];
-            $_MOD_SETTINGS = BackendUtility::getModuleData($_MOD_MENU, GeneralUtility::_GP('SET'), 'file_list');
-            $addParams = HttpUtility::buildQueryString($this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]), '&');
-            $thumbNailCheck = '<div class="form-check form-switch">'
-                . BackendUtility::getFuncCheck(
-                    '',
-                    'SET[displayThumbs]',
-                    $_MOD_SETTINGS['displayThumbs'] ?? true,
-                    $this->thisScript,
-                    $addParams,
-                    'id="checkDisplayThumbs"'
-                )
-                . '<label for="checkDisplayThumbs" class="form-check-label">'
-                . htmlspecialchars($lang->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:displayThumbs')) . '</label></div>';
-            $out .= '<div class="float-end ps-2">' . $thumbNailCheck . '</div>';
-        } else {
-            $out .= '';
+        if (!$this->selectedFolder || ($this->getBackendUser()->getTSConfig()['options.']['noThumbsInEB'] ?? false)) {
+            return '';
         }
-        return $out;
+
+        $lang = $this->getLanguageService();
+
+        // MENU-ITEMS, fetching the setting for thumbnails from File>List module:
+        $_MOD_MENU = ['displayThumbs' => ''];
+        $currentValue = BackendUtility::getModuleData($_MOD_MENU, GeneralUtility::_GP('SET'), 'file_list')['displayThumbs'] ?? true;
+        $addParams = HttpUtility::buildQueryString($this->getUrlParameters(['identifier' => $this->selectedFolder->getCombinedIdentifier()]), '&');
+
+        return '
+            <div class="col-auto">
+                <div class="form-check form-switch">
+                    ' . BackendUtility::getFuncCheck('', 'SET[displayThumbs]', $currentValue, $this->thisScript, $addParams, 'id="checkDisplayThumbs"') . '
+                    <label for="checkDisplayThumbs" class="form-check-label">
+                        ' . htmlspecialchars($lang->sL('LLL:EXT:recordlist/Resources/Private/Language/locallang_browse_links.xlf:displayThumbs')) . '
+                    </label>
+                </div>
+            </div>';
     }
 
     /**
diff --git a/typo3/sysext/recordlist/Resources/Public/JavaScript/BrowseFiles.js b/typo3/sysext/recordlist/Resources/Public/JavaScript/BrowseFiles.js
index a2dd3ec92289..e1bc77a820de 100644
--- a/typo3/sysext/recordlist/Resources/Public/JavaScript/BrowseFiles.js
+++ b/typo3/sysext/recordlist/Resources/Public/JavaScript/BrowseFiles.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-define(["require","exports","TYPO3/CMS/Backend/Utility/MessageUtility","./ElementBrowser","nprogress","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,l,s,n,o){"use strict";var c,i=TYPO3.Icons;!function(e){e.bulkItemSelector=".typo3-list-check",e.importSelectionSelector='button[data-action="import"]',e.selectionToggleSelector=".typo3-selection-toggle",e.listContainer='[data-list-container="files"]'}(c||(c={}));class r{constructor(){r.Selector=new a,new o("click",(e,t)=>{e.preventDefault(),r.insertElement(t.dataset.fileName,Number(t.dataset.fileUid),1===parseInt(t.dataset.close||"0",10))}).delegateTo(document,"[data-close]"),new o("change",r.Selector.toggleImportButton).delegateTo(document,c.bulkItemSelector),new o("click",r.Selector.handle).delegateTo(document,c.importSelectionSelector),new o("click",r.Selector.toggle).delegateTo(document,c.selectionToggleSelector),new o("change",r.Selector.toggle).delegateTo(document,c.bulkItemSelector)}static insertElement(e,t,l){return s.insertElement("sys_file",String(t),e,String(t),l)}}class a{constructor(){this.toggle=e=>{e.preventDefault();const t=e.target,l=t.dataset.action,s=this.getItems();switch(l){case"select-toggle":s.forEach(e=>{e.checked=!e.checked,e.closest("tr").classList.toggle("success")});break;case"select-all":s.forEach(e=>{e.checked=!0,e.closest("tr").classList.add("success")});break;case"select-none":s.forEach(e=>{e.checked=!1,e.closest("tr").classList.remove("success")});break;default:t.classList.contains("typo3-list-check")&&t.closest("tr").classList.toggle("success")}this.toggleImportButton()},this.handle=(e,t)=>{e.preventDefault();const l=this.getItems(),s=[];l.length&&(l.forEach(e=>{e.checked&&e.name&&e.dataset.fileName&&e.dataset.fileUid&&s.unshift({uid:e.dataset.fileUid,fileName:e.dataset.fileName})}),i.getIcon("spinner-circle",i.sizes.small,null,null,i.markupIdentifiers.inline).then(e=>{t.classList.add("disabled"),t.innerHTML=e}),this.handleSelection(s))}}getItems(){return document.querySelector(c.listContainer).querySelectorAll(c.bulkItemSelector)}toggleImportButton(){var e;const t=(null===(e=document.querySelector(c.listContainer))||void 0===e?void 0:e.querySelectorAll(c.bulkItemSelector+":checked").length)>0;document.querySelector(c.importSelectionSelector).classList.toggle("disabled",!t)}handleSelection(e){n.configure({parent:".element-browser-main-content",showSpinner:!1}),n.start();const t=1/e.length;this.handleNext(e),new o("message",o=>{if(!l.MessageUtility.verifyOrigin(o.origin))throw"Denied message sent by "+o.origin;"typo3:foreignRelation:inserted"===o.data.actionName&&(e.length>0?(n.inc(t),this.handleNext(e)):(n.done(),s.focusOpenerAndClose()))}).bindTo(window)}handleNext(e){if(e.length>0){const t=e.pop();r.insertElement(t.fileName,Number(t.uid))}}}return new r}));
\ No newline at end of file
+define(["require","exports","TYPO3/CMS/Backend/Utility/MessageUtility","./ElementBrowser","nprogress","TYPO3/CMS/Core/Event/RegularEvent"],(function(e,t,n,i,s,r){"use strict";var a=TYPO3.Icons;class l{constructor(){this.importSelection=e=>{e.preventDefault();const t=e.target,o=document.querySelectorAll(".t3js-multi-record-selection-check");if(!o.length)return;const c=[];o.forEach(e=>{e.checked&&e.name&&e.dataset.fileName&&e.dataset.fileUid&&c.unshift({uid:e.dataset.fileUid,fileName:e.dataset.fileName})}),a.getIcon("spinner-circle",a.sizes.small,null,null,a.markupIdentifiers.inline).then(e=>{t.classList.add("disabled"),t.innerHTML=e}),s.configure({parent:".element-browser-main-content",showSpinner:!1}),s.start();const d=1/c.length;l.handleNext(c),new r("message",e=>{if(!n.MessageUtility.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;"typo3:foreignRelation:inserted"===e.data.actionName&&(c.length>0?(s.inc(d),l.handleNext(c)):(s.done(),i.focusOpenerAndClose()))}).bindTo(window)},new r("click",(e,t)=>{e.preventDefault(),l.insertElement(t.dataset.fileName,Number(t.dataset.fileUid),1===parseInt(t.dataset.close||"0",10))}).delegateTo(document,"[data-close]"),new r("multiRecordSelection:action:import",this.importSelection).bindTo(document)}static insertElement(e,t,n){return i.insertElement("sys_file",String(t),e,String(t),n)}static handleNext(e){if(e.length>0){const t=e.pop();l.insertElement(t.fileName,Number(t.uid))}}}return new l}));
\ No newline at end of file
-- 
GitLab