From 08e7dc7012d5c2e86244a3811f10fdcfb9859e20 Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Tue, 23 Aug 2022 12:06:53 +0200
Subject: [PATCH] [!!!][TASK] Remove jQuery in Popover

The support for jQuery in the module `@typo3/backend/popover` has been
dropped. Passing jQuery elements to the module's methods is not possible
anymore.

Resolves: #98261
Releases: main
Change-Id: I8716ad1762d67faf51b67eacec82435690f5b097
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/75535
Tested-by: core-ci <typo3@b13.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
---
 .../TypeScript/backend/context-help.ts        |  49 +++----
 .../TypeScript/backend/form-engine-review.ts  |  38 ++---
 Build/Sources/TypeScript/backend/popover.ts   | 135 ++++++++----------
 .../TypeScript/backend/tests/popover-test.ts  |  76 +++++-----
 .../Public/JavaScript/context-help.js         |   2 +-
 .../Public/JavaScript/form-engine-review.js   |   2 +-
 .../Resources/Public/JavaScript/popover.js    |   2 +-
 .../backend/Tests/JavaScript/popover-test.js  |   2 +-
 ...ing-98261-RemovedJQueryInPopoverModule.rst |  60 ++++++++
 9 files changed, 212 insertions(+), 154 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98261-RemovedJQueryInPopoverModule.rst

diff --git a/Build/Sources/TypeScript/backend/context-help.ts b/Build/Sources/TypeScript/backend/context-help.ts
index 447f58ae5957..3edd127af497 100644
--- a/Build/Sources/TypeScript/backend/context-help.ts
+++ b/Build/Sources/TypeScript/backend/context-help.ts
@@ -12,9 +12,9 @@
  */
 
 import 'bootstrap';
-import $ from 'jquery';
 import {Popover as BootstrapPopover} from 'bootstrap';
 import Popover from './popover';
