Commit 27881b60 authored by Benjamin Franzke's avatar Benjamin Franzke Committed by Benni Mack
Browse files

[TASK] Update bootstrap javascript to 5.0.0-beta1

Bootstrap v5 – introduced in #92616 – was added with CCS from beta1 but
JavaScript from alpha2. bootstrap.bundle.js was manually wrapped
into a AMD closure, and because bootstrap 5.0.0-beta1 contains alot of
changes regarding data tags, it couldn't be updated in the initial
patch.

Bootstrap is now bundled using rollup using the ES6 sources in order
to allow for automatic updates through `grunt build`.

popperjs – previously bundled into bootstrap distributed files –
is now added as dependency. The bootstap ES6 sources, that we now use
through rollup, do not bundle this external dependency (for good reasons).

Dependency added with:

   yarn add @popperjs/core

Further adaptions contained in this change to ensure beta1 compatibility:

a) Carousel "item" to "carousel-item" class migration
b) $.fn.modal(options) does no longer imply $.fn.modal('show')
c) Fix panels, both JS and CSS (card-group can't be used here)
d) All bootstrap data- tags are migrated to data-bs-.
   (see https://github.com/twbs/bootstrap/pull/31827)
   Migrated with

   # renderes a sed substition with the help of a nested sed from all the
   # data-bs attributes that where changed in the twbs/bootstrap commit
   git grep -l data- | xargs sed -i $( \
        curl -s \
        https://patch-diff.githubusercontent.com/raw/twbs/bootstrap/pull/31827.patch | \
        sed 's/data-bs-[a-z-]*/\n&\n/g' | grep "data-bs-[a-z-]" | \
        sort | uniq | \
        sed 's/data-bs-\(.*\)\([^a-z-]\|$\)/ -e s\/data-\1\\\([^a-z-]\\\)\/data-bs-\1\\1\/g -e s\/data('"'"'\1'"'"')\/data('"'"'bs-\1'"'"')\/g/g' \
   )

   # Revert false positives from the above auto-replacement
   git checkout -- typo3/sysext/core/Documentation/Changelog/ \
        typo3/sysext/backend/Classes/Form/Container/FlexFormContainerContainer.php \
        Build/Sources/TypeScript/backend/Resources/Public/TypeScript/LiveSearch.ts \
        Build/Sources/TypeScript/backend/Resources/Public/TypeScript/FormEngineFlexForm.ts \
        Build/Sources/TypeScript/install/Resources/Public/TypeScript/Module/Settings/ExtensionConfiguration.ts \
        Build/Sources/Sass/typo3/_element_panel.scss

   (cd Build && grunt build)

Resolves: #93126
Resolves: #93123
Resolves: #93132
Related: #92616
Releases: master
Change-Id: Ie194d0f87d2c60df7b9e8a6de4893cfaaea55356
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67215

Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: default avatarMartin Kutschker <mkutschker-typo3@yahoo.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: default avatarMartin Kutschker <mkutschker-typo3@yahoo.com>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 89f2e9f8
...@@ -514,6 +514,37 @@ module.exports = function (grunt) { ...@@ -514,6 +514,37 @@ module.exports = function (grunt) {
] ]
} }
}, },
'bootstrap': {
options: {
preserveModules: false,
plugins: () => [
{
name: 'terser',
renderChunk: code => require('terser').minify(code, grunt.config.get('terser.options'))
},
{
name: 'externals',
resolveId: (source) => {
if (source === 'jquery') {
return {id: 'jquery', external: true}
}
if (source === 'bootstrap') {
return {id: 'node_modules/bootstrap/dist/js/bootstrap.esm.js'}
}
if (source === '@popperjs/core') {
return {id: 'node_modules/@popperjs/core/dist/esm/index.js'}
}
return null
}
}
]
},
files: {
'<%= paths.core %>Public/JavaScript/Contrib/bootstrap/bootstrap.js': [
'Sources/JavaScript/core/Resources/Public/JavaScript/Contrib/bootstrap.js'
]
}
}
}, },
npmcopy: { npmcopy: {
options: { options: {
......
// bootstrap needs a jQuery global to initialize $.fn
import './jquery-global.js';
export * from 'bootstrap';
...@@ -305,7 +305,9 @@ label { ...@@ -305,7 +305,9 @@ label {
@extend .card !optional; @extend .card !optional;
&-group { &-group {
@extend .card-group !optional; // .card-group is not a replacement for panel-group, therefore we build our own
display: flex;
flex-flow: column;
} }
&-heading { &-heading {
...@@ -315,10 +317,9 @@ label { ...@@ -315,10 +317,9 @@ label {
&-title { &-title {
@extend .card-title !optional; @extend .card-title !optional;
}
&-title { margin-top: 0;
@extend .card-title !optional; margin-bottom: 0;
} }
&-body { &-body {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
// <div class="panel panel-default"> // <div class="panel panel-default">
// <div class="panel-heading"> // <div class="panel-heading">
// <div class="panel-heading-right"> // <div class="panel-heading-right">
// <a href="#panelContentId" class="panel-heading-collapse" role="button" data-toggle="collapse" aria-expanded="true"> // <a href="#panelContentId" class="panel-heading-collapse" role="button" data-bs-toggle="collapse" aria-expanded="true">
// <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-view-list-collapse" data-identifier="actions-view-list-collapse"> // <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-view-list-collapse" data-identifier="actions-view-list-collapse">
// ... IconAPI ... // ... IconAPI ...
// </span> // </span>
......
...@@ -204,7 +204,7 @@ ...@@ -204,7 +204,7 @@
} }
// Collapsibles clickable on full length // Collapsibles clickable on full length
a[data-toggle="collapse"] { a[data-bs-toggle="collapse"] {
display: block; display: block;
} }
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
border-left: 2px solid $color-orange; border-left: 2px solid $color-orange;
position: relative; position: relative;
&[data-toggle=collapse] { &[data-bs-toggle=collapse] {
background-color: #333; background-color: #333;
} }
} }
......
...@@ -7,7 +7,7 @@ $option-margin-bottom: $padding-small-horizontal; ...@@ -7,7 +7,7 @@ $option-margin-bottom: $padding-small-horizontal;
} }
} }
[data-slide="localize-summary"] { [data-bs-slide="localize-summary"] {
.input-group { .input-group {
margin-bottom: $option-margin-bottom; margin-bottom: $option-margin-bottom;
......
...@@ -187,10 +187,10 @@ class AjaxDataHandler { ...@@ -187,10 +187,10 @@ class AjaxDataHandler {
// Update tooltip title // Update tooltip title
$anchorElement.tooltip('hide').one('hidden.bs.tooltip', (): void => { $anchorElement.tooltip('hide').one('hidden.bs.tooltip', (): void => {
const nextTitle = $anchorElement.data('toggleTitle'); const nextTitle = $anchorElement.data('toggleTitle');
// Bootstrap Tooltip internally uses only .attr('data-original-title') // Bootstrap Tooltip internally uses only .attr('data-bs-original-title')
$anchorElement $anchorElement
.data('toggleTitle', $anchorElement.attr('data-original-title')) .data('toggleTitle', $anchorElement.attr('data-bs-original-title'))
.attr('data-original-title', nextTitle); .attr('data-bs-original-title', nextTitle);
}); });
const $iconElement = $anchorElement.find(Identifiers.icon); const $iconElement = $anchorElement.find(Identifiers.icon);
......
...@@ -69,10 +69,10 @@ class ContextHelp { ...@@ -69,10 +69,10 @@ class ContextHelp {
const $element = $(this.selector); const $element = $(this.selector);
$element $element
.attr('data-loaded', 'false') .attr('data-loaded', 'false')
.attr('data-html', 'true') .attr('data-bs-html', 'true')
.attr('data-original-title', title) .attr('data-bs-original-title', title)
.attr('data-placement', this.placement) .attr('data-bs-placement', this.placement)
.attr('data-trigger', this.trigger); .attr('data-bs-trigger', this.trigger);
Popover.popover($element); Popover.popover($element);
$(document).on('show.bs.popover', this.selector, (e: Event): void => { $(document).on('show.bs.popover', this.selector, (e: Event): void => {
......
...@@ -78,7 +78,7 @@ class DebugConsole { ...@@ -78,7 +78,7 @@ class DebugConsole {
$('<li />', {role: 'presentation', class: 'nav-item', 'data-identifier': tabIdentifier}).append( $('<li />', {role: 'presentation', class: 'nav-item', 'data-identifier': tabIdentifier}).append(
$('<a />', { $('<a />', {
'aria-controls': tabIdentifier, 'aria-controls': tabIdentifier,
'data-toggle': 'tab', 'data-bs-toggle': 'tab',
class: 'nav-link', class: 'nav-link',
href: '#' + tabIdentifier, href: '#' + tabIdentifier,
role: 'tab', role: 'tab',
......
...@@ -29,7 +29,7 @@ import Severity = require('../../Severity'); ...@@ -29,7 +29,7 @@ import Severity = require('../../Severity');
import Utility = require('../../Utility'); import Utility = require('../../Utility');
enum Selectors { enum Selectors {
toggleSelector = '[data-toggle="formengine-inline"]', toggleSelector = '[data-bs-toggle="formengine-inline"]',
controlSectionSelector = '.t3js-formengine-irre-control', controlSectionSelector = '.t3js-formengine-irre-control',
createNewRecordButtonSelector = '.t3js-create-new-button', createNewRecordButtonSelector = '.t3js-create-new-button',
createNewRecordBySelectorSelector = '.t3js-create-new-selector', createNewRecordBySelectorSelector = '.t3js-create-new-selector',
......
...@@ -322,7 +322,7 @@ class ImageManipulation { ...@@ -322,7 +322,7 @@ class ImageManipulation {
* Assign EventListener to aspectRatioTrigger * Assign EventListener to aspectRatioTrigger
*/ */
this.aspectRatioTrigger.off('click').on('click', (e: JQueryEventObject): void => { this.aspectRatioTrigger.off('click').on('click', (e: JQueryEventObject): void => {
const ratioId: string = $(e.currentTarget).attr('data-option'); const ratioId: string = $(e.currentTarget).attr('data-bs-option');
const temp: CropVariant = $.extend(true, {}, this.currentCropVariant); const temp: CropVariant = $.extend(true, {}, this.currentCropVariant);
const ratio: Ratio = temp.allowedAspectRatios[ratioId]; const ratio: Ratio = temp.allowedAspectRatios[ratioId];
this.setAspectRatio(ratio); this.setAspectRatio(ratio);
...@@ -448,7 +448,7 @@ class ImageManipulation { ...@@ -448,7 +448,7 @@ class ImageManipulation {
if (this.currentCropVariant.selectedRatio) { if (this.currentCropVariant.selectedRatio) {
// set data explicitly or setAspectRatio up-scales the crop // set data explicitly or setAspectRatio up-scales the crop
this.currentModal.find(`[data-option='${this.currentCropVariant.selectedRatio}']`).addClass('active'); this.currentModal.find(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`).addClass('active');
} }
} }
...@@ -507,8 +507,8 @@ class ImageManipulation { ...@@ -507,8 +507,8 @@ class ImageManipulation {
private update(cropVariant: CropVariant): void { private update(cropVariant: CropVariant): void {
const temp: CropVariant = $.extend(true, {}, cropVariant); const temp: CropVariant = $.extend(true, {}, cropVariant);
const selectedRatio: Ratio = cropVariant.allowedAspectRatios[cropVariant.selectedRatio]; const selectedRatio: Ratio = cropVariant.allowedAspectRatios[cropVariant.selectedRatio];
this.currentModal.find('[data-option]').removeClass('active'); this.currentModal.find('[data-bs-option]').removeClass('active');
this.currentModal.find(`[data-option="${cropVariant.selectedRatio}"]`).addClass('active'); this.currentModal.find(`[data-bs-option="${cropVariant.selectedRatio}"]`).addClass('active');
/** /**
* Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data * Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
*/ */
......
...@@ -116,7 +116,7 @@ class Localization { ...@@ -116,7 +116,7 @@ class Localization {
); );
} }
slideStep1 += '<div data-toggle="buttons">' + actions.join('<hr>') + '</div>'; slideStep1 += '<div data-bs-toggle="buttons">' + actions.join('<hr>') + '</div>';
Wizard.addSlide( Wizard.addSlide(
'localize-choose-action', 'localize-choose-action',
TYPO3.lang['localize.wizard.header_page'] TYPO3.lang['localize.wizard.header_page']
...@@ -155,7 +155,7 @@ class Localization { ...@@ -155,7 +155,7 @@ class Localization {
Wizard.unlockNextStep(); Wizard.unlockNextStep();
}); });
const $languageButtons = $('<div />', {class: 'row', 'data-toggle': 'buttons'}); const $languageButtons = $('<div />', {class: 'row', 'data-bs-toggle': 'buttons'});
for (const languageObject of result) { for (const languageObject of result) {
$languageButtons.append( $languageButtons.append(
......
...@@ -105,6 +105,7 @@ class LoginRefresh { ...@@ -105,6 +105,7 @@ class LoginRefresh {
public showTimeoutModal(): void { public showTimeoutModal(): void {
this.isTimingOut = true; this.isTimingOut = true;
this.$timeoutModal.modal(this.options.modalConfig); this.$timeoutModal.modal(this.options.modalConfig);
this.$timeoutModal.modal('show');
this.fillProgressbar(this.$timeoutModal); this.fillProgressbar(this.$timeoutModal);
} }
...@@ -121,6 +122,7 @@ class LoginRefresh { ...@@ -121,6 +122,7 @@ class LoginRefresh {
*/ */
public showBackendLockedModal(): void { public showBackendLockedModal(): void {
this.$backendLockedModal.modal(this.options.modalConfig); this.$backendLockedModal.modal(this.options.modalConfig);
this.$backendLockedModal.modal('show');
} }
/** /**
...@@ -136,9 +138,12 @@ class LoginRefresh { ...@@ -136,9 +138,12 @@ class LoginRefresh {
public showLoginForm(): void { public showLoginForm(): void {
// log off for sure // log off for sure
new AjaxRequest(TYPO3.settings.ajaxUrls.logout).get().then((): void => { new AjaxRequest(TYPO3.settings.ajaxUrls.logout).get().then((): void => {
TYPO3.configuration.showRefreshLoginPopup if (TYPO3.configuration.showRefreshLoginPopup) {
? this.showLoginPopup() this.showLoginPopup();
: this.$loginForm.modal(this.options.modalConfig); } else {
this.$loginForm.modal(this.options.modalConfig);
this.$loginForm.modal('show');
}
}); });
} }
......
...@@ -378,7 +378,7 @@ class Modal { ...@@ -378,7 +378,7 @@ class Modal {
$(theDocument).on('click', '.t3js-modal-trigger', (evt: JQueryEventObject): void => { $(theDocument).on('click', '.t3js-modal-trigger', (evt: JQueryEventObject): void => {
evt.preventDefault(); evt.preventDefault();
const $element = $(evt.currentTarget); const $element = $(evt.currentTarget);
const content = $element.data('content') || 'Are you sure?'; const content = $element.data('bs-content') || 'Are you sure?';
const severity = typeof SeverityEnum[$element.data('severity')] !== 'undefined' const severity = typeof SeverityEnum[$element.data('severity')] !== 'undefined'
? SeverityEnum[$element.data('severity')] ? SeverityEnum[$element.data('severity')]
: SeverityEnum.info; : SeverityEnum.info;
...@@ -536,7 +536,7 @@ class Modal { ...@@ -536,7 +536,7 @@ class Modal {
configuration.callback(currentModal); configuration.callback(currentModal);
} }
return currentModal.modal(); return currentModal.modal('show');
} }
} }
......
...@@ -518,7 +518,7 @@ class MultiStepWizard { ...@@ -518,7 +518,7 @@ class MultiStepWizard {
return this.setup.$carousel; return this.setup.$carousel;
} }
let slides = '<div class="carousel slide" data-ride="carousel" data-interval="false">' let slides = '<div class="carousel slide" data-bs-ride="carousel" data-bs-interval="false">'
+ '<div class="carousel-inner" role="listbox">'; + '<div class="carousel-inner" role="listbox">';
for (let i = 0; i < this.setup.slides.length; ++i) { for (let i = 0; i < this.setup.slides.length; ++i) {
...@@ -528,7 +528,7 @@ class MultiStepWizard { ...@@ -528,7 +528,7 @@ class MultiStepWizard {
if (typeof slideContent === 'object') { if (typeof slideContent === 'object') {
slideContent = slideContent.html(); slideContent = slideContent.html();
} }
slides += '<div class="item" data-slide="' + currentSlide.identifier + '" data-step="' + i + '">' + slideContent + '</div>'; slides += '<div class="item" data-bs-slide="' + currentSlide.identifier + '" data-step="' + i + '">' + slideContent + '</div>';
} }
slides += '</div></div>'; slides += '</div></div>';
......
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
*/ */
import $ from 'jquery'; import $ from 'jquery';
// @todo Importing bootstrap here, to have jQuery.fn.alert applied
import 'bootstrap';
import {AbstractAction} from './ActionButton/AbstractAction'; import {AbstractAction} from './ActionButton/AbstractAction';
import {SeverityEnum} from './Enum/Severity'; import {SeverityEnum} from './Enum/Severity';
import Severity = require('./Severity'); import Severity = require('./Severity');
...@@ -139,7 +141,7 @@ class Notification { ...@@ -139,7 +141,7 @@ class Notification {
const $box = $( const $box = $(
'<div id="' + notificationId + '" class="alert alert-' + className + ' alert-dismissible fade" role="alert">' + '<div id="' + notificationId + '" class="alert alert-' + className + ' alert-dismissible fade" role="alert">' +
'<button type="button" class="close" data-dismiss="alert">' + '<button type="button" class="close" data-bs-dismiss="alert">' +
'<span aria-hidden="true"><i class="fa fa-times-circle"></i></span>' + '<span aria-hidden="true"><i class="fa fa-times-circle"></i></span>' +
'<span class="sr-only">Close</span>' + '<span class="sr-only">Close</span>' +
'</button>' + '</button>' +
......
...@@ -12,8 +12,6 @@ ...@@ -12,8 +12,6 @@
*/ */
import $ from 'jquery'; import $ from 'jquery';
// @todo Importing bootstrap here, to have jQuery.fn.popup applied
import 'bootstrap';
import {Popover as BootstrapPopover} from 'bootstrap'; import {Popover as BootstrapPopover} from 'bootstrap';
/** /**
...@@ -28,7 +26,7 @@ class Popover { ...@@ -28,7 +26,7 @@ class Popover {
* *
* @return {string} * @return {string}
*/ */
private readonly DEFAULT_SELECTOR: string = '[data-toggle="popover"]'; private readonly DEFAULT_SELECTOR: string = '[data-bs-toggle="popover"]';
constructor() { constructor() {
this.initialize(); this.initialize();
...@@ -39,7 +37,10 @@ class Popover { ...@@ -39,7 +37,10 @@ class Popover {
*/ */
public initialize(selector?: string): void { public initialize(selector?: string): void {
selector = selector || this.DEFAULT_SELECTOR; selector = selector || this.DEFAULT_SELECTOR;
$(selector).popover(); $(selector).each((i, el) => {
const popover = new BootstrapPopover(el)
$(el).data('typo3.bs.popover', popover);
});
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
...@@ -48,8 +49,11 @@ class Popover { ...@@ -48,8 +49,11 @@ class Popover {
* *
* @param {JQuery} $element * @param {JQuery} $element
*/ */
public popover($element: JQuery): void { public popover($element: JQuery) {
$element.popover(); $element.each((i, el) => {
const popover = new BootstrapPopover(el)
$(el).data('typo3.bs.popover', popover);
});
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
...@@ -62,12 +66,15 @@ class Popover { ...@@ -62,12 +66,15 @@ class Popover {
public setOptions($element: JQuery, options?: BootstrapPopover.Options): void { public setOptions($element: JQuery, options?: BootstrapPopover.Options): void {
options = options || <BootstrapPopover.Options>{}; options = options || <BootstrapPopover.Options>{};
const title: string|(() => void) = options.title || $element.data('title') || ''; const title: string|(() => void) = options.title || $element.data('title') || '';
const content: string|(() => void) = options.content || $element.data('content') || ''; const content: string|(() => void) = options.content || $element.data('bs-content') || '';
$element $element
.attr('data-original-title', (title as string)) .attr('data-bs-original-title', (title as string))
.attr('data-content', (content as string)) .attr('data-bs-content', (content as string))
.attr('data-placement', 'auto') .attr('data-bs-placement', 'auto')
.popover(options);
$.each(options, (key, value) => {
this.setOption($element, key, value);
});
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
...@@ -79,7 +86,16 @@ class Popover { ...@@ -79,7 +86,16 @@ class Popover {
* @param {String} value * @param {String} value
*/ */
public setOption($element: JQuery, key: string, value: string): void { public setOption($element: JQuery, key: string, value: string): void {
$element.data('bs.popover').options[key] = value; if (key === 'content') {
$element.attr('data-bs-content', value);
} else {
$element.each((i, el) => {
const popover = $(el).data('typo3.bs.popover');
if (popover) {
popover.config[key] = value;
}
});
}
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
...@@ -89,7 +105,12 @@ class Popover { ...@@ -89,7 +105,12 @@ class Popover {
* @param {JQuery} $element * @param {JQuery} $element
*/ */
public show($element: JQuery): void { public show($element: JQuery): void {
$element.popover('show'); $element.each((i, el) => {
const popover = $(el).data('typo3.bs.popover');
if (popover) {
popover.show();
}
});
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
...@@ -99,7 +120,12 @@ class Popover { ...@@ -99,7 +120,12 @@ class Popover {
* @param {JQuery} $element * @param {JQuery} $element
*/ */
public hide($element: JQuery): void { public hide($element: JQuery): void {
$element.popover('hide'); $element.each((i, el) => {
const popover = $(el).data('typo3.bs.popover');
if (popover) {
popover.hide();
}
});
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
...@@ -109,7 +135,12 @@ class Popover { ...@@ -109,7 +135,12 @@ class Popover {
* @param {Object} $element