[FEATURE] Add wizard component 51/46651/11
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Sun, 14 Feb 2016 16:20:45 +0000 (17:20 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Fri, 19 Feb 2016 18:08:11 +0000 (19:08 +0100)
This patch adds a wizard component. The component allows
to add "slides" combined with a callback.

The localization wizard used in the Page module is converted
to the module wizard.

Change-Id: Id60830370a85ced2425c14a593352a9f165ff4c2
Resolves: #73429
Releases: master
Reviewed-on: https://review.typo3.org/46651
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
typo3/sysext/backend/Resources/Public/JavaScript/Localization.js
typo3/sysext/backend/Resources/Public/JavaScript/Modal.js
typo3/sysext/backend/Resources/Public/JavaScript/Wizard.js [new file with mode: 0644]
typo3/sysext/core/Classes/Page/PageRenderer.php
typo3/sysext/core/Documentation/Changelog/master/Feature-73429-WizardComponentHasBeenAdded.rst [new file with mode: 0644]
typo3/sysext/core/Resources/Private/Language/wizard.xlf [new file with mode: 0644]

index c26f566..c70cd7d 100644 (file)
 define([
        'jquery',
        'TYPO3/CMS/Backend/AjaxDataHandler',
-       'TYPO3/CMS/Backend/Modal',
+       'TYPO3/CMS/Backend/Wizard',
        'TYPO3/CMS/Backend/Icons',
        'TYPO3/CMS/Backend/Severity',
        'bootstrap'
-], function($, DataHandler, Modal, Icons, Severity) {
+], function($, DataHandler, Wizard, Icons, Severity) {
        'use strict';
 
        /**
@@ -74,20 +74,17 @@ define([
 
                $(document).on('click', Localization.identifier.triggerButton, function() {
                        var $triggerButton = $(this),
-                               modalContent =
-                                       '<div id="localization-carousel" class="carousel slide" data-ride="carousel" data-interval="false">'
-                                               + '<div class="carousel-inner" role="listbox">'
-                                                       + '<div class="item active">'
-                                                               + '<div data-toggle="buttons">'
-                                                                       + '<div class="row">'
-                                                                               + '<div class="btn-group col-sm-3">' + Localization.actions.translate[0].outerHTML + '</div>'
-                                                                               + '<div class="col-sm-9">'
-                                                                                       + '<p class="t3js-helptext t3js-helptext-translate text-muted">' + TYPO3.lang['localize.educate.translate'] + '</p>'
-                                                                               + '</div>'
-                                                                       + '</div>';
+                               slideStep1 =
+                                       '<div data-toggle="buttons">'
+                                               + '<div class="row">'
+                                                       + '<div class="btn-group col-sm-3">' + Localization.actions.translate[0].outerHTML + '</div>'
+                                                       + '<div class="col-sm-9">'
+                                                               + '<p class="t3js-helptext t3js-helptext-translate text-muted">' + TYPO3.lang['localize.educate.translate'] + '</p>'
+                                                       + '</div>'
+                                               + '</div>';
 
                        if ($triggerButton.data('hasElements') === 0) {
-                               modalContent +=
+                               slideStep1 +=
                                        '<hr>'
                                        + '<div class="row">'
                                                + '<div class="col-sm-3 btn-group">' + Localization.actions.copy[0].outerHTML + '</div>'
@@ -96,218 +93,105 @@ define([
                                                + '</div>'
                                        + '</div>';
                        }
+                       slideStep1 += '</div>';
 
-                       modalContent +=         '</div>'
-                                                       + '</div>'
-                                                       + '<div class="item">'
-                                                               + '<h4>' + TYPO3.lang['localize.view.chooseLanguage'] + '</h4>'
-                                                               + '<div class="t3js-available-languages">'
-                                                               + '</div>'
-                                                       + '</div>'
-                                                       + '<div class="item">'
-                                                               + '<h4>' + TYPO3.lang['localize.view.summary'] + '</h4>'
-                                                               + '<div class="t3js-summary">'
-                                                               + '</div>'
-                                                       + '</div>'
-                                                       + '<div class="item">'
-                                                               + '<h4>' + TYPO3.lang['localize.view.processing'] + '</h4>'
-                                                               + '<div class="t3js-processing">'
-                                                               + '</div>'
-                                                       + '</div>'
-                                               + '</div>'
-                                       + '</div>';
-
-                       var $modal = Modal.confirm(
-                               TYPO3.lang['localize.wizard.header'].replace('{0}', $triggerButton.data('colposName')).replace('{1}', $triggerButton.data('languageName')),
-                               modalContent,
-                               Severity.info, [
-                                       {
-                                               text: TYPO3.lang['localize.wizard.button.cancel'] || 'Cancel',
-                                               active: true,
-                                               btnClass: 'btn-default',
-                                               name: 'cancel',
-                                               trigger: function() {
-                                                       Modal.currentModal.trigger('modal-dismiss');
+                       Wizard.addSlide('localize-choose-action', TYPO3.lang['localize.wizard.header'].replace('{0}', $triggerButton.data('colposName')).replace('{1}', $triggerButton.data('languageName')), slideStep1, Severity.info);
+                       Wizard.addSlide('localize-choose-language', TYPO3.lang['localize.view.chooseLanguage'], '', Severity.info, function($slide) {
+                               Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
+                                       $slide.html(
+                                               $('<div />', {class: 'text-center'}).append(markup)
+                                       );
+                                       Localization.loadAvailableLanguages(
+                                               $triggerButton.data('pageId'),
+                                               $triggerButton.data('colposId'),
+                                               $triggerButton.data('languageId')
+                                       ).done(function(result) {
+                                               if (result.length === 1) {
+                                                       // We only have one result, auto select the record and continue
+                                                       Localization.settings.language = result[0].uid + ''; // we need a string
+                                                       Wizard.unlockNextStep().trigger('click');
+                                                       return;
                                                }
-                                       }, {
-                                               text: TYPO3.lang['localize.wizard.button.next'] || 'Next',
-                                               btnClass: 'btn-info',
-                                               name: 'next'
-                                       }
-                               ], [
-                                       'localization-wizard'
-                               ]
-                       );
 
-                       var $carousel = $modal.find('#localization-carousel'),
-                               slideCount = Math.max(1, $modal.find('#localization-carousel .item').length),
-                               initialStep = Math.round(100 / slideCount),
-                               $modalFooter = $modal.find('.modal-footer'),
-                               $nextButton = $modalFooter.find('button[name="next"]');
+                                               var $languageButtons = $('<div />', {class: 'row', 'data-toggle': 'buttons'});
+
+                                               $.each(result, function(_, languageObject) {
+                                                       $languageButtons.append(
+                                                               $('<div />', {class: 'col-sm-4'}).append(
+                                                                       $('<label />', {class: 'btn btn-default btn-block t3js-option option'}).text(' ' + languageObject.title).prepend(
+                                                                               languageObject.flagIcon
+                                                                       ).prepend(
+                                                                               $('<input />', {
+                                                                                       type: 'radio',
+                                                                                       name: 'language',
+                                                                                       id: 'language' + languageObject.uid,
+                                                                                       value: languageObject.uid,
+                                                                                       style: 'display: none;'
+                                                                               })
+                                                                       )
+                                                               )
+                                                       );
+                                               });
+                                               $slide.html($languageButtons);
+                                       });
+                               });
+                       });
+                       Wizard.addSlide('localize-summary', TYPO3.lang['localize.view.summary'], '', Severity.info, function($slide) {
+                               Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
+                                       $slide.html(
+                                               $('<div />', {class: 'text-center'}).append(markup)
+                                       );
 
-                       $carousel.data('slideCount', slideCount);
-                       $carousel.data('currentSlide', 1);
+                                       Localization.getSummary(
+                                               $triggerButton.data('pageId'),
+                                               $triggerButton.data('colposId')
+                                       ).done(function(result) {
+                                               var $summary = $('<div />', {class: 'row'});
+                                               Localization.records = [];
 
-                       // Append progress bar to modal footer
-                       $modalFooter.prepend(
-                               $('<div />', {class: 'progress'}).append(
-                                       $('<div />', {
-                                               role: 'progressbar',
-                                               class: 'progress-bar',
-                                               'aria-valuemin': 0,
-                                               'aria-valuenow': initialStep,
-                                               'aria-valuemax': 100
-                                       }).width(initialStep + '%').text(TYPO3.lang['localize.progress.step'].replace('{0}', '1').replace('{1}', slideCount))
-                               )
-                       );
+                                               $.each(result, function(_, record) {
+                                                       Localization.records.push(record.uid);
+                                                       $summary.append(
+                                                               $('<div />', {class: 'col-sm-6'}).text(' (' + record.uid + ') ' + record.title).prepend(record.icon)
+                                                       );
+                                               });
+                                               $slide.html($summary);
 
-                       // Disable "next" button on initialization and bind "click" event
-                       $nextButton.prop('disabled', true).on('click', function() {
-                               Localization.synchronizeSlidesHeight($carousel);
-                               $carousel.carousel('next');
+                                               // Unlock button as we don't have an option
+                                               Wizard.unlockNextStep();
+                                       });
+                               });
+                       });
+                       Wizard.addFinalProcessingSlide(function() {
+                               Localization.localizeRecords(
+                                       $triggerButton.data('pageId'),
+                                       $triggerButton.data('languageId'),
+                                       Localization.records
+                               ).done(function() {
+                                       Wizard.dismiss();
+                                       document.location.reload();
+                               });
+                       }).done(function() {
+                               Wizard.show();
                        });
 
-                       // Register "click" event on options
-                       $modal.on('click', '.t3js-option', function() {
+                       Wizard.getComponent().on('click', '.t3js-option', function(e) {
                                var $me = $(this),
                                        $radio = $me.find('input[type="radio"]');
 
                                if ($me.data('helptext')) {
-                                       $modal.find('.t3js-helptext').addClass('text-muted');
-                                       $modal.find($me.data('helptext')).removeClass('text-muted');
+                                       var $container = $(e.delegateTarget);
+                                       $container.find('.t3js-helptext').addClass('text-muted');
+                                       $container.find($me.data('helptext')).removeClass('text-muted');
                                }
                                if ($radio.length > 0) {
                                        Localization.settings[$radio.attr('name')] = $radio.val();
                                }
-                               $nextButton.prop('disabled', false);
-                       });
-
-                       $carousel.on('slide.bs.carousel', function(e) {
-                               var nextSlideNumber = $carousel.data('currentSlide') + 1,
-                                       $modalFooter = $carousel.parent().next();
-
-                               $carousel.data('currentSlide', nextSlideNumber);
-                               $modalFooter.find('.progress-bar')
-                                       .width(initialStep * nextSlideNumber + '%')
-                                       .text(TYPO3.lang['localize.progress.step'].replace('{0}', nextSlideNumber).replace('{1}', slideCount));
-
-                               // Disable next button again
-                               $nextButton.prop('disabled', true);
-                       }).on('slid.bs.carousel', function(e) {
-                               var $activeSlide = $(e.relatedTarget),
-                                       $modalFooter = $carousel.parent().next(),
-                                       $languageView = $activeSlide.find('.t3js-available-languages'),
-                                       $summaryView = $activeSlide.find('.t3js-summary'),
-                                       $processingView = $activeSlide.find('.t3js-processing');
-
-                               if ($languageView.length > 0) {
-                                       // Prepare language view
-                                       Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
-                                               $languageView.html(
-                                                       $('<div />', {class: 'text-center'}).append(markup)
-                                               );
-                                               Localization.loadAvailableLanguages(
-                                                       $triggerButton.data('pageId'),
-                                                       $triggerButton.data('colposId'),
-                                                       $triggerButton.data('languageId')
-                                               ).done(function(result) {
-                                                       if (result.length === 1) {
-                                                               // We only have one result, auto select the record and continue
-                                                               Localization.settings.language = result[0].uid + ''; // we need a string
-                                                               $carousel.carousel('next');
-                                                               return;
-                                                       }
-
-                                                       var $languageButtons = $('<div />', {class: 'row', 'data-toggle': 'buttons'});
-
-                                                       $.each(result, function(_, languageObject) {
-                                                               $languageButtons.append(
-                                                                       $('<div />', {class: 'col-sm-4'}).append(
-                                                                               $('<label />', {class: 'btn btn-default btn-block t3js-option option'}).text(' ' + languageObject.title).prepend(
-                                                                                       languageObject.flagIcon
-                                                                               ).prepend(
-                                                                                       $('<input />', {
-                                                                                               type: 'radio',
-                                                                                               name: 'language',
-                                                                                               id: 'language' + languageObject.uid,
-                                                                                               value: languageObject.uid,
-                                                                                               style: 'display: none;'
-                                                                                       })
-                                                                               )
-                                                                       )
-                                                               );
-                                                       });
-                                                       $languageView.html($languageButtons);
-                                               });
-                                       });
-                               } else if ($summaryView.length > 0) {
-                                       Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
-                                               $summaryView.html(
-                                                       $('<div />', {class: 'text-center'}).append(markup)
-                                               );
-
-                                               Localization.getSummary(
-                                                       $triggerButton.data('pageId'),
-                                                       $triggerButton.data('colposId')
-                                               ).done(function(result) {
-                                                       var $summary = $('<div />', {class: 'row'});
-                                                       Localization.records = [];
-
-                                                       $.each(result, function(_, record) {
-                                                               Localization.records.push(record.uid);
-                                                               $summary.append(
-                                                                       $('<div />', {class: 'col-sm-6'}).text(' (' + record.uid + ') ' + record.title).prepend(record.icon)
-                                                               );
-                                                       });
-                                                       $summaryView.html($summary);
-
-                                                       // Unlock button as we don't have an option
-                                                       $nextButton.prop('disabled', false);
-                                                       $nextButton.text(TYPO3.lang['localize.wizard.button.process'])
-                                               });
-                                       });
-                               } else if ($processingView.length > 0) {
-                                       // Point of no return - hide modal footer disable any closing ability
-                                       $modal.find('.modal-header .close').remove();
-                                       $modalFooter.slideUp();
-
-                                       Icons.getIcon('spinner-circle-dark', Icons.sizes.large).done(function(markup) {
-                                               $processingView.html(
-                                                       $('<div />', {class: 'text-center'}).append(markup)
-                                               );
-
-                                               Localization.localizeRecords(
-                                                       $triggerButton.data('pageId'),
-                                                       $triggerButton.data('languageId'),
-                                                       Localization.records
-                                               ).done(function() {
-                                                       Modal.dismiss();
-                                                       document.location.reload();
-                                               });
-                                       });
-                               }
+                               Wizard.unlockNextStep();
                        });
                });
 
                /**
-                * Synchronize height of slides
-                *
-                * @param {$} $carousel
-                */
-               Localization.synchronizeSlidesHeight = function($carousel) {
-                       var $slides = $carousel.find('.item'),
-                               maxHeight = 0;
-
-                       $slides.each(function(_, slide) {
-                               var height = $(slide).height();
-                               if (height > maxHeight) {
-                                       maxHeight = height;
-                               }
-                       });
-                       $slides.height(maxHeight);
-               };
-
-               /**
                 * Load available languages from page and colPos
                 *
                 * @param {Integer} pageId
index 5ad6a96..eed1538 100644 (file)
@@ -93,7 +93,7 @@ define(['jquery',
         * - confirm.button.ok
         *
         * @param {String} title the title for the confirm modal
-        * @param {String} content the content for the conform modal, e.g. the main question
+        * @param {*} content the content for the conform modal, e.g. the main question
         * @param {int} [severity=Severity.warning] severity default Severity.warning
         * @param {array} [buttons] an array with buttons, default no buttons
         * @param {array} [additionalCssClasses=''] additional css classes to add to the modal
@@ -154,7 +154,7 @@ define(['jquery',
         * - button.clicked
         *
         * @param {String} title the title for the confirm modal
-        * @param {String} content the content for the conform modal, e.g. the main question
+        * @param {*} content the content for the conform modal, e.g. the main question
         * @param {int} severity default Severity.info
         * @param {array} buttons an array with buttons, default no buttons
         * @param {array} additionalCssClasses additional css classes to add to the modal
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/Wizard.js b/typo3/sysext/backend/Resources/Public/JavaScript/Wizard.js
new file mode 100644 (file)
index 0000000..a39acf8
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Module: TYPO3/CMS/Backend/Wizard
+ * API for wizard windows.
+ */
+define(['jquery',
+       'TYPO3/CMS/Backend/Modal',
+       'TYPO3/CMS/Backend/Severity',
+       'TYPO3/CMS/Backend/Icons',
+       'bootstrap'
+], function($, Modal, Severity, Icons) {
+       'use strict';
+
+       try {
+               // fetch from parent
+               if (parent && parent.window.TYPO3 && parent.window.TYPO3.Wizard) {
+                       return parent.window.TYPO3.Wizard;
+               }
+
+               // fetch object from outer frame
+               if (top && top.TYPO3 && top.TYPO3.Wizard) {
+                       return top.TYPO3.Wizard;
+               }
+       } catch (e) {
+               // This only happens if the opener, parent or top is some other url (eg a local file)
+               // which loaded the current window. Then the browser's cross domain policy jumps in
+               // and raises an exception.
+               // For this case we are safe and we can create our global object below.
+       }
+
+       /**
+        * @type {{slides: Array, settings: {}, forceSelection: boolean, $carousel: null}}
+        * @exports TYPO3/CMS/Backend/Wizard
+        */
+       var Wizard = {
+               slides: [],
+               settings: {},
+               forceSelection: true,
+               $carousel: null
+       };
+
+       /**
+        * Initializes the events after building the wizards
+        *
+        * @private
+        */
+       Wizard.initializeEvents = function() {
+               var $modal = Wizard.$carousel.closest('.modal'),
+                       $modalTitle = $modal.find('.modal-title'),
+                       $modalFooter = $modal.find('.modal-footer'),
+                       $nextButton = $modalFooter.find('button[name="next"]');
+
+               $nextButton.on('click', function() {
+                       Wizard.$carousel.carousel('next');
+               });
+
+               Wizard.$carousel.on('slide.bs.carousel', function() {
+                       var nextSlideNumber = Wizard.$carousel.data('currentSlide') + 1,
+                               currentIndex = Wizard.$carousel.data('currentIndex') + 1;
+
+                       $modalTitle.text(Wizard.slides[currentIndex].title);
+
+                       Wizard.$carousel.data('currentSlide', nextSlideNumber);
+                       Wizard.$carousel.data('currentIndex', currentIndex);
+
+                       if (nextSlideNumber >= Wizard.$carousel.data('realSlideCount')) {
+                               // Point of no return - hide modal footer disable any closing ability
+                               $modal.find('.modal-header .close').remove();
+                               $modalFooter.slideUp();
+                       } else {
+                               $modalFooter.find('.progress-bar')
+                                       .width(Wizard.$carousel.data('initialStep') * nextSlideNumber + '%')
+                                       .text(top.TYPO3.lang['wizard.progress']
+                                               .replace('{0}', nextSlideNumber)
+                                               .replace('{1}', Wizard.$carousel.data('slideCount')));
+                       }
+
+                       $nextButton
+                               .removeClass('btn-' + Severity.getCssClass(Wizard.slides[currentIndex -1].severity))
+                               .addClass('btn-' + Severity.getCssClass(Wizard.slides[currentIndex].severity));
+
+                       $modal
+                               .removeClass('t3-modal-' + Severity.getCssClass(Wizard.slides[currentIndex -1].severity))
+                               .addClass('t3-modal-' + Severity.getCssClass(Wizard.slides[currentIndex].severity));
+               }).on('slid.bs.carousel', function(e) {
+                       var currentIndex = Wizard.$carousel.data('currentIndex'),
+                               slide = Wizard.slides[currentIndex];
+
+                       Wizard.runSlideCallback(slide, $(e.relatedTarget));
+
+                       if (Wizard.forceSelection) {
+                               Wizard.lockNextStep();
+                       }
+               });
+
+               /**
+                * Custom event, closes the wizard
+                */
+               var cmp = Wizard.getComponent();
+               cmp.on('wizard-dismiss', Wizard.dismiss);
+
+               Modal.currentModal.on('hidden.bs.modal', function() {
+                       cmp.trigger('wizard-dismissed');
+               }).on('shown.bs.modal', function() {
+                       cmp.trigger('wizard-visible');
+               });
+       };
+
+       /**
+        * @param {String} key
+        * @param {*} value
+        * @returns {Object}
+        */
+       Wizard.set = function(key, value) {
+               Wizard.settings[key] = value;
+               return Wizard;
+       };
+
+       /**
+        * Adds a new slide to the wizard
+        *
+        * @param {String} identifier
+        * @param {String} title
+        * @param {String} content
+        * @param {String} severity
+        * @param {Function} callback
+        * @returns {Object}
+        */
+       Wizard.addSlide = function(identifier, title, content, severity, callback) {
+               Wizard.slides.push({
+                       identifier: identifier,
+                       title: title,
+                       content: content || '',
+                       severity: (typeof severity !== 'undefined' ? severity : Severity.info),
+                       callback: callback
+               });
+
+               return Wizard;
+       };
+
+       /**
+        * Adds a final processing slide
+        *
+        * @param {Function} callback
+        * @returns {Object}
+        */
+       Wizard.addFinalProcessingSlide = function(callback) {
+               if (typeof callback !== 'function') {
+                       callback = function() {
+                               Wizard.dismiss();
+                       }
+               }
+
+               return Icons.getIcon('spinner-circle-dark', Icons.sizes.large, null, null).done(function(markup) {
+                       var $processingSlide = $('<div />', {class: 'text-center'}).append(markup);
+                       Wizard.addSlide(
+                               'final-processing-slide', top.TYPO3.lang['wizard.processing.title'],
+                               $processingSlide[0].outerHTML,
+                               Severity.info,
+                               callback
+                       );
+               });
+       };
+
+       /**
+        * Processes the footer of the modal
+        *
+        * @private
+        */
+       Wizard.addProgressBar = function() {
+               var realSlideCount = Wizard.$carousel.find('.item').length,
+                       slideCount = Math.max(1, realSlideCount),
+                       initialStep,
+                       $modal = Wizard.$carousel.closest('.modal'),
+                       $modalFooter = $modal.find('.modal-footer');
+
+               initialStep = Math.round(100 / slideCount);
+
+               Wizard.$carousel
+                       .data('initialStep', initialStep)
+                       .data('slideCount', slideCount)
+                       .data('realSlideCount', realSlideCount)
+                       .data('currentIndex', 0)
+                       .data('currentSlide', 1);
+
+               // Append progress bar to modal footer
+               if (slideCount > 1) {
+                       $modalFooter.prepend(
+                               $('<div />', {class: 'progress'}).append(
+                                       $('<div />', {
+                                               role: 'progressbar',
+                                               class: 'progress-bar',
+                                               'aria-valuemin': 0,
+                                               'aria-valuenow': initialStep,
+                                               'aria-valuemax': 100
+                                       }).width(initialStep + '%').text(
+                                               top.TYPO3.lang['wizard.progress']
+                                                       .replace('{0}', '1')
+                                                       .replace('{1}', slideCount)
+                                       )
+                               )
+                       );
+               }
+       };
+
+       /**
+        * Generates the markup of slides added by addSlide()
+        *
+        * @returns {$}
+        * @private
+        */
+       Wizard.generateSlides = function() {
+               var slides =
+                       '<div class="carousel slide" data-ride="carousel" data-interval="false">'
+                       + '<div class="carousel-inner" role="listbox">';
+
+               for (var i = 0; i < Wizard.slides.length; ++i) {
+                       var currentSlide = Wizard.slides[i],
+                               slideContent = currentSlide.content;
+
+                       if (typeof slideContent === 'object') {
+                               slideContent = slideContent.html();
+                       }
+                       slides += '<div class="item" data-slide="' + currentSlide.identifier + '">' + slideContent + '</div>';
+               }
+
+               slides += '</div></div>';
+
+               Wizard.$carousel = $(slides);
+               Wizard.$carousel.find('.item').first().addClass('active');
+
+               return Wizard.$carousel;
+       };
+
+       /**
+        * Renders the wizard
+        *
+        * @returns {$}
+        */
+       Wizard.show = function() {
+               var $slides = Wizard.generateSlides(),
+                       firstSlide = Wizard.slides[0];
+
+               var $modal = Modal.confirm(
+                       firstSlide.title,
+                       $slides,
+                       firstSlide.severity,
+                       [{
+                               text: top.TYPO3.lang['wizard.button.cancel'],
+                               active: true,
+                               btnClass: 'btn-default',
+                               name: 'cancel',
+                               trigger: function() {
+                                       Wizard.getComponent().trigger('wizard-dismiss');
+                               }
+                       }, {
+                               text: top.TYPO3.lang['wizard.button.next'],
+                               btnClass: 'btn-' + Severity.getCssClass(firstSlide.severity),
+                               name: 'next'
+                       }]
+               );
+               $modal.on('hidden.bs.modal', function() {
+                       Wizard.slides = [];
+                       Wizard.settings = [];
+                       Wizard.forceSelection = true;
+               });
+
+               Wizard.runSlideCallback(firstSlide, Wizard.$carousel.find('.item').first());
+
+               if (Wizard.forceSelection) {
+                       Wizard.lockNextStep();
+               }
+
+               Wizard.addProgressBar($modal);
+               Wizard.initializeEvents();
+       };
+
+       /**
+        * Runs the callback for the given slide
+        *
+        * @param {Object} slide
+        * @param {$} $slide
+        * @private
+        */
+       Wizard.runSlideCallback = function(slide, $slide) {
+               if (typeof slide.callback === 'function') {
+                       slide.callback($slide, Wizard.settings);
+               }
+       };
+
+       /**
+        * Get the wizard component
+        *
+        * @returns {$}
+        */
+       Wizard.getComponent = function() {
+               return Wizard.$carousel.parent();
+       };
+
+       /**
+        * Closes the wizard window
+        */
+       Wizard.dismiss = function() {
+               Modal.dismiss();
+       };
+
+       /**
+        * Locks the button for continuing to the next step
+        *
+        * @returns {$}
+        */
+       Wizard.lockNextStep = function() {
+               var $button = Wizard.$carousel.closest('.modal').find('button[name="next"]');
+               $button.prop('disabled', true);
+
+               return $button;
+       };
+
+       /**
+        * Unlocks the button for continuing to the next step
+        *
+        * @returns {$}
+        */
+       Wizard.unlockNextStep = function() {
+               var $button = Wizard.$carousel.closest('.modal').find('button[name="next"]');
+               $button.prop('disabled', false);
+
+               return $button;
+       };
+
+       // expose as global object
+       TYPO3.Wizard = Wizard;
+
+       return Wizard;
+});
\ No newline at end of file
index c661fdb..74af408 100644 (file)
@@ -1335,6 +1335,8 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
 
             // Debugger Console strings
             $this->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/debugger.xlf');
+
+            $this->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/wizard.xlf');
         }
         /** @var $extDirect \TYPO3\CMS\Core\ExtDirect\ExtDirectApi */
         $extDirect = GeneralUtility::makeInstance(\TYPO3\CMS\Core\ExtDirect\ExtDirectApi::class);
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-73429-WizardComponentHasBeenAdded.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-73429-WizardComponentHasBeenAdded.rst
new file mode 100644 (file)
index 0000000..158b5f7
--- /dev/null
@@ -0,0 +1,110 @@
+=================================================
+Feature: #73429 - Wizard component has been added
+=================================================
+
+Description
+===========
+
+A new wizard component has been added. This component may be used for user-guided interactions.
+The RequireJS module can be used by including ``TYPO3/CMS/Backend/Wizard``.
+
+The wizard supports straight forward actions only, junctions are not possible yet.
+
+
+Impact
+======
+
+The wizard component has some public methods:
+#. :code:`addSlide(identifier, title, content, severity, callback)`
+#. :code:`addFinalProcessingSlide(callback)`
+#. :code:`set(key, value)`
+#. :code:`show()`
+#. :code:`dismiss()`
+#. :code:`getComponent()`
+#. :code:`lockNextStep()`
+#. :code:`unlockNextStep()`
+
+addSlide
+~~~~~~~~
+
+Adds a slide to the wizard.
+
+========== =============== ============ ======================================================================================================
+Name       DataType        Mandatory    Description
+========== =============== ============ ======================================================================================================
+identifier string          Yes          The internal identifier of the slide
+title      string          Yes          The title of the slide
+content    string          Yes          The content of the slide
+severity   int                          Represents the severity of a slide. Please see TYPO3.Severity. Default is :code:`TYPO3.Severity.info`.
+callback   function                     Callback method run after the slide appeared. The callback receives two parameters:
+                                        :code:`$slide`: The current slide as a jQuery object
+                                        :code:`settings`: The settings defined via :js:`Wizard.set()`
+========== =============== ============ ======================================================================================================
+
+
+addFinalProcessingSlide
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Adds a slide to the wizard containing a spinner. This should always be the latest slide. This method returns a Promise
+object due to internal handling. This means you have to add a :js:`done()` callback containing :js:`Wizard.show()`,
+please see the example below.
+
+========== =============== ============ ======================================================================================================
+Name       DataType        Mandatory    Description
+========== =============== ============ ======================================================================================================
+callback   function                     Callback method run after the slide appeared. If no callback method is given, the wizard dismisses
+                                        without any further action.
+========== =============== ============ ======================================================================================================
+
+Example code:
+
+.. code-block:: js
+
+        Wizard.addFinalProcessingSlide().done(function() {
+            Wizard.show();
+        });
+
+set
+~~~
+
+Adds values to the internal settings stack usable in other slides.
+
+========== =============== ============ ======================================================================================================
+Name       DataType        Mandatory    Description
+========== =============== ============ ======================================================================================================
+key        string          Yes          The key of the setting
+value      string          Yes          The value of the setting
+========== =============== ============ ======================================================================================================
+
+Events
+~~~~~~
+
+The event `wizard-visible` is fired when the wizard rendering has finished.
+
+Example code:
+
+.. code-block:: js
+
+        Wizard.getComponent().on('wizard-visible', function() {
+            Wizard.unlockNextButton();
+        });
+
+
+Wizards can be closed by firing the `wizard-dismiss` event.
+
+Example code:
+
+.. code-block:: js
+
+        Wizard.getComponent().trigger('wizard-dismiss');
+
+
+Wizards fire the `wizard-dismissed` event if the wizard is closed. You can integrate your own listener by using :js:`Wizard.getComponent()`.
+
+Example code:
+
+.. code-block:: js
+
+        Wizard.getComponent().on('wizard-dismissed', function() {
+            // Calculate the answer of life the universe and everything
+        });
diff --git a/typo3/sysext/core/Resources/Private/Language/wizard.xlf b/typo3/sysext/core/Resources/Private/Language/wizard.xlf
new file mode 100644 (file)
index 0000000..690d985
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
+       <file t3:id="1455466284" source-language="en" datatype="plaintext" original="messages" date="2016-02-14T16:11:24Z" product-name="core">
+               <header/>
+               <body>
+                       <trans-unit id="wizard.button.next">
+                               <source>Next</source>
+                       </trans-unit>
+                       <trans-unit id="wizard.button.cancel">
+                               <source>Cancel</source>
+                       </trans-unit>
+                       <trans-unit id="wizard.progress">
+                               <source>Step {0} of {1}</source>
+                       </trans-unit>
+                       <trans-unit id="wizard.processing.title">
+                               <source>Processing...</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
\ No newline at end of file