+import RegularEvent from '@typo3/core/event/regular-event';
 
 /**
  * Module: @typo3/backend/context-help
@@ -31,36 +31,37 @@ class ContextHelp {
   }
 
   public initialize(): void {
-    const $element = $(this.selector);
-    $element
-      .attr('data-bs-html', 'true')
-      .attr('data-bs-placement', this.placement)
-      .attr('data-bs-trigger', this.trigger);
-    Popover.popover($element);
+    const elements = document.querySelectorAll(this.selector);
+    elements.forEach((element: HTMLElement): void => {
+      element.dataset.bsHtml = 'true';
+      element.dataset.bsPlacement = this.placement;
+      element.dataset.bsTrigger = this.trigger;
 
-    $(document).on('show.bs.popover', this.selector, (e: Event): void => {
-      const $me = $(e.currentTarget);
-      const description = $me.data('description');
-      if (typeof description !== 'undefined' && description !== '') {
+      Popover.popover(element);
+    });
+
+    new RegularEvent('show.bs.popover', (e: Event): void => {
+      const me = e.target as HTMLElement;
+      const description = me.dataset.description;
+
+      if (!!description) {
         const options = <BootstrapPopover.Options>{
-          title: $me.data('title') || '',
+          title: me.dataset.title || '',
           content: description,
         };
-        Popover.setOptions($me, options);
+        Popover.setOptions(me, options);
       }
-    }).on('click', 'body', (e: any): void => {
-      $(this.selector).each((index: number, triggerElement: Element): void => {
-        const $triggerElement = $(triggerElement);
-        // the 'is' for buttons that trigger popups
-        // the 'has' for icons within a button that triggers a popup
-        if (!$triggerElement.is(e.target)
-          && $triggerElement.has(e.target).length === 0
-          && $('.popover').has(e.target).length === 0
-        ) {
-          Popover.hide($triggerElement);
+    }).delegateTo(document, this.selector);
+
+    new RegularEvent('click', (e: Event): void => {
+      const me = e.target as HTMLElement;
+      const elements = document.querySelectorAll(this.selector);
+      elements.forEach((element: HTMLElement): void => {
+        if (!element.isEqualNode(me)) {
+          Popover.hide(element);
         }
       });
-    });
+    }).delegateTo(document, 'body');
   }
 }
 
diff --git a/Build/Sources/TypeScript/backend/form-engine-review.ts b/Build/Sources/TypeScript/backend/form-engine-review.ts
index 063a9eabf18c..ee714c65b97c 100644
--- a/Build/Sources/TypeScript/backend/form-engine-review.ts
+++ b/Build/Sources/TypeScript/backend/form-engine-review.ts
@@ -50,17 +50,20 @@ class FormEngineReview {
    * @param {Object} context
    */
   public static attachButtonToModuleHeader(context: any): void {
-    const $leastButtonBar: any = $('.t3js-module-docheader-bar-buttons').children().last().find('[role="toolbar"]');
-    const $button: any = $('<a />', {
-      class: 'btn btn-danger btn-sm hidden ' + context.toggleButtonClass,
-      href: '#',
-      title: TYPO3.lang['buttons.reviewFailedValidationFields'],
-    }).append(
-      $('<typo3-backend-icon/>', {identifier: 'actions-info', size: 'small'}),
-    );
-
-    Popover.popover($button);
-    $leastButtonBar.prepend($button);
+    const leastButtonBar: HTMLElement = document.querySelector('.t3js-module-docheader-bar-buttons').lastElementChild.querySelector('[role="toolbar"]');
+
+    const icon = document.createElement('typo3-backend-icon');
+    icon.setAttribute('identifier', 'actions-info');
+    icon.setAttribute('size', 'small');
+
+    const button = document.createElement('button');
+    button.type = 'button';
+    button.classList.add('btn', 'btn-danger', 'btn-sm', 'hidden', context.toggleButtonClass);
+    button.title = TYPO3.lang['buttons.reviewFailedValidationFields'];
+    button.appendChild(icon);
+
+    Popover.popover(button);
+    leastButtonBar.prepend(button);
   }
 
   /**
@@ -89,7 +92,10 @@ class FormEngineReview {
   public checkForReviewableField = (): void => {
     const me: any = this;
     const $invalidFields: any = FormEngineReview.findInvalidField();
-    const $toggleButton: any = $('.' + this.toggleButtonClass);
+    const toggleButton: HTMLElement = document.querySelector('.' + this.toggleButtonClass);
+    if (toggleButton === null) {
+      return;
+    }
 
     if ($invalidFields.length > 0) {
       const $list: any = $('<div />', {class: 'list-group'});
@@ -107,14 +113,14 @@ class FormEngineReview {
         $list.append(link);
       });
 
-      $toggleButton.removeClass('hidden');
-      Popover.setOptions($toggleButton, <BootstrapPopover.Options>{
+      toggleButton.classList.remove('hidden');
+      Popover.setOptions(toggleButton, <BootstrapPopover.Options>{
         html: true,
         content: $list[0]
       });
     } else {
-      $toggleButton.addClass('hidden');
-      Popover.hide($toggleButton);
+      toggleButton.classList.add('hidden');
+      Popover.hide(toggleButton);
     }
   }
 
diff --git a/Build/Sources/TypeScript/backend/popover.ts b/Build/Sources/TypeScript/backend/popover.ts
index 0144cc5b8dc4..49826cd598db 100644
--- a/Build/Sources/TypeScript/backend/popover.ts
+++ b/Build/Sources/TypeScript/backend/popover.ts
@@ -11,7 +11,6 @@
  * The TYPO3 project - inspiring people to share!
  */
 
