From a8526b7b2efdf7d61d5ae67cb6c1a1851062179b Mon Sep 17 00:00:00 2001 From: Andreas Kienast <a.fernandez@scripting-base.de> Date: Tue, 20 Feb 2024 09:43:56 +0100 Subject: [PATCH] [TASK] Migrate Localization wizard to MultiStepWizard component In order to deprecate the inferior Wizard component introduced back in TYPO3 v7, the Localization wizard is migrated to the MultiStepWizard component. In the same run, the opportunity to cleanup the code a bit is taken: * replace `.then()` callback hell with async/await * use setting "API" of MSW instead of local properties * steps that don't allow interaction are skipped from composition Also, some minor quirks in MSW are fixed without doing major refactorings: * `forceSelection` is checked and enforced before running callbacks that may unlock any buttons, otherwise leading to a unresolvable situation * the "previous" button is now automatically (un)locked, depending on whether there are previous steps * the content of the slide that is slid to is reset as it may have been altered in a slide callback Resolves: #103155 Releases: main Change-Id: I35d5d4a14b7635c0df3f5248491f0cc45074c430 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83040 Tested-by: Andreas Nedbal <andy@pixelde.su> Tested-by: Oliver Bartsch <bo@cedev.de> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Andreas Nedbal <andy@pixelde.su> --- .../TypeScript/backend/localization.ts | 556 +++++++++--------- .../TypeScript/backend/multi-step-wizard.ts | 55 +- .../Private/Language/locallang_layout.xlf | 22 +- .../Public/JavaScript/localization.js | 2 +- .../Public/JavaScript/multi-step-wizard.js | 2 +- 5 files changed, 333 insertions(+), 304 deletions(-) diff --git a/Build/Sources/TypeScript/backend/localization.ts b/Build/Sources/TypeScript/backend/localization.ts index 4333cc736aa9..a627a8b83674 100644 --- a/Build/Sources/TypeScript/backend/localization.ts +++ b/Build/Sources/TypeScript/backend/localization.ts @@ -17,7 +17,8 @@ import { AjaxResponse } from '@typo3/core/ajax/ajax-response'; import { SeverityEnum } from './enum/severity'; import AjaxRequest from '@typo3/core/ajax/ajax-request'; import Icons from './icons'; -import Wizard from './wizard'; +import Modal, { ModalElement } from './modal'; +import MultiStepWizard, { MultiStepWizardSettings } from './multi-step-wizard'; import '@typo3/backend/element/icon-element'; type LanguageRecord = { @@ -44,9 +45,6 @@ type SummaryRecord = { class Localization { private readonly triggerButton: string = '.t3js-localize'; - private localizationMode: string = null; - private sourceLanguage: number = null; - private records: Array<any> = []; constructor() { DocumentService.ready().then((): void => { @@ -54,279 +52,284 @@ class Localization { }); } - private initialize(): void { - Icons.getIcon('actions-localize', Icons.sizes.large).then((localizeIconMarkup: string): void => { - Icons.getIcon('actions-edit-copy', Icons.sizes.large).then((copyIconMarkup: string): void => { - $(this.triggerButton).removeClass('disabled'); - - $(document).on('click', this.triggerButton, (e: JQueryEventObject): void => { - e.preventDefault(); - - const $triggerButton = $(e.currentTarget); - const actions: Array<string> = []; - const availableLocalizationModes: Array<string> = []; - let slideStep1: string = ''; - - if ($triggerButton.data('allowTranslate')) { - actions.push( - '<div class="row">' - + '<div class="col-sm-3">' - + '<label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-translate">' - + localizeIconMarkup - + '<input type="radio" name="mode" id="mode_translate" value="localize" style="display: none">' - + '<br>' + TYPO3.lang['localize.wizard.button.translate'] + '</label>' - + '</div>' - + '<div class="col-sm-9">' - + '<p class="t3js-helptext t3js-helptext-translate text-body-secondary">' + TYPO3.lang['localize.educate.translate'] + '</p>' - + '</div>' - + '</div>', - ); - availableLocalizationModes.push('localize'); + private async initialize(): Promise<void> { + const localizeIconMarkup = await Icons.getIcon('actions-localize', Icons.sizes.large); + const copyIconMarkup = await Icons.getIcon('actions-edit-copy', Icons.sizes.large); + + $(this.triggerButton).removeClass('disabled'); + + $(document).on('click', this.triggerButton, async (e: JQueryEventObject): Promise<void> => { + e.preventDefault(); + + const $triggerButton = $(e.currentTarget); + const actions: Array<string> = []; + const availableLocalizationModes: Array<string> = []; + let slideStep1: string = ''; + + if ($triggerButton.data('allowTranslate') === 0 && $triggerButton.data('allowCopy') === 0) { + Modal.confirm( + TYPO3.lang['window.localization.mixed_mode.title'], + TYPO3.lang['window.localization.mixed_mode.message'], + SeverityEnum.warning, + [ + { + text: TYPO3?.lang?.['button.ok'] || 'OK', + btnClass: 'btn-warning', + name: 'ok', + trigger: (e: Event, modal: ModalElement): void => modal.hideModal() + } + ] + ); + return; + } + + const availableLanguages: LanguageRecord[] = await (await this.loadAvailableLanguages( + parseInt($triggerButton.data('pageId'), 10), + parseInt($triggerButton.data('languageId'), 10), + )).resolve(); + + if ($triggerButton.data('allowTranslate')) { + actions.push( + '<div class="row">' + + '<div class="col-sm-3">' + + '<label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-translate">' + + localizeIconMarkup + + '<input type="radio" name="mode" id="mode_translate" value="localize" style="display: none">' + + '<br>' + TYPO3.lang['localize.wizard.button.translate'] + '</label>' + + '</div>' + + '<div class="col-sm-9">' + + '<p class="t3js-helptext t3js-helptext-translate text-body-secondary">' + TYPO3.lang['localize.educate.translate'] + '</p>' + + '</div>' + + '</div>', + ); + availableLocalizationModes.push('localize'); + } + + if ($triggerButton.data('allowCopy')) { + actions.push( + '<div class="row">' + + '<div class="col-sm-3">' + + '<label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-copy">' + + copyIconMarkup + + '<input type="radio" name="mode" id="mode_copy" value="copyFromLanguage" style="display: none">' + + '<br>' + TYPO3.lang['localize.wizard.button.copy'] + '</label>' + + '</div>' + + '<div class="col-sm-9">' + + '<p class="t3js-helptext t3js-helptext-copy text-body-secondary">' + TYPO3.lang['localize.educate.copy'] + '</p>' + + '</div>' + + '</div>', + ); + availableLocalizationModes.push('copyFromLanguage'); + } + + if (availableLocalizationModes.length === 1) { + MultiStepWizard.set('localizationMode', availableLocalizationModes[0]); + } else { + slideStep1 += '<div data-bs-toggle="buttons">' + actions.join('') + '</div>'; + MultiStepWizard.addSlide( + 'localize-choose-action', + TYPO3.lang['localize.wizard.header_page'] + .replace('{0}', $triggerButton.data('page')) + .replace('{1}', $triggerButton.data('languageName')), + slideStep1, + SeverityEnum.notice, + TYPO3.lang['localize.wizard.step.selectMode'], + ($slide: JQuery, settings: MultiStepWizardSettings): void => { + if (settings.localizationMode !== undefined) { + MultiStepWizard.unlockNextStep(); + } } + ); + } + + if (availableLanguages.length === 1) { + MultiStepWizard.set('sourceLanguage', availableLanguages[0].uid); + } else { + MultiStepWizard.addSlide( + 'localize-choose-language', + TYPO3.lang['localize.view.chooseLanguage'], + '', + SeverityEnum.notice, + TYPO3.lang['localize.wizard.step.chooseLanguage'], + async ($slide: JQuery, settings: MultiStepWizardSettings): Promise<void> => { + if (settings.sourceLanguage !== undefined) { + MultiStepWizard.unlockNextStep(); + } - if ($triggerButton.data('allowCopy')) { - actions.push( - '<div class="row">' - + '<div class="col-sm-3">' - + '<label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-copy">' - + copyIconMarkup - + '<input type="radio" name="mode" id="mode_copy" value="copyFromLanguage" style="display: none">' - + '<br>' + TYPO3.lang['localize.wizard.button.copy'] + '</label>' - + '</div>' - + '<div class="col-sm-9">' - + '<p class="t3js-helptext t3js-helptext-copy text-body-secondary">' + TYPO3.lang['localize.educate.copy'] + '</p>' - + '</div>' - + '</div>', - ); - availableLocalizationModes.push('copyFromLanguage'); - } + $slide.html('<div class="text-center">' + (await Icons.getIcon('spinner-circle', Icons.sizes.large)) + '</div>'); + MultiStepWizard.getComponent().on('click', '.t3js-language-option', (optionEvt: JQueryEventObject): void => { + const $me = $(optionEvt.currentTarget); + const $radio = $me.prev(); - if ($triggerButton.data('allowTranslate') === 0 && $triggerButton.data('allowCopy') === 0) { - actions.push( - '<div class="row">' - + '<div class="col-sm-12">' - + '<div class="alert alert-warning">' - + '<div class="media">' - + '<div class="media-left">' - + '<span class="icon-emphasized"><typo3-backend-icon identifier="actions-exclamation" size="small"></typo3-backend-icon></span>' - + '</div>' - + '<div class="media-body">' - + '<p class="alert-message">' + TYPO3.lang['localize.educate.noTranslate'] + '</p>' - + '</div>' - + '</div>' - + '</div>' - + '</div>' - + '</div>', - ); - } + MultiStepWizard.set('sourceLanguage', $radio.val()); + MultiStepWizard.unlockNextStep(); + }); - slideStep1 += '<div data-bs-toggle="buttons">' + actions.join('') + '</div>'; - Wizard.addSlide( - 'localize-choose-action', - TYPO3.lang['localize.wizard.header_page'] - .replace('{0}', $triggerButton.data('page')) - .replace('{1}', $triggerButton.data('languageName')), - slideStep1, - SeverityEnum.notice, - (): void => { - if (availableLocalizationModes.length === 1) { - // In case only one mode is available, select the mode and continue - this.localizationMode = availableLocalizationModes[0]; - Wizard.unlockNextStep().get(0).click(); - } - } - ); - Wizard.addSlide( - 'localize-choose-language', - TYPO3.lang['localize.view.chooseLanguage'], - '', - SeverityEnum.notice, - ($slide: JQuery): void => { - Icons.getIcon('spinner-circle', Icons.sizes.large).then((markup: string): void => { - $slide.html('<div class="text-center">' + markup + '</div>'); - - this.loadAvailableLanguages( - parseInt($triggerButton.data('pageId'), 10), - parseInt($triggerButton.data('languageId'), 10), - ).then(async (response: AjaxResponse): Promise<void> => { - const result: Array<LanguageRecord> = await response.resolve(); - if (result.length === 1) { - // We only have one result, auto select the record and continue - this.sourceLanguage = result[0].uid; - Wizard.unlockNextStep().get(0).click(); - return; - } - - Wizard.getComponent().on('click', '.t3js-language-option', (optionEvt: JQueryEventObject): void => { - const $me = $(optionEvt.currentTarget); - const $radio = $me.prev(); - - this.sourceLanguage = $radio.val(); - Wizard.unlockNextStep(); - }); - - const $languageButtons = $('<div />', { class: 'row' }); - - for (const languageObject of result) { - const id: string = 'language' + languageObject.uid; - const $input: JQuery = $('<input />', { - type: 'radio', - name: 'language', - id: id, - value: languageObject.uid, - style: 'display: none;', - class: 'btn-check' - }); - const $label: JQuery = $('<label />', { class: 'btn btn-default d-block t3js-language-option option', 'for': id }) - .text(' ' + languageObject.title) - .prepend(languageObject.flagIcon); - - $languageButtons.append( - $('<div />', { class: 'col-sm-4' }) - .append($input) - .append($label), - ); - } - $slide.empty().append($languageButtons); - }); - }); - }, - ); - Wizard.addSlide( - 'localize-summary', - TYPO3.lang['localize.view.summary'], - '', - SeverityEnum.notice, ($slide: JQuery): void => { - Icons.getIcon('spinner-circle', Icons.sizes.large).then((markup: string): void => { - $slide.html('<div class="text-center">' + markup + '</div>'); + const $languageButtons = $('<div />', { class: 'row' }); + + for (const languageObject of availableLanguages) { + const id: string = 'language' + languageObject.uid; + const $input: JQuery = $('<input />', { + type: 'radio', + name: 'language', + id: id, + value: languageObject.uid, + style: 'display: none;', + class: 'btn-check' }); - this.getSummary( - parseInt($triggerButton.data('pageId'), 10), - parseInt($triggerButton.data('languageId'), 10), - ).then(async (response: AjaxResponse): Promise<void> => { - const result: SummaryRecord = await response.resolve(); - $slide.empty(); - this.records = []; - - const columns = result.columns.columns; - const columnList = result.columns.columnList; - - columnList.forEach((colPos: number): void => { - if (typeof result.records[colPos] === 'undefined') { - return; - } - - const column = columns[colPos]; - const $row = $('<div />', { class: 'row' }); - - result.records[colPos].forEach((record: SummaryColPosRecord): void => { - const label = ' (' + record.uid + ') ' + record.title; - this.records.push(record.uid); - - $row.append( - $('<div />', { 'class': 'col-sm-6' }).append( - $('<div />', { 'class': 'input-group' }).append( - $('<span />', { 'class': 'input-group-text' }).append( - $('<input />', { - type: 'checkbox', - 'class': 't3js-localization-toggle-record', - id: 'record-uid-' + record.uid, - checked: 'checked', - 'data-uid': record.uid, - 'aria-label': label, - }), - ), - $('<label />', { - 'class': 'form-control', - for: 'record-uid-' + record.uid, - }).text(label).prepend(record.icon), - ), - ), - ); - }); - - $slide.append( - $('<fieldset />', { - 'class': 'localization-fieldset', - }).append( - $('<label />').text(column).prepend( - $('<input />', { - 'class': 't3js-localization-toggle-column', - type: 'checkbox', - checked: 'checked', - }), - ), - $row, + const $label: JQuery = $('<label />', { + class: 'btn btn-default d-block t3js-language-option option', + 'for': id + }) + .text(' ' + languageObject.title) + .prepend(languageObject.flagIcon); + + $languageButtons.append( + $('<div />', { class: 'col-sm-4' }) + .append($input) + .append($label), + ); + } + $slide.empty().append($languageButtons); + }, + ); + } + MultiStepWizard.addSlide( + 'localize-summary', + TYPO3.lang['localize.view.summary'], + '', + SeverityEnum.notice, + TYPO3.lang['localize.wizard.step.selectRecords'], + async ($slide: JQuery, settings: MultiStepWizardSettings): Promise<void> => { + $slide.empty().html('<div class="text-center">' + (await Icons.getIcon('spinner-circle', Icons.sizes.large)) + '</div>'); + + const result: SummaryRecord = await (await this.getSummary( + parseInt($triggerButton.data('pageId'), 10), + parseInt($triggerButton.data('languageId'), 10), + settings.sourceLanguage + )).resolve(); + + $slide.empty(); + + MultiStepWizard.set('records', []); + + const columns = result.columns.columns; + const columnList = result.columns.columnList; + + columnList.forEach((colPos: number): void => { + if (typeof result.records[colPos] === 'undefined') { + return; + } + + const column = columns[colPos]; + const $row = $('<div />', { class: 'row' }); + + result.records[colPos].forEach((record: SummaryColPosRecord): void => { + const label = ' (' + record.uid + ') ' + record.title; + settings.records.push(record.uid); + + $row.append( + $('<div />', { 'class': 'col-sm-6' }).append( + $('<div />', { 'class': 'input-group' }).append( + $('<span />', { 'class': 'input-group-text' }).append( + $('<input />', { + type: 'checkbox', + 'class': 't3js-localization-toggle-record', + id: 'record-uid-' + record.uid, + checked: 'checked', + 'data-uid': record.uid, + 'aria-label': label, + }), ), - ); - }); - - Wizard.unlockNextStep(); - - Wizard.getComponent().on('change', '.t3js-localization-toggle-record', (cmpEvt: JQueryEventObject): void => { - const $me = $(cmpEvt.currentTarget); - const uid = $me.data('uid'); - const $parent = $me.closest('fieldset'); - const $columnCheckbox = $parent.find('.t3js-localization-toggle-column'); - - if ($me.is(':checked')) { - this.records.push(uid); - } else { - const index = this.records.indexOf(uid); - if (index > -1) { - this.records.splice(index, 1); - } - } - - const $allChildren = $parent.find('.t3js-localization-toggle-record'); - const $checkedChildren = $parent.find('.t3js-localization-toggle-record:checked'); - - $columnCheckbox.prop('checked', $checkedChildren.length > 0); - $columnCheckbox.prop('__indeterminate', $checkedChildren.length > 0 && $checkedChildren.length < $allChildren.length); - - if (this.records.length > 0) { - Wizard.unlockNextStep(); - } else { - Wizard.lockNextStep(); - } - }).on('change', '.t3js-localization-toggle-column', (toggleEvt: JQueryEventObject): void => { - const $me = $(toggleEvt.currentTarget); - const $children = $me.closest('fieldset').find('.t3js-localization-toggle-record'); - - $children.prop('checked', $me.is(':checked')); - $children.trigger('change'); - }); - }); - }, - ); - - Wizard.addFinalProcessingSlide((): void => { - this.localizeRecords( - parseInt($triggerButton.data('pageId'), 10), - parseInt($triggerButton.data('languageId'), 10), - this.records, - ).then((): void => { - Wizard.dismiss(); - document.location.reload(); + $('<label />', { + 'class': 'form-control', + for: 'record-uid-' + record.uid, + }).text(label).prepend(record.icon), + ), + ), + ); }); - }).then((): void => { - Wizard.show(); - Wizard.getComponent().on('click', '.t3js-localization-option', (optionEvt: JQueryEventObject): void => { - const $me = $(optionEvt.currentTarget); - const $radio = $me.find('input[type="radio"]'); - - if ($me.data('helptext')) { - const $container = $(optionEvt.delegateTarget); - $container.find('.t3js-localization-option').removeClass('active'); - $container.find('.t3js-helptext').addClass('text-body-secondary'); - $me.addClass('active'); - $container.find($me.data('helptext')).removeClass('text-body-secondary'); + $slide.append( + $('<fieldset />', { + 'class': 'localization-fieldset', + }).append( + $('<label />').text(column).prepend( + $('<input />', { + 'class': 't3js-localization-toggle-column', + type: 'checkbox', + checked: 'checked', + }), + ), + $row, + ), + ); + }); + + MultiStepWizard.unlockNextStep(); + + MultiStepWizard.getComponent().on('change', '.t3js-localization-toggle-record', (cmpEvt: JQueryEventObject): void => { + const $me = $(cmpEvt.currentTarget); + const uid = $me.data('uid'); + const $parent = $me.closest('fieldset'); + const $columnCheckbox = $parent.find('.t3js-localization-toggle-column'); + + if ($me.is(':checked')) { + settings.records.push(uid); + } else { + const index = settings.records.indexOf(uid); + if (index > -1) { + settings.records.splice(index, 1); } - this.localizationMode = $radio.val(); - Wizard.unlockNextStep(); - }); + } + + const $allChildren = $parent.find('.t3js-localization-toggle-record'); + const $checkedChildren = $parent.find('.t3js-localization-toggle-record:checked'); + + $columnCheckbox.prop('checked', $checkedChildren.length > 0); + $columnCheckbox.prop('__indeterminate', $checkedChildren.length > 0 && $checkedChildren.length < $allChildren.length); + + if (settings.records.length > 0) { + MultiStepWizard.unlockNextStep(); + } else { + MultiStepWizard.lockNextStep(); + } + }).on('change', '.t3js-localization-toggle-column', (toggleEvt: JQueryEventObject): void => { + const $me = $(toggleEvt.currentTarget); + const $children = $me.closest('fieldset').find('.t3js-localization-toggle-record'); + + $children.prop('checked', $me.is(':checked')); + $children.trigger('change'); }); + }, + ); + + MultiStepWizard.addFinalProcessingSlide(async ($slide: JQuery, settings: MultiStepWizardSettings): Promise<void> => { + await this.localizeRecords( + parseInt($triggerButton.data('pageId'), 10), + parseInt($triggerButton.data('languageId'), 10), + settings.sourceLanguage, + settings.localizationMode, + settings.records, + ); + MultiStepWizard.dismiss(); + document.location.reload(); + }).then((): void => { + MultiStepWizard.show(); + + MultiStepWizard.getComponent().on('click', '.t3js-localization-option', (optionEvt: JQueryEventObject): void => { + const $me = $(optionEvt.currentTarget); + const $radio = $me.find('input[type="radio"]'); + + if ($me.data('helptext')) { + const $container = $(optionEvt.delegateTarget); + $container.find('.t3js-localization-option').removeClass('active'); + $container.find('.t3js-helptext').addClass('text-body-secondary'); + $me.addClass('active'); + $container.find($me.data('helptext')).removeClass('text-body-secondary'); + } + MultiStepWizard.set('localizationMode', $radio.val()); + MultiStepWizard.unlockNextStep(); }); }); }); @@ -348,33 +351,24 @@ class Localization { /** * Get summary for record processing - * - * @param {number} pageId - * @param {number} languageId - * @returns {Promise<AjaxResponse>} */ - private getSummary(pageId: number, languageId: number): Promise<AjaxResponse> { + private getSummary(pageId: number, languageId: number, sourceLanguage: number): Promise<AjaxResponse> { return new AjaxRequest(TYPO3.settings.ajaxUrls.records_localize_summary).withQueryArguments({ pageId: pageId, destLanguageId: languageId, - languageId: this.sourceLanguage, + languageId: sourceLanguage, }).get(); } /** * Localize records - * - * @param {number} pageId - * @param {number} languageId - * @param {Array<number>} uidList - * @returns {Promise<AjaxResponse>} */ - private localizeRecords(pageId: number, languageId: number, uidList: Array<number>): Promise<AjaxResponse> { + private localizeRecords(pageId: number, languageId: number, sourceLanguage: number, localizationMode: string, uidList: Array<number>): Promise<AjaxResponse> { return new AjaxRequest(TYPO3.settings.ajaxUrls.records_localize).withQueryArguments({ pageId: pageId, - srcLanguageId: this.sourceLanguage, + srcLanguageId: sourceLanguage, destLanguageId: languageId, - action: this.localizationMode, + action: localizationMode, uidList: uidList, }).get(); } diff --git a/Build/Sources/TypeScript/backend/multi-step-wizard.ts b/Build/Sources/TypeScript/backend/multi-step-wizard.ts index 2ddb31dca28b..209637d1d63f 100644 --- a/Build/Sources/TypeScript/backend/multi-step-wizard.ts +++ b/Build/Sources/TypeScript/backend/multi-step-wizard.ts @@ -20,12 +20,12 @@ import Icons from './icons'; type SlideCallback = ($slide: JQuery, settings: MultiStepWizardSettings, identifier: string) => void; -interface MultiStepWizardSettings { +export interface MultiStepWizardSettings { [key: string]: any; } interface MultiStepWizardSetup { - slides: Array<any>; + slides: Slide[]; settings: MultiStepWizardSettings; forceSelection: boolean; $carousel: JQuery; @@ -108,23 +108,22 @@ class MultiStepWizard { * @param {Function} callback * @returns {Promise<string>} */ - public addFinalProcessingSlide(callback?: SlideCallback): Promise<void> { + public async addFinalProcessingSlide(callback?: SlideCallback): Promise<void> { if (!callback) { callback = (): void => { this.dismiss(); }; } - return Icons.getIcon('spinner-circle', Icons.sizes.default, null, null).then((markup: string) => { - const $processingSlide = $('<div />', { class: 'text-center' }).append(markup); - this.addSlide( - 'final-processing-slide', top.TYPO3.lang['wizard.processing.title'], - $processingSlide[0].outerHTML, - Severity.info, - null, - callback, - ); - }); + const spinnerIcon = await Icons.getIcon('spinner-circle', Icons.sizes.large, null, null); + const $processingSlide = $('<div />', { class: 'text-center' }).append(spinnerIcon); + this.addSlide( + 'final-processing-slide', top.TYPO3.lang['wizard.processing.title'], + $processingSlide[0].outerHTML, + Severity.info, + null, + callback, + ); } /** @@ -166,6 +165,12 @@ class MultiStepWizard { }); this.getComponent().on('wizard-visible', (): void => { + if (this.setup.forceSelection) { + // @todo: This is a hack as modal buttons cannot be initially disabled. + this.lockPrevStep(); + this.lockNextStep(); + } + this.runSlideCallback(firstSlide, this.setup.$carousel.find('.carousel-item').first()); }).on('wizard-dismissed', (): void => { this.setup = $.extend(true, {}, this.originalSetup); @@ -282,11 +287,11 @@ class MultiStepWizard { const currentIndex = this.setup.$carousel.data('currentIndex'); const slide = this.setup.slides[currentIndex]; - this.runSlideCallback(slide, $(evt.relatedTarget)); - if (this.setup.forceSelection) { this.lockNextStep(); } + + this.runSlideCallback(slide, $(evt.relatedTarget)); }); // Custom event, closes the wizard @@ -331,9 +336,15 @@ class MultiStepWizard { const nextSlideNumber = this.setup.$carousel.data('currentSlide') + 1; const currentIndex = this.setup.$carousel.data('currentIndex'); const nextIndex = currentIndex + 1; + const $slideContent = $modal.find('.carousel-item:eq(' + nextIndex + ')'); + // Flush content when sliding + $slideContent.empty().append(this.setup.slides[nextIndex].content); $modalTitle.text(this.setup.slides[nextIndex].title); + // Always unlock previous step + this.unlockPrevStep(); + this.setup.$carousel.data('currentSlide', nextSlideNumber); this.setup.$carousel.data('currentIndex', nextIndex); @@ -368,12 +379,22 @@ class MultiStepWizard { const nextSlideNumber = this.setup.$carousel.data('currentSlide') - 1; const currentIndex = this.setup.$carousel.data('currentIndex'); const nextIndex = currentIndex - 1; + const $slideContent = $modal.find('.carousel-item:eq(' + nextIndex + ')'); + + // Flush content when sliding + $slideContent.empty().append(this.setup.slides[nextIndex].content); + $modalTitle.text(this.setup.slides[nextIndex].title); + + // Always unlock previous step if there is any + if (nextIndex > 0) { + this.unlockPrevStep(); + } else { + this.lockPrevStep(); + } this.setup.$carousel.data('currentSlide', nextSlideNumber); this.setup.$carousel.data('currentIndex', nextIndex); - $modalTitle.text(this.setup.slides[nextIndex].title); - $modalFooter.find('.progress-bar.last-step') .width(this.setup.$carousel.data('initialStep') + '%') .text(this.getProgressBarTitle(this.setup.$carousel.data('slideCount') - 1)); diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf index 75b9c93db071..733028994f31 100644 --- a/typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_layout.xlf @@ -366,6 +366,18 @@ <trans-unit id="localize.wizard.button.process" resname="localize.wizard.button.process"> <source>Start processing</source> </trans-unit> + <trans-unit id="localize.wizard.step.selectMode" resname="localize.wizard.step.selectMode"> + <source>Localization mode</source> + </trans-unit> + <trans-unit id="localize.wizard.step.chooseLanguage" resname="localize.wizard.step.chooseLanguage"> + <source>Source Language</source> + </trans-unit> + <trans-unit id="localize.wizard.step.chooseLanguage" resname="localize.wizard.step.chooseLanguage"> + <source>Source Language</source> + </trans-unit> + <trans-unit id="localize.wizard.step.selectRecords" resname="localize.wizard.step.selectRecords"> + <source>Records</source> + </trans-unit> <trans-unit id="localize.view.chooseLanguage" resname="localize.view.chooseLanguage"> <source>Choose the language from which you want to localize the content</source> </trans-unit> @@ -390,10 +402,12 @@ <strong>Use this when you want to have freedom in designing your translated website.</strong> </source> </trans-unit> - <trans-unit id="localize.educate.noTranslate" resname="localize.educate.noTranslate" xml:space="preserve"> - <source>Using the translation wizard is not possible, because either translation is disabled or the translated page is in "mixed mode". Being in "mixed mode" means that both translated records that have a source language and "free mode" records without a parent, are present.<br> - We highlighted the problematic records for you in this case. - </source> + <trans-unit id="window.localization.mixed_mode.title" resname="window.localization.mixed_mode.title"> + <source>Localization not possible</source> + </trans-unit> + <trans-unit id="window.localization.mixed_mode.message" resname="window.localization.mixed_mode.message"> + <source>Using the translation wizard is not possible, because either translation is disabled or the translated page is in "mixed mode". Being in "mixed mode" means that both translated records that have a source language and "free mode" records without a parent, are present. + We highlighted the problematic records for you in this case.</source> </trans-unit> <trans-unit id="error.invalidBackendLayout" resname="error.invalidBackendLayout"> <source>The selected page layout is mis-configured, no columns are specified to be editable. Please update your backend layout to have at least one parameter "colPos" set.</source> diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/localization.js b/typo3/sysext/backend/Resources/Public/JavaScript/localization.js index 5a2e9fb79a56..96be7d8b0171 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/localization.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/localization.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Icons from"@typo3/backend/icons.js";import Wizard from"@typo3/backend/wizard.js";import"@typo3/backend/element/icon-element.js";class Localization{constructor(){this.triggerButton=".t3js-localize",this.localizationMode=null,this.sourceLanguage=null,this.records=[],DocumentService.ready().then((()=>{this.initialize()}))}initialize(){Icons.getIcon("actions-localize",Icons.sizes.large).then((e=>{Icons.getIcon("actions-edit-copy",Icons.sizes.large).then((a=>{$(this.triggerButton).removeClass("disabled"),$(document).on("click",this.triggerButton,(t=>{t.preventDefault();const o=$(t.currentTarget),i=[],l=[];let s="";o.data("allowTranslate")&&(i.push('<div class="row"><div class="col-sm-3"><label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-translate">'+e+'<input type="radio" name="mode" id="mode_translate" value="localize" style="display: none"><br>'+TYPO3.lang["localize.wizard.button.translate"]+'</label></div><div class="col-sm-9"><p class="t3js-helptext t3js-helptext-translate text-body-secondary">'+TYPO3.lang["localize.educate.translate"]+"</p></div></div>"),l.push("localize")),o.data("allowCopy")&&(i.push('<div class="row"><div class="col-sm-3"><label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-copy">'+a+'<input type="radio" name="mode" id="mode_copy" value="copyFromLanguage" style="display: none"><br>'+TYPO3.lang["localize.wizard.button.copy"]+'</label></div><div class="col-sm-9"><p class="t3js-helptext t3js-helptext-copy text-body-secondary">'+TYPO3.lang["localize.educate.copy"]+"</p></div></div>"),l.push("copyFromLanguage")),0===o.data("allowTranslate")&&0===o.data("allowCopy")&&i.push('<div class="row"><div class="col-sm-12"><div class="alert alert-warning"><div class="media"><div class="media-left"><span class="icon-emphasized"><typo3-backend-icon identifier="actions-exclamation" size="small"></typo3-backend-icon></span></div><div class="media-body"><p class="alert-message">'+TYPO3.lang["localize.educate.noTranslate"]+"</p></div></div></div></div></div>"),s+='<div data-bs-toggle="buttons">'+i.join("")+"</div>",Wizard.addSlide("localize-choose-action",TYPO3.lang["localize.wizard.header_page"].replace("{0}",o.data("page")).replace("{1}",o.data("languageName")),s,SeverityEnum.notice,(()=>{1===l.length&&(this.localizationMode=l[0],Wizard.unlockNextStep().get(0).click())})),Wizard.addSlide("localize-choose-language",TYPO3.lang["localize.view.chooseLanguage"],"",SeverityEnum.notice,(e=>{Icons.getIcon("spinner-circle",Icons.sizes.large).then((a=>{e.html('<div class="text-center">'+a+"</div>"),this.loadAvailableLanguages(parseInt(o.data("pageId"),10),parseInt(o.data("languageId"),10)).then((async a=>{const t=await a.resolve();if(1===t.length)return this.sourceLanguage=t[0].uid,void Wizard.unlockNextStep().get(0).click();Wizard.getComponent().on("click",".t3js-language-option",(e=>{const a=$(e.currentTarget).prev();this.sourceLanguage=a.val(),Wizard.unlockNextStep()}));const o=$("<div />",{class:"row"});for(const e of t){const a="language"+e.uid,t=$("<input />",{type:"radio",name:"language",id:a,value:e.uid,style:"display: none;",class:"btn-check"}),i=$("<label />",{class:"btn btn-default d-block t3js-language-option option",for:a}).text(" "+e.title).prepend(e.flagIcon);o.append($("<div />",{class:"col-sm-4"}).append(t).append(i))}e.empty().append(o)}))}))})),Wizard.addSlide("localize-summary",TYPO3.lang["localize.view.summary"],"",SeverityEnum.notice,(e=>{Icons.getIcon("spinner-circle",Icons.sizes.large).then((a=>{e.html('<div class="text-center">'+a+"</div>")})),this.getSummary(parseInt(o.data("pageId"),10),parseInt(o.data("languageId"),10)).then((async a=>{const t=await a.resolve();e.empty(),this.records=[];const o=t.columns.columns;t.columns.columnList.forEach((a=>{if(void 0===t.records[a])return;const i=o[a],l=$("<div />",{class:"row"});t.records[a].forEach((e=>{const a=" ("+e.uid+") "+e.title;this.records.push(e.uid),l.append($("<div />",{class:"col-sm-6"}).append($("<div />",{class:"input-group"}).append($("<span />",{class:"input-group-text"}).append($("<input />",{type:"checkbox",class:"t3js-localization-toggle-record",id:"record-uid-"+e.uid,checked:"checked","data-uid":e.uid,"aria-label":a})),$("<label />",{class:"form-control",for:"record-uid-"+e.uid}).text(a).prepend(e.icon))))})),e.append($("<fieldset />",{class:"localization-fieldset"}).append($("<label />").text(i).prepend($("<input />",{class:"t3js-localization-toggle-column",type:"checkbox",checked:"checked"})),l))})),Wizard.unlockNextStep(),Wizard.getComponent().on("change",".t3js-localization-toggle-record",(e=>{const a=$(e.currentTarget),t=a.data("uid"),o=a.closest("fieldset"),i=o.find(".t3js-localization-toggle-column");if(a.is(":checked"))this.records.push(t);else{const e=this.records.indexOf(t);e>-1&&this.records.splice(e,1)}const l=o.find(".t3js-localization-toggle-record"),s=o.find(".t3js-localization-toggle-record:checked");i.prop("checked",s.length>0),i.prop("__indeterminate",s.length>0&&s.length<l.length),this.records.length>0?Wizard.unlockNextStep():Wizard.lockNextStep()})).on("change",".t3js-localization-toggle-column",(e=>{const a=$(e.currentTarget),t=a.closest("fieldset").find(".t3js-localization-toggle-record");t.prop("checked",a.is(":checked")),t.trigger("change")}))}))})),Wizard.addFinalProcessingSlide((()=>{this.localizeRecords(parseInt(o.data("pageId"),10),parseInt(o.data("languageId"),10),this.records).then((()=>{Wizard.dismiss(),document.location.reload()}))})).then((()=>{Wizard.show(),Wizard.getComponent().on("click",".t3js-localization-option",(e=>{const a=$(e.currentTarget),t=a.find('input[type="radio"]');if(a.data("helptext")){const t=$(e.delegateTarget);t.find(".t3js-localization-option").removeClass("active"),t.find(".t3js-helptext").addClass("text-body-secondary"),a.addClass("active"),t.find(a.data("helptext")).removeClass("text-body-secondary")}this.localizationMode=t.val(),Wizard.unlockNextStep()}))}))}))}))}))}loadAvailableLanguages(e,a){return new AjaxRequest(TYPO3.settings.ajaxUrls.page_languages).withQueryArguments({pageId:e,languageId:a}).get()}getSummary(e,a){return new AjaxRequest(TYPO3.settings.ajaxUrls.records_localize_summary).withQueryArguments({pageId:e,destLanguageId:a,languageId:this.sourceLanguage}).get()}localizeRecords(e,a,t){return new AjaxRequest(TYPO3.settings.ajaxUrls.records_localize).withQueryArguments({pageId:e,srcLanguageId:this.sourceLanguage,destLanguageId:a,action:this.localizationMode,uidList:t}).get()}}export default new Localization; \ No newline at end of file +import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import{SeverityEnum}from"@typo3/backend/enum/severity.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import Icons from"@typo3/backend/icons.js";import Modal from"@typo3/backend/modal.js";import MultiStepWizard from"@typo3/backend/multi-step-wizard.js";import"@typo3/backend/element/icon-element.js";class Localization{constructor(){this.triggerButton=".t3js-localize",DocumentService.ready().then((()=>{this.initialize()}))}async initialize(){const e=await Icons.getIcon("actions-localize",Icons.sizes.large),t=await Icons.getIcon("actions-edit-copy",Icons.sizes.large);$(this.triggerButton).removeClass("disabled"),$(document).on("click",this.triggerButton,(async a=>{a.preventDefault();const o=$(a.currentTarget),l=[],i=[];let n="";if(0===o.data("allowTranslate")&&0===o.data("allowCopy"))return void Modal.confirm(TYPO3.lang["window.localization.mixed_mode.title"],TYPO3.lang["window.localization.mixed_mode.message"],SeverityEnum.warning,[{text:TYPO3?.lang?.["button.ok"]||"OK",btnClass:"btn-warning",name:"ok",trigger:(e,t)=>t.hideModal()}]);const s=await(await this.loadAvailableLanguages(parseInt(o.data("pageId"),10),parseInt(o.data("languageId"),10))).resolve();o.data("allowTranslate")&&(l.push('<div class="row"><div class="col-sm-3"><label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-translate">'+e+'<input type="radio" name="mode" id="mode_translate" value="localize" style="display: none"><br>'+TYPO3.lang["localize.wizard.button.translate"]+'</label></div><div class="col-sm-9"><p class="t3js-helptext t3js-helptext-translate text-body-secondary">'+TYPO3.lang["localize.educate.translate"]+"</p></div></div>"),i.push("localize")),o.data("allowCopy")&&(l.push('<div class="row"><div class="col-sm-3"><label class="btn btn-default d-block t3js-localization-option" data-helptext=".t3js-helptext-copy">'+t+'<input type="radio" name="mode" id="mode_copy" value="copyFromLanguage" style="display: none"><br>'+TYPO3.lang["localize.wizard.button.copy"]+'</label></div><div class="col-sm-9"><p class="t3js-helptext t3js-helptext-copy text-body-secondary">'+TYPO3.lang["localize.educate.copy"]+"</p></div></div>"),i.push("copyFromLanguage")),1===i.length?MultiStepWizard.set("localizationMode",i[0]):(n+='<div data-bs-toggle="buttons">'+l.join("")+"</div>",MultiStepWizard.addSlide("localize-choose-action",TYPO3.lang["localize.wizard.header_page"].replace("{0}",o.data("page")).replace("{1}",o.data("languageName")),n,SeverityEnum.notice,TYPO3.lang["localize.wizard.step.selectMode"],((e,t)=>{void 0!==t.localizationMode&&MultiStepWizard.unlockNextStep()}))),1===s.length?MultiStepWizard.set("sourceLanguage",s[0].uid):MultiStepWizard.addSlide("localize-choose-language",TYPO3.lang["localize.view.chooseLanguage"],"",SeverityEnum.notice,TYPO3.lang["localize.wizard.step.chooseLanguage"],(async(e,t)=>{void 0!==t.sourceLanguage&&MultiStepWizard.unlockNextStep(),e.html('<div class="text-center">'+await Icons.getIcon("spinner-circle",Icons.sizes.large)+"</div>"),MultiStepWizard.getComponent().on("click",".t3js-language-option",(e=>{const t=$(e.currentTarget).prev();MultiStepWizard.set("sourceLanguage",t.val()),MultiStepWizard.unlockNextStep()}));const a=$("<div />",{class:"row"});for(const e of s){const t="language"+e.uid,o=$("<input />",{type:"radio",name:"language",id:t,value:e.uid,style:"display: none;",class:"btn-check"}),l=$("<label />",{class:"btn btn-default d-block t3js-language-option option",for:t}).text(" "+e.title).prepend(e.flagIcon);a.append($("<div />",{class:"col-sm-4"}).append(o).append(l))}e.empty().append(a)})),MultiStepWizard.addSlide("localize-summary",TYPO3.lang["localize.view.summary"],"",SeverityEnum.notice,TYPO3.lang["localize.wizard.step.selectRecords"],(async(e,t)=>{e.empty().html('<div class="text-center">'+await Icons.getIcon("spinner-circle",Icons.sizes.large)+"</div>");const a=await(await this.getSummary(parseInt(o.data("pageId"),10),parseInt(o.data("languageId"),10),t.sourceLanguage)).resolve();e.empty(),MultiStepWizard.set("records",[]);const l=a.columns.columns;a.columns.columnList.forEach((o=>{if(void 0===a.records[o])return;const i=l[o],n=$("<div />",{class:"row"});a.records[o].forEach((e=>{const a=" ("+e.uid+") "+e.title;t.records.push(e.uid),n.append($("<div />",{class:"col-sm-6"}).append($("<div />",{class:"input-group"}).append($("<span />",{class:"input-group-text"}).append($("<input />",{type:"checkbox",class:"t3js-localization-toggle-record",id:"record-uid-"+e.uid,checked:"checked","data-uid":e.uid,"aria-label":a})),$("<label />",{class:"form-control",for:"record-uid-"+e.uid}).text(a).prepend(e.icon))))})),e.append($("<fieldset />",{class:"localization-fieldset"}).append($("<label />").text(i).prepend($("<input />",{class:"t3js-localization-toggle-column",type:"checkbox",checked:"checked"})),n))})),MultiStepWizard.unlockNextStep(),MultiStepWizard.getComponent().on("change",".t3js-localization-toggle-record",(e=>{const a=$(e.currentTarget),o=a.data("uid"),l=a.closest("fieldset"),i=l.find(".t3js-localization-toggle-column");if(a.is(":checked"))t.records.push(o);else{const e=t.records.indexOf(o);e>-1&&t.records.splice(e,1)}const n=l.find(".t3js-localization-toggle-record"),s=l.find(".t3js-localization-toggle-record:checked");i.prop("checked",s.length>0),i.prop("__indeterminate",s.length>0&&s.length<n.length),t.records.length>0?MultiStepWizard.unlockNextStep():MultiStepWizard.lockNextStep()})).on("change",".t3js-localization-toggle-column",(e=>{const t=$(e.currentTarget),a=t.closest("fieldset").find(".t3js-localization-toggle-record");a.prop("checked",t.is(":checked")),a.trigger("change")}))})),MultiStepWizard.addFinalProcessingSlide((async(e,t)=>{await this.localizeRecords(parseInt(o.data("pageId"),10),parseInt(o.data("languageId"),10),t.sourceLanguage,t.localizationMode,t.records),MultiStepWizard.dismiss(),document.location.reload()})).then((()=>{MultiStepWizard.show(),MultiStepWizard.getComponent().on("click",".t3js-localization-option",(e=>{const t=$(e.currentTarget),a=t.find('input[type="radio"]');if(t.data("helptext")){const a=$(e.delegateTarget);a.find(".t3js-localization-option").removeClass("active"),a.find(".t3js-helptext").addClass("text-body-secondary"),t.addClass("active"),a.find(t.data("helptext")).removeClass("text-body-secondary")}MultiStepWizard.set("localizationMode",a.val()),MultiStepWizard.unlockNextStep()}))}))}))}loadAvailableLanguages(e,t){return new AjaxRequest(TYPO3.settings.ajaxUrls.page_languages).withQueryArguments({pageId:e,languageId:t}).get()}getSummary(e,t,a){return new AjaxRequest(TYPO3.settings.ajaxUrls.records_localize_summary).withQueryArguments({pageId:e,destLanguageId:t,languageId:a}).get()}localizeRecords(e,t,a,o,l){return new AjaxRequest(TYPO3.settings.ajaxUrls.records_localize).withQueryArguments({pageId:e,srcLanguageId:a,destLanguageId:t,action:o,uidList:l}).get()}}export default new Localization; \ No newline at end of file diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/multi-step-wizard.js b/typo3/sysext/backend/Resources/Public/JavaScript/multi-step-wizard.js index af3e70a780db..562fb5cf4e24 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/multi-step-wizard.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/multi-step-wizard.js @@ -10,4 +10,4 @@ * * The TYPO3 project - inspiring people to share! */ -import{SeverityEnum}from"@typo3/backend/enum/severity.js";import $ from"jquery";import{Carousel}from"bootstrap";import Modal from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import Icons from"@typo3/backend/icons.js";class MultiStepWizard{constructor(){this.setup={slides:[],settings:{},forceSelection:!0,$carousel:null,carousel:null},this.originalSetup=$.extend(!0,{},this.setup)}set(t,e){return this.setup.settings[t]=e,this}addSlide(t,e,s="",i=SeverityEnum.info,r,a){const l={identifier:t,title:e,content:s,severity:i,progressBarTitle:r,callback:a};return this.setup.slides.push(l),this}addFinalProcessingSlide(t){return t||(t=()=>{this.dismiss()}),Icons.getIcon("spinner-circle",Icons.sizes.default,null,null).then((e=>{const s=$("<div />",{class:"text-center"}).append(e);this.addSlide("final-processing-slide",top.TYPO3.lang["wizard.processing.title"],s[0].outerHTML,Severity.info,null,t)}))}show(){const t=this.generateSlides(),e=this.setup.slides[0];Modal.advanced({title:e.title,content:t,severity:e.severity,staticBackdrop:!0,buttons:[{text:top.TYPO3.lang["wizard.button.cancel"],active:!0,btnClass:"btn-default float-start",name:"cancel",trigger:()=>{this.getComponent().trigger("wizard-dismiss")}},{text:top.TYPO3.lang["wizard.button.prev"],btnClass:"btn-"+Severity.getCssClass(e.severity),name:"prev"},{text:top.TYPO3.lang["wizard.button.next"],btnClass:"btn-"+Severity.getCssClass(e.severity),name:"next"}],additionalCssClasses:["modal-multi-step-wizard"],callback:t=>{this.setup.carousel=new Carousel(t.querySelector(".carousel")),this.addButtonContainer(),this.addProgressBar(),this.initializeEvents()}}),this.getComponent().on("wizard-visible",(()=>{this.runSlideCallback(e,this.setup.$carousel.find(".carousel-item").first())})).on("wizard-dismissed",(()=>{this.setup=$.extend(!0,{},this.originalSetup)}))}getComponent(){return null===this.setup.$carousel&&this.generateSlides(),this.setup.$carousel}dismiss(){Modal.dismiss()}lockNextStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="next"]');return t.prop("disabled",!0),t}unlockNextStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="next"]');return t.prop("disabled",!1),t}lockPrevStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="prev"]');return t.prop("disabled",!0),t}unlockPrevStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="prev"]');return t.prop("disabled",!1),t}triggerStepButton(t){const e=this.setup.$carousel.closest(".modal").find('button[name="'+t+'"]');return e.length>0&&!0!==e.prop("disabled")&&e.get(0).click(),e}blurCancelStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="cancel"]');return t.trigger("blur"),t}initializeEvents(){const t=this.setup.$carousel.closest(".modal");this.initializeSlideNextEvent(t),this.initializeSlidePrevEvent(t),this.setup.$carousel.get(0).addEventListener("slide.bs.carousel",(e=>{"left"===e.direction?this.nextSlideChanges(t):this.prevSlideChanges(t)})),this.setup.$carousel.get(0).addEventListener("slid.bs.carousel",(t=>{const e=this.setup.$carousel.data("currentIndex"),s=this.setup.slides[e];this.runSlideCallback(s,$(t.relatedTarget)),this.setup.forceSelection&&this.lockNextStep()}));const e=this.getComponent();e.on("wizard-dismiss",this.dismiss),Modal.currentModal.addEventListener("typo3-modal-hidden",(()=>{e.trigger("wizard-dismissed")})),Modal.currentModal.addEventListener("typo3-modal-shown",(()=>{e.trigger("wizard-visible")}))}initializeSlideNextEvent(t){t.find(".modal-footer").find('button[name="next"]').off().on("click",(()=>{this.setup.carousel.next()}))}initializeSlidePrevEvent(t){t.find(".modal-footer").find('button[name="prev"]').off().on("click",(()=>{this.setup.carousel.prev()}))}nextSlideChanges(t){this.initializeSlideNextEvent(t);const e=t.find(".modal-title"),s=t.find(".modal-footer"),i=this.setup.$carousel.data("currentSlide")+1,r=this.setup.$carousel.data("currentIndex"),a=r+1;e.text(this.setup.slides[a].title),this.setup.$carousel.data("currentSlide",i),this.setup.$carousel.data("currentIndex",a);const l=s.find(".progress-bar");l.eq(r).width("0%"),l.eq(a).width(this.setup.$carousel.data("initialStep")*i+"%").removeClass("inactive"),this.updateCurrentSeverity(t,r,a)}prevSlideChanges(t){this.initializeSlidePrevEvent(t);const e=t.find(".modal-title"),s=t.find(".modal-footer"),i=s.find('button[name="next"]'),r=this.setup.$carousel.data("currentSlide")-1,a=this.setup.$carousel.data("currentIndex"),l=a-1;this.setup.$carousel.data("currentSlide",r),this.setup.$carousel.data("currentIndex",l),e.text(this.setup.slides[l].title),s.find(".progress-bar.last-step").width(this.setup.$carousel.data("initialStep")+"%").text(this.getProgressBarTitle(this.setup.$carousel.data("slideCount")-1)),i.text(top.TYPO3.lang["wizard.button.next"]);const n=s.find(".progress-bar");n.eq(a).width(this.setup.$carousel.data("initialStep")+"%").addClass("inactive"),n.eq(l).width(this.setup.$carousel.data("initialStep")*r+"%").removeClass("inactive"),this.updateCurrentSeverity(t,a,l)}updateCurrentSeverity(t,e,s){t.find(".modal-footer").find('button[name="next"]').removeClass("btn-"+Severity.getCssClass(this.setup.slides[e].severity)).addClass("btn-"+Severity.getCssClass(this.setup.slides[s].severity)),t.removeClass("modal-severity-"+Severity.getCssClass(this.setup.slides[e].severity)).addClass("modal-severity-"+Severity.getCssClass(this.setup.slides[s].severity))}getProgressBarTitle(t){let e;return e=null===this.setup.slides[t].progressBarTitle?0===t?top.TYPO3.lang["wizard.progressStep.start"]:t>=this.setup.$carousel.data("slideCount")-1?top.TYPO3.lang["wizard.progressStep.finish"]:top.TYPO3.lang["wizard.progressStep"]+String(t+1):this.setup.slides[t].progressBarTitle,e}runSlideCallback(t,e){"function"==typeof t.callback&&t.callback(e,this.setup.settings,t.identifier)}addProgressBar(){const t=this.setup.$carousel.find(".carousel-item").length,e=Math.max(1,t),s=Math.round(100/e),i=this.setup.$carousel.closest(".modal").find(".modal-footer");if(this.setup.$carousel.data("initialStep",s).data("slideCount",e).data("realSlideCount",t).data("currentIndex",0).data("currentSlide",1),e>1){i.prepend($("<div />",{class:"progress"}));for(let t=0;t<this.setup.slides.length;++t){let e;e=0===t?"progress-bar first-step":t===this.setup.$carousel.data("slideCount")-1?"progress-bar last-step inactive":"progress-bar step inactive",i.find(".progress").append($("<div />",{role:"progressbar",class:e,"aria-valuemin":0,"aria-valuenow":s,"aria-valuemax":100}).width(s+"%").text(this.getProgressBarTitle(t)))}}}addButtonContainer(){this.setup.$carousel.closest(".modal").find(".modal-footer .btn").wrapAll('<div class="modal-btn-group" />')}generateSlides(){if(null!==this.setup.$carousel)return this.setup.$carousel;let t='<div class="carousel slide" data-bs-ride="false"><div class="carousel-inner" role="listbox">';for(let e=0;e<this.setup.slides.length;++e){const s=this.setup.slides[e];let i=s.content;"object"==typeof i&&(i=i.html()),t+='<div class="carousel-item" data-bs-slide="'+s.identifier+'" data-step="'+e+'">'+i+"</div>"}return t+="</div></div>",this.setup.$carousel=$(t),this.setup.$carousel.find(".carousel-item").first().addClass("active"),this.setup.$carousel}}let multistepWizardObject;try{window.opener&&window.opener.TYPO3&&window.opener.TYPO3.MultiStepWizard&&(multistepWizardObject=window.opener.TYPO3.MultiStepWizard),parent&&parent.window.TYPO3&&parent.window.TYPO3.MultiStepWizard&&(multistepWizardObject=parent.window.TYPO3.MultiStepWizard),top&&top.TYPO3&&top.TYPO3.MultiStepWizard&&(multistepWizardObject=top.TYPO3.MultiStepWizard)}catch(t){}multistepWizardObject||(multistepWizardObject=new MultiStepWizard,"undefined"!=typeof TYPO3&&(TYPO3.MultiStepWizard=multistepWizardObject));export default multistepWizardObject; \ No newline at end of file +import{SeverityEnum}from"@typo3/backend/enum/severity.js";import $ from"jquery";import{Carousel}from"bootstrap";import Modal from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import Icons from"@typo3/backend/icons.js";class MultiStepWizard{constructor(){this.setup={slides:[],settings:{},forceSelection:!0,$carousel:null,carousel:null},this.originalSetup=$.extend(!0,{},this.setup)}set(t,e){return this.setup.settings[t]=e,this}addSlide(t,e,s="",i=SeverityEnum.info,r,a){const l={identifier:t,title:e,content:s,severity:i,progressBarTitle:r,callback:a};return this.setup.slides.push(l),this}async addFinalProcessingSlide(t){t||(t=()=>{this.dismiss()});const e=await Icons.getIcon("spinner-circle",Icons.sizes.large,null,null),s=$("<div />",{class:"text-center"}).append(e);this.addSlide("final-processing-slide",top.TYPO3.lang["wizard.processing.title"],s[0].outerHTML,Severity.info,null,t)}show(){const t=this.generateSlides(),e=this.setup.slides[0];Modal.advanced({title:e.title,content:t,severity:e.severity,staticBackdrop:!0,buttons:[{text:top.TYPO3.lang["wizard.button.cancel"],active:!0,btnClass:"btn-default float-start",name:"cancel",trigger:()=>{this.getComponent().trigger("wizard-dismiss")}},{text:top.TYPO3.lang["wizard.button.prev"],btnClass:"btn-"+Severity.getCssClass(e.severity),name:"prev"},{text:top.TYPO3.lang["wizard.button.next"],btnClass:"btn-"+Severity.getCssClass(e.severity),name:"next"}],additionalCssClasses:["modal-multi-step-wizard"],callback:t=>{this.setup.carousel=new Carousel(t.querySelector(".carousel")),this.addButtonContainer(),this.addProgressBar(),this.initializeEvents()}}),this.getComponent().on("wizard-visible",(()=>{this.setup.forceSelection&&(this.lockPrevStep(),this.lockNextStep()),this.runSlideCallback(e,this.setup.$carousel.find(".carousel-item").first())})).on("wizard-dismissed",(()=>{this.setup=$.extend(!0,{},this.originalSetup)}))}getComponent(){return null===this.setup.$carousel&&this.generateSlides(),this.setup.$carousel}dismiss(){Modal.dismiss()}lockNextStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="next"]');return t.prop("disabled",!0),t}unlockNextStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="next"]');return t.prop("disabled",!1),t}lockPrevStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="prev"]');return t.prop("disabled",!0),t}unlockPrevStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="prev"]');return t.prop("disabled",!1),t}triggerStepButton(t){const e=this.setup.$carousel.closest(".modal").find('button[name="'+t+'"]');return e.length>0&&!0!==e.prop("disabled")&&e.get(0).click(),e}blurCancelStep(){const t=this.setup.$carousel.closest(".modal").find('button[name="cancel"]');return t.trigger("blur"),t}initializeEvents(){const t=this.setup.$carousel.closest(".modal");this.initializeSlideNextEvent(t),this.initializeSlidePrevEvent(t),this.setup.$carousel.get(0).addEventListener("slide.bs.carousel",(e=>{"left"===e.direction?this.nextSlideChanges(t):this.prevSlideChanges(t)})),this.setup.$carousel.get(0).addEventListener("slid.bs.carousel",(t=>{const e=this.setup.$carousel.data("currentIndex"),s=this.setup.slides[e];this.setup.forceSelection&&this.lockNextStep(),this.runSlideCallback(s,$(t.relatedTarget))}));const e=this.getComponent();e.on("wizard-dismiss",this.dismiss),Modal.currentModal.addEventListener("typo3-modal-hidden",(()=>{e.trigger("wizard-dismissed")})),Modal.currentModal.addEventListener("typo3-modal-shown",(()=>{e.trigger("wizard-visible")}))}initializeSlideNextEvent(t){t.find(".modal-footer").find('button[name="next"]').off().on("click",(()=>{this.setup.carousel.next()}))}initializeSlidePrevEvent(t){t.find(".modal-footer").find('button[name="prev"]').off().on("click",(()=>{this.setup.carousel.prev()}))}nextSlideChanges(t){this.initializeSlideNextEvent(t);const e=t.find(".modal-title"),s=t.find(".modal-footer"),i=this.setup.$carousel.data("currentSlide")+1,r=this.setup.$carousel.data("currentIndex"),a=r+1;t.find(".carousel-item:eq("+a+")").empty().append(this.setup.slides[a].content),e.text(this.setup.slides[a].title),this.unlockPrevStep(),this.setup.$carousel.data("currentSlide",i),this.setup.$carousel.data("currentIndex",a);const l=s.find(".progress-bar");l.eq(r).width("0%"),l.eq(a).width(this.setup.$carousel.data("initialStep")*i+"%").removeClass("inactive"),this.updateCurrentSeverity(t,r,a)}prevSlideChanges(t){this.initializeSlidePrevEvent(t);const e=t.find(".modal-title"),s=t.find(".modal-footer"),i=s.find('button[name="next"]'),r=this.setup.$carousel.data("currentSlide")-1,a=this.setup.$carousel.data("currentIndex"),l=a-1;t.find(".carousel-item:eq("+l+")").empty().append(this.setup.slides[l].content),e.text(this.setup.slides[l].title),l>0?this.unlockPrevStep():this.lockPrevStep(),this.setup.$carousel.data("currentSlide",r),this.setup.$carousel.data("currentIndex",l),s.find(".progress-bar.last-step").width(this.setup.$carousel.data("initialStep")+"%").text(this.getProgressBarTitle(this.setup.$carousel.data("slideCount")-1)),i.text(top.TYPO3.lang["wizard.button.next"]);const n=s.find(".progress-bar");n.eq(a).width(this.setup.$carousel.data("initialStep")+"%").addClass("inactive"),n.eq(l).width(this.setup.$carousel.data("initialStep")*r+"%").removeClass("inactive"),this.updateCurrentSeverity(t,a,l)}updateCurrentSeverity(t,e,s){t.find(".modal-footer").find('button[name="next"]').removeClass("btn-"+Severity.getCssClass(this.setup.slides[e].severity)).addClass("btn-"+Severity.getCssClass(this.setup.slides[s].severity)),t.removeClass("modal-severity-"+Severity.getCssClass(this.setup.slides[e].severity)).addClass("modal-severity-"+Severity.getCssClass(this.setup.slides[s].severity))}getProgressBarTitle(t){let e;return e=null===this.setup.slides[t].progressBarTitle?0===t?top.TYPO3.lang["wizard.progressStep.start"]:t>=this.setup.$carousel.data("slideCount")-1?top.TYPO3.lang["wizard.progressStep.finish"]:top.TYPO3.lang["wizard.progressStep"]+String(t+1):this.setup.slides[t].progressBarTitle,e}runSlideCallback(t,e){"function"==typeof t.callback&&t.callback(e,this.setup.settings,t.identifier)}addProgressBar(){const t=this.setup.$carousel.find(".carousel-item").length,e=Math.max(1,t),s=Math.round(100/e),i=this.setup.$carousel.closest(".modal").find(".modal-footer");if(this.setup.$carousel.data("initialStep",s).data("slideCount",e).data("realSlideCount",t).data("currentIndex",0).data("currentSlide",1),e>1){i.prepend($("<div />",{class:"progress"}));for(let t=0;t<this.setup.slides.length;++t){let e;e=0===t?"progress-bar first-step":t===this.setup.$carousel.data("slideCount")-1?"progress-bar last-step inactive":"progress-bar step inactive",i.find(".progress").append($("<div />",{role:"progressbar",class:e,"aria-valuemin":0,"aria-valuenow":s,"aria-valuemax":100}).width(s+"%").text(this.getProgressBarTitle(t)))}}}addButtonContainer(){this.setup.$carousel.closest(".modal").find(".modal-footer .btn").wrapAll('<div class="modal-btn-group" />')}generateSlides(){if(null!==this.setup.$carousel)return this.setup.$carousel;let t='<div class="carousel slide" data-bs-ride="false"><div class="carousel-inner" role="listbox">';for(let e=0;e<this.setup.slides.length;++e){const s=this.setup.slides[e];let i=s.content;"object"==typeof i&&(i=i.html()),t+='<div class="carousel-item" data-bs-slide="'+s.identifier+'" data-step="'+e+'">'+i+"</div>"}return t+="</div></div>",this.setup.$carousel=$(t),this.setup.$carousel.find(".carousel-item").first().addClass("active"),this.setup.$carousel}}let multistepWizardObject;try{window.opener&&window.opener.TYPO3&&window.opener.TYPO3.MultiStepWizard&&(multistepWizardObject=window.opener.TYPO3.MultiStepWizard),parent&&parent.window.TYPO3&&parent.window.TYPO3.MultiStepWizard&&(multistepWizardObject=parent.window.TYPO3.MultiStepWizard),top&&top.TYPO3&&top.TYPO3.MultiStepWizard&&(multistepWizardObject=top.TYPO3.MultiStepWizard)}catch(t){}multistepWizardObject||(multistepWizardObject=new MultiStepWizard,"undefined"!=typeof TYPO3&&(TYPO3.MultiStepWizard=multistepWizardObject));export default multistepWizardObject; \ No newline at end of file -- GitLab