-import $ from 'jquery';
 import {Popover as BootstrapPopover} from 'bootstrap';
 
 /**
@@ -37,22 +36,20 @@ class Popover {
    */
   public initialize(selector?: string): void {
     selector = selector || this.DEFAULT_SELECTOR;
-    $(selector).each((i, el) => {
-      const popover = new BootstrapPopover(el);
-      $(el).data('typo3.bs.popover', popover);
+    document.querySelectorAll(selector).forEach((element: HTMLElement): void => {
+      this.applyTitleIfAvailable(element);
+      new BootstrapPopover(element);
     });
   }
 
   // noinspection JSMethodCanBeStatic
   /**
    * Popover wrapper function
-   *
-   * @param {JQuery} $element
    */
-  public popover($element: JQuery) {
-    $element.each((i, el) => {
-      const popover = new BootstrapPopover(el);
-      $(el).data('typo3.bs.popover', popover);
+  public popover(element: NodeListOf<HTMLElement> | HTMLElement) {
+    this.toIterable(element).forEach((element: HTMLElement): void => {
+      this.applyTitleIfAvailable(element);
+      new BootstrapPopover(element);
     });
   }
 
@@ -60,117 +57,99 @@ class Popover {
   /**
    * Set popover options on $element
    *
-   * @param {JQuery} $element
-   * @param {PopoverOptions} options
+   * @param {element: HTMLElement} element
+   * @param {BootstrapPopover.Options} options
    */
-  public setOptions($element: JQuery, options?: BootstrapPopover.Options): void {
+  public setOptions(element: HTMLElement, options?: BootstrapPopover.Options): void {
     options = options || <BootstrapPopover.Options>{};
-    options.html = true;
-    const title: string = options.title || $element.data('title') || '';
-    const content: string = options.content || $element.data('bs-content') || '';
-    $element
-      .attr('data-bs-original-title', (title as string))
-      .attr('data-bs-content', (content as string))
-      .attr('data-bs-placement', 'auto')
+    const title: string = (options.title as string) || element.dataset.title || element.dataset.bsTitle || '';
+    const content: string = (options.content as string) || element.dataset.bsContent || '';
+    element.dataset.bsTitle = title;
+    element.dataset.bsOriginalTitle = title;
+    element.dataset.bsContent = content;
+    element.dataset.bsPlacement = 'auto';
 
     delete options.title;
     delete options.content;
-    $.each(options, (key, value) => {
-      this.setOption($element, key, value);
-    });
 
-    const popover = $element.data('typo3.bs.popover');
+    const popover = BootstrapPopover.getInstance(element);
+    // @ts-ignore
     popover.setContent({
       '.popover-header': title,
       '.popover-body': content
     });
-  }
 
-  // noinspection JSMethodCanBeStatic
-  /**
-   * Set popover option on $element
-   *
-   * @param {JQuery} $element
-   * @param {String} key
-   * @param {String} value
-   */
-  public setOption($element: JQuery, key: string, value: string): void {
-    $element.each((i, el) => {
-      const popover = $(el).data('typo3.bs.popover');
-      if (popover) {
-        popover._config[key] = value;
-      }
-    });
+    for (const [optionName, optionValue] of Object.entries(options)) {
+      // @ts-ignore: using internal _config attribute
+      popover._config[optionName] = optionValue;
+    }
   }
 
   // noinspection JSMethodCanBeStatic
   /**
    * Show popover with title and content on $element
    *
-   * @param {JQuery} $element
+   * @param {element: HTMLElement} element
    */
-  public show($element: JQuery): void {
-    $element.each((i, el) => {
-      const popover = $(el).data('typo3.bs.popover');
-      if (popover) {
-        popover.show();
-      }
-    });
+  public show(element: HTMLElement): void {
+    const popover = BootstrapPopover.getInstance(element);
+    popover.show();
   }
 
   // noinspection JSMethodCanBeStatic
   /**
    * Hide popover on $element
    *
-   * @param {JQuery} $element
+   * @param {HTMLElement} element
    */
-  public hide($element: JQuery): void {
-    $element.each((i, el) => {
-      const popover = $(el).data('typo3.bs.popover');
-      if (popover) {
-        popover.hide();
-      }
-    });
+  public hide(element: HTMLElement): void {
+    const popover = BootstrapPopover.getInstance(element);
+    popover.hide();
   }
 
   // noinspection JSMethodCanBeStatic
   /**
    * Destroy popover on $element
    *
-   * @param {Object} $element
+   * @param {HTMLElement} element
    */
-  public destroy($element: JQuery): void {
-    $element.each((i, el) => {
-      const popover = $(el).data('typo3.bs.popover');
-      if (popover) {
-        popover.dispose();
-      }
-    });
+  public destroy(element: HTMLElement): void {
+    const popover = BootstrapPopover.getInstance(element);
+    popover.dispose();
   }
 
   // noinspection JSMethodCanBeStatic
   /**
    * Toggle popover on $element
    *
-   * @param {Object} $element
+   * @param {HTMLElement} element
    */
-  public toggle($element: JQuery): void {
-    $element.each((i, el) => {
-      const popover = $(el).data('typo3.bs.popover');
-      if (popover) {
-        popover.toggle();
-      }
-    });
+  public toggle(element: HTMLElement): void {
+    const popover = BootstrapPopover.getInstance(element);
+    popover.toggle();
+  }
+
+  private toIterable(element: NodeListOf<HTMLElement> | HTMLElement | unknown): NodeList | HTMLElement[] {
+    let elementList;
+    if (element instanceof HTMLElement) {
+      elementList = [element];
+    } else if (element instanceof NodeList) {
+      elementList = element;
+    } else {
+      throw `Cannot consume element of type ${element.constructor.name}, expected NodeListOf<HTMLElement> or HTMLElement`;
+    }
+
+    return elementList;
   }
 
-  // noinspection JSMethodCanBeStatic
   /**
-   * Update popover with new content
-   *
-   * @param $element
+   * If the element contains an attributes that qualifies as a title, store it as data attribute "bs-title"
    */
-  public update($element: JQuery): void {
-    $element.data('typo3.bs.popover')._popper.update();
+  private applyTitleIfAvailable(element: HTMLElement): void {
+    const title = (element.title as string) || element.dataset.title || '';
+    if (title) {
+      element.dataset.bsTitle = title;
+    }
   }
 }
 
diff --git a/Build/Sources/TypeScript/backend/tests/popover-test.ts b/Build/Sources/TypeScript/backend/tests/popover-test.ts
index 6b6f419aeaa1..c34ad1ed00f8 100644
--- a/Build/Sources/TypeScript/backend/tests/popover-test.ts
+++ b/Build/Sources/TypeScript/backend/tests/popover-test.ts
@@ -1,4 +1,3 @@
-import $ from 'jquery';
 import {Popover as BootstrapPopover} from 'bootstrap';
 import Popover from '@typo3/backend/popover';
 
@@ -7,66 +6,79 @@ describe('TYPO3/CMS/Backend/PopoverTest:', () => {
    * @test
    */
   describe('initialize', () => {
-    const $body = $('body');
-    const $element = $('<div data-bs-toggle="popover">');
-    $body.append($element);
+    const element = document.createElement('div');
+    element.dataset.bsToggle = 'popover';
+    document.body.append(element);
+
     it('works with default selector', () => {
       Popover.initialize();
-      expect($element[0].outerHTML).toBe('<div data-bs-toggle="popover"></div>');
+      expect(element.outerHTML).toBe('<div data-bs-toggle="popover"></div>');
     });
 
-    const $element2 = $('<div data-bs-toggle="popover" data-title="foo">');
-    $body.append($element2);
+    const element2 = document.createElement('div');
+    element2.dataset.bsToggle = 'popover';
+    element2.dataset.title = 'foo';
+    document.body.append(element2);
     it('works with default selector and title attribute', () => {
       Popover.initialize();
-      expect($element2[0].outerHTML).toBe('<div data-bs-toggle="popover" data-title="foo"></div>');
+      expect(element2.outerHTML).toBe('<div data-bs-toggle="popover" data-title="foo" data-bs-title="foo"></div>');
     });
 
-    const $element3 = $('<div data-bs-toggle="popover" data-bs-content="foo">');
-    $body.append($element3);
+    const element3 = document.createElement('div');
+    element3.dataset.bsToggle = 'popover';
+    element3.dataset.bsContent = 'foo';
+    document.body.append(element3);
     it('works with default selector and content attribute', () => {
       Popover.initialize();
-      expect($element3[0].outerHTML).toBe('<div data-bs-toggle="popover" data-bs-content="foo"></div>');
+      expect(element3.outerHTML).toBe('<div data-bs-toggle="popover" data-bs-content="foo"></div>');
     });
 
-    const $element4 = $('<div class="t3js-popover">');
-    $body.append($element4);
+    const element4 = document.createElement('div');
+    element4.classList.add('t3js-popover')
+    document.body.append(element4);
     it('works with custom selector', () => {
       Popover.initialize('.t3js-popover');
-      expect($element4[0].outerHTML).toBe('<div class="t3js-popover"></div>');
+      expect(element4.outerHTML).toBe('<div class="t3js-popover"></div>');
     });
   });
 
   describe('call setOptions', () => {
-    const $body = $('body');
-    const $element = $('<div class="t3js-test-set-options" data-title="foo-title" data-bs-content="foo-content">');
-    $body.append($element);
+    const element = document.createElement('div');
+    element.classList.add('t3js-test-set-options');
+    element.dataset.title = 'foo-title';
+    element.dataset.bsContent = 'foo-content';
+    document.body.append(element);
+
     it('can set title', () => {
       Popover.initialize('.t3js-test-set-options');
-      expect($element.attr('data-title')).toBe('foo-title');
-      expect($element.attr('data-bs-content')).toBe('foo-content');
-      Popover.setOptions($element, <BootstrapPopover.Options>{
+      expect(element.getAttribute('data-title')).toBe('foo-title');
+      expect(element.getAttribute('data-bs-content')).toBe('foo-content');
+      Popover.setOptions(element, <BootstrapPopover.Options>{
         'title': 'bar-title'
       });
-      expect($element.attr('data-title')).toBe('foo-title');
-      expect($element.attr('data-bs-content')).toBe('foo-content');
-      expect($element.attr('data-bs-original-title')).toBe('bar-title');
+      expect(element.getAttribute('data-title')).toBe('foo-title');
+      expect(element.getAttribute('data-bs-content')).toBe('foo-content');
+      expect(element.getAttribute('data-bs-original-title')).toBe('bar-title');
     });
-    const $element2 = $('<div class="t3js-test-set-options2" data-title="foo-title" data-bs-content="foo-content">');
-    $body.append($element2);
+
+    const element2 = document.createElement('div');
+    element2.classList.add('t3js-test-set-options2');
+    element2.dataset.title = 'foo-title';
+    element2.dataset.bsContent = 'foo-content';
+    document.body.append(element2);
 
     it('can set content', () => {
       Popover.initialize('.t3js-test-set-options2');
       // Popover must be visible before the content can be updated manually via setOptions()
-      Popover.show($element2);
-      expect($element2.attr('data-title')).toBe('foo-title');
-      expect($element2.attr('data-bs-content')).toBe('foo-content');
-      Popover.setOptions($element2, <BootstrapPopover.Options>{
+      Popover.show(element2);
+      expect(element2.getAttribute('data-title')).toBe('foo-title');
+      expect(element2.getAttribute('data-bs-content')).toBe('foo-content');
+      Popover.setOptions(element2, <BootstrapPopover.Options>{
         'content': 'bar-content'
       });
-      expect($element2.attr('data-title')).toBe('foo-title');
-      expect($element2.attr('data-bs-content')).toBe('bar-content');
-      expect($element2.attr('data-bs-original-title')).toBe('foo-title');
+      expect(element2.getAttribute('data-title')).toBe('foo-title');
+      expect(element2.getAttribute('data-bs-content')).toBe('bar-content');
+      expect(element2.getAttribute('data-bs-original-title')).toBe('foo-title');
     });
   });
 });
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/context-help.js b/typo3/sysext/backend/Resources/Public/JavaScript/context-help.js
index b3d17ef1446f..673c501d2012 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/context-help.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/context-help.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import"bootstrap";import $ from"jquery";import Popover from"@typo3/backend/popover.js";class ContextHelp{constructor(){this.trigger="click",this.placement="auto",this.selector=".help-link",this.initialize()}initialize(){const t=$(this.selector);t.attr("data-bs-html","true").attr("data-bs-placement",this.placement).attr("data-bs-trigger",this.trigger),Popover.popover(t),$(document).on("show.bs.popover",this.selector,(t=>{const e=$(t.currentTarget),o=e.data("description");if(void 0!==o&&""!==o){const t={title:e.data("title")||"",content:o};Popover.setOptions(e,t)}})).on("click","body",(t=>{$(this.selector).each(((e,o)=>{const r=$(o);r.is(t.target)||0!==r.has(t.target).length||0!==$(".popover").has(t.target).length||Popover.hide(r)}))}))}}export default new ContextHelp;
\ No newline at end of file
+import"bootstrap";import Popover from"@typo3/backend/popover.js";import RegularEvent from"@typo3/core/event/regular-event.js";class ContextHelp{constructor(){this.trigger="click",this.placement="auto",this.selector=".help-link",this.initialize()}initialize(){document.querySelectorAll(this.selector).forEach((e=>{e.dataset.bsHtml="true",e.dataset.bsPlacement=this.placement,e.dataset.bsTrigger=this.trigger,Popover.popover(e)})),new RegularEvent("show.bs.popover",(e=>{const t=e.target,o=t.dataset.description;if(o){const e={title:t.dataset.title||"",content:o};Popover.setOptions(t,e)}})).delegateTo(document,this.selector),new RegularEvent("click",(e=>{const t=e.target;document.querySelectorAll(this.selector).forEach((e=>{e.isEqualNode(t)||Popover.hide(e)}))})).delegateTo(document,"body")}}export default new ContextHelp;
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js
index 8e7ce4854bc9..6f52ccb79ac0 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/form-engine-review.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import"bootstrap";import $ from"jquery";import FormEngine from"@typo3/backend/form-engine.js";import"@typo3/backend/element/icon-element.js";import Popover from"@typo3/backend/popover.js";class FormEngineReview{constructor(){this.toggleButtonClass="t3js-toggle-review-panel",this.labelSelector=".t3js-formengine-label",this.checkForReviewableField=()=>{const e=this,t=FormEngineReview.findInvalidField(),i=$("."+this.toggleButtonClass);if(t.length>0){const o=$("<div />",{class:"list-group"});t.each((function(){const t=$(this),i=t.find("[data-formengine-validation-rules]"),n=document.createElement("a");n.classList.add("list-group-item"),n.href="#",n.textContent=t.find(e.labelSelector).text(),n.addEventListener("click",(t=>e.switchToField(t,i))),o.append(n)})),i.removeClass("hidden"),Popover.setOptions(i,{html:!0,content:o[0]})}else i.addClass("hidden"),Popover.hide(i)},this.switchToField=(e,t)=>{e.preventDefault();e.currentTarget;t.parents('[id][role="tabpanel"]').each((function(){$('[aria-controls="'+$(this).attr("id")+'"]').tab("show")})),t.focus()},this.initialize()}static findInvalidField(){return $(document).find(".tab-content ."+FormEngine.Validation.errorClass)}static attachButtonToModuleHeader(e){const t=$(".t3js-module-docheader-bar-buttons").children().last().find('[role="toolbar"]'),i=$("<a />",{class:"btn btn-danger btn-sm hidden "+e.toggleButtonClass,href:"#",title:TYPO3.lang["buttons.reviewFailedValidationFields"]}).append($("<typo3-backend-icon/>",{identifier:"actions-info",size:"small"}));Popover.popover(i),t.prepend(i)}initialize(){const e=this,t=$(document);$((()=>{FormEngineReview.attachButtonToModuleHeader(e)})),t.on("t3-formengine-postfieldvalidation",this.checkForReviewableField)}}export default new FormEngineReview;
\ No newline at end of file
+import"bootstrap";import $ from"jquery";import FormEngine from"@typo3/backend/form-engine.js";import"@typo3/backend/element/icon-element.js";import Popover from"@typo3/backend/popover.js";class FormEngineReview{constructor(){this.toggleButtonClass="t3js-toggle-review-panel",this.labelSelector=".t3js-formengine-label",this.checkForReviewableField=()=>{const e=this,t=FormEngineReview.findInvalidField(),o=document.querySelector("."+this.toggleButtonClass);if(null!==o)if(t.length>0){const i=$("<div />",{class:"list-group"});t.each((function(){const t=$(this),o=t.find("[data-formengine-validation-rules]"),n=document.createElement("a");n.classList.add("list-group-item"),n.href="#",n.textContent=t.find(e.labelSelector).text(),n.addEventListener("click",(t=>e.switchToField(t,o))),i.append(n)})),o.classList.remove("hidden"),Popover.setOptions(o,{html:!0,content:i[0]})}else o.classList.add("hidden"),Popover.hide(o)},this.switchToField=(e,t)=>{e.preventDefault();e.currentTarget;t.parents('[id][role="tabpanel"]').each((function(){$('[aria-controls="'+$(this).attr("id")+'"]').tab("show")})),t.focus()},this.initialize()}static findInvalidField(){return $(document).find(".tab-content ."+FormEngine.Validation.errorClass)}static attachButtonToModuleHeader(e){const t=document.querySelector(".t3js-module-docheader-bar-buttons").lastElementChild.querySelector('[role="toolbar"]'),o=document.createElement("typo3-backend-icon");o.setAttribute("identifier","actions-info"),o.setAttribute("size","small");const i=document.createElement("button");i.type="button",i.classList.add("btn","btn-danger","btn-sm","hidden",e.toggleButtonClass),i.title=TYPO3.lang["buttons.reviewFailedValidationFields"],i.appendChild(o),Popover.popover(i),t.prepend(i)}initialize(){const e=this,t=$(document);$((()=>{FormEngineReview.attachButtonToModuleHeader(e)})),t.on("t3-formengine-postfieldvalidation",this.checkForReviewableField)}}export default new FormEngineReview;
\ No newline at end of file
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/popover.js b/typo3/sysext/backend/Resources/Public/JavaScript/popover.js
index 84e98a95d8e5..8fa5821ff2e4 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/popover.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/popover.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import $ from"jquery";import{Popover as BootstrapPopover}from"bootstrap";class Popover{constructor(){this.DEFAULT_SELECTOR='[data-bs-toggle="popover"]',this.initialize()}initialize(t){t=t||this.DEFAULT_SELECTOR,$(t).each(((t,o)=>{const e=new BootstrapPopover(o);$(o).data("typo3.bs.popover",e)}))}popover(t){t.each(((t,o)=>{const e=new BootstrapPopover(o);$(o).data("typo3.bs.popover",e)}))}setOptions(t,o){(o=o||{}).html=!0;const e=o.title||t.data("title")||"",p=o.content||t.data("bs-content")||"";t.attr("data-bs-original-title",e).attr("data-bs-content",p).attr("data-bs-placement","auto"),delete o.title,delete o.content,$.each(o,((o,e)=>{this.setOption(t,o,e)}));t.data("typo3.bs.popover").setContent({".popover-header":e,".popover-body":p})}setOption(t,o,e){t.each(((t,p)=>{const a=$(p).data("typo3.bs.popover");a&&(a._config[o]=e)}))}show(t){t.each(((t,o)=>{const e=$(o).data("typo3.bs.popover");e&&e.show()}))}hide(t){t.each(((t,o)=>{const e=$(o).data("typo3.bs.popover");e&&e.hide()}))}destroy(t){t.each(((t,o)=>{const e=$(o).data("typo3.bs.popover");e&&e.dispose()}))}toggle(t){t.each(((t,o)=>{const e=$(o).data("typo3.bs.popover");e&&e.toggle()}))}update(t){t.data("typo3.bs.popover")._popper.update()}}export default new Popover;
\ No newline at end of file
+import{Popover as BootstrapPopover}from"bootstrap";class Popover{constructor(){this.DEFAULT_SELECTOR='[data-bs-toggle="popover"]',this.initialize()}initialize(t){t=t||this.DEFAULT_SELECTOR,document.querySelectorAll(t).forEach((t=>{this.applyTitleIfAvailable(t),new BootstrapPopover(t)}))}popover(t){this.toIterable(t).forEach((t=>{this.applyTitleIfAvailable(t),new BootstrapPopover(t)}))}setOptions(t,e){const o=(e=e||{}).title||t.dataset.title||t.dataset.bsTitle||"",s=e.content||t.dataset.bsContent||"";t.dataset.bsTitle=o,t.dataset.bsOriginalTitle=o,t.dataset.bsContent=s,t.dataset.bsPlacement="auto",delete e.title,delete e.content;const a=BootstrapPopover.getInstance(t);a.setContent({".popover-header":o,".popover-body":s});for(const[t,o]of Object.entries(e))a._config[t]=o}show(t){BootstrapPopover.getInstance(t).show()}hide(t){BootstrapPopover.getInstance(t).hide()}destroy(t){BootstrapPopover.getInstance(t).dispose()}toggle(t){BootstrapPopover.getInstance(t).toggle()}toIterable(t){let e;if(t instanceof HTMLElement)e=[t];else{if(!(t instanceof NodeList))throw`Cannot consume element of type ${t.constructor.name}, expected NodeListOf<HTMLElement> or HTMLElement`;e=t}return e}applyTitleIfAvailable(t){const e=t.title||t.dataset.title||"";e&&(t.dataset.bsTitle=e)}}export default new Popover;
\ No newline at end of file
diff --git a/typo3/sysext/backend/Tests/JavaScript/popover-test.js b/typo3/sysext/backend/Tests/JavaScript/popover-test.js
index f738e6e1c05d..d1953289a2bb 100644
--- a/typo3/sysext/backend/Tests/JavaScript/popover-test.js
+++ b/typo3/sysext/backend/Tests/JavaScript/popover-test.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import $ from"jquery";import Popover from"@typo3/backend/popover.js";describe("TYPO3/CMS/Backend/PopoverTest:",(()=>{describe("initialize",(()=>{const t=$("body"),e=$('<div data-bs-toggle="popover">');t.append(e),it("works with default selector",(()=>{Popover.initialize(),expect(e[0].outerHTML).toBe('<div data-bs-toggle="popover"></div>')}));const o=$('<div data-bs-toggle="popover" data-title="foo">');t.append(o),it("works with default selector and title attribute",(()=>{Popover.initialize(),expect(o[0].outerHTML).toBe('<div data-bs-toggle="popover" data-title="foo"></div>')}));const a=$('<div data-bs-toggle="popover" data-bs-content="foo">');t.append(a),it("works with default selector and content attribute",(()=>{Popover.initialize(),expect(a[0].outerHTML).toBe('<div data-bs-toggle="popover" data-bs-content="foo"></div>')}));const i=$('<div class="t3js-popover">');t.append(i),it("works with custom selector",(()=>{Popover.initialize(".t3js-popover"),expect(i[0].outerHTML).toBe('<div class="t3js-popover"></div>')}))})),describe("call setOptions",(()=>{const t=$("body"),e=$('<div class="t3js-test-set-options" data-title="foo-title" data-bs-content="foo-content">');t.append(e),it("can set title",(()=>{Popover.initialize(".t3js-test-set-options"),expect(e.attr("data-title")).toBe("foo-title"),expect(e.attr("data-bs-content")).toBe("foo-content"),Popover.setOptions(e,{title:"bar-title"}),expect(e.attr("data-title")).toBe("foo-title"),expect(e.attr("data-bs-content")).toBe("foo-content"),expect(e.attr("data-bs-original-title")).toBe("bar-title")}));const o=$('<div class="t3js-test-set-options2" data-title="foo-title" data-bs-content="foo-content">');t.append(o),it("can set content",(()=>{Popover.initialize(".t3js-test-set-options2"),Popover.show(o),expect(o.attr("data-title")).toBe("foo-title"),expect(o.attr("data-bs-content")).toBe("foo-content"),Popover.setOptions(o,{content:"bar-content"}),expect(o.attr("data-title")).toBe("foo-title"),expect(o.attr("data-bs-content")).toBe("bar-content"),expect(o.attr("data-bs-original-title")).toBe("foo-title")}))}))}));
\ No newline at end of file
+import Popover from"@typo3/backend/popover.js";describe("TYPO3/CMS/Backend/PopoverTest:",(()=>{describe("initialize",(()=>{const t=document.createElement("div");t.dataset.bsToggle="popover",document.body.append(t),it("works with default selector",(()=>{Popover.initialize(),expect(t.outerHTML).toBe('<div data-bs-toggle="popover"></div>')}));const e=document.createElement("div");e.dataset.bsToggle="popover",e.dataset.title="foo",document.body.append(e),it("works with default selector and title attribute",(()=>{Popover.initialize(),expect(e.outerHTML).toBe('<div data-bs-toggle="popover" data-title="foo" data-bs-title="foo"></div>')}));const o=document.createElement("div");o.dataset.bsToggle="popover",o.dataset.bsContent="foo",document.body.append(o),it("works with default selector and content attribute",(()=>{Popover.initialize(),expect(o.outerHTML).toBe('<div data-bs-toggle="popover" data-bs-content="foo"></div>')}));const i=document.createElement("div");i.classList.add("t3js-popover"),document.body.append(i),it("works with custom selector",(()=>{Popover.initialize(".t3js-popover"),expect(i.outerHTML).toBe('<div class="t3js-popover"></div>')}))})),describe("call setOptions",(()=>{const t=document.createElement("div");t.classList.add("t3js-test-set-options"),t.dataset.title="foo-title",t.dataset.bsContent="foo-content",document.body.append(t),it("can set title",(()=>{Popover.initialize(".t3js-test-set-options"),expect(t.getAttribute("data-title")).toBe("foo-title"),expect(t.getAttribute("data-bs-content")).toBe("foo-content"),Popover.setOptions(t,{title:"bar-title"}),expect(t.getAttribute("data-title")).toBe("foo-title"),expect(t.getAttribute("data-bs-content")).toBe("foo-content"),expect(t.getAttribute("data-bs-original-title")).toBe("bar-title")}));const e=document.createElement("div");e.classList.add("t3js-test-set-options2"),e.dataset.title="foo-title",e.dataset.bsContent="foo-content",document.body.append(e),it("can set content",(()=>{Popover.initialize(".t3js-test-set-options2"),Popover.show(e),expect(e.getAttribute("data-title")).toBe("foo-title"),expect(e.getAttribute("data-bs-content")).toBe("foo-content"),Popover.setOptions(e,{content:"bar-content"}),expect(e.getAttribute("data-title")).toBe("foo-title"),expect(e.getAttribute("data-bs-content")).toBe("bar-content"),expect(e.getAttribute("data-bs-original-title")).toBe("foo-title")}))}))}));
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98261-RemovedJQueryInPopoverModule.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98261-RemovedJQueryInPopoverModule.rst
new file mode 100644
index 000000000000..8fd1c67cac29
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-98261-RemovedJQueryInPopoverModule.rst
@@ -0,0 +1,60 @@
+.. include:: /Includes.rst.txt
+
+.. _breaking-98261-1662389392:
+
+===================================================
+Breaking: #98261 - Removed jQuery in Popover module
+===================================================
+
+See :issue:`98261`
+
+Description
+===========
+
+The support for jQuery in the module :js:`@typo3/backend/popover` has been
+dropped. Passing jQuery elements to the module's methods is not possible anymore.
+
+This affects the following methods:
+
+* :js:`popover()`
+* :js:`setOptions()`
+* :js:`show()`
+* :js:`hide()`
+* :js:`destroy()`
+* :js:`toggle()`
+
+
+Impact
+======
+
+Calling any of the aforementioned methods with passing a jQuery-based object is
+undefined and will lead to JavaScript errors.
+
+
+Affected installations
+======================
+
+All 3rd party extensions using the API of the :js:`@typo3/backend/popover` module
+are affected.
+
+
+Migration
+=========
+
+The method :js:`popover()` accepts either an object of type :js:`HTMLElement`
+or a collection of type :js:`NodeList`, where all elements must be of type
+:js:`HTMLElement`.
+
+Any other method accepts objects of type :js:`HTMLElement` only.
+
+Example:
+
+.. code-block:: js
+
+    // Before
+    Popover.popover($('button.popover'));
+
+    // After
+    Popover.popover(document.querySelectorAll('button.popover'));
+
+.. index:: Backend, JavaScript, NotScanned, ext:backend
-- 
GitLab