.../tour/config/tour.tooltip.admin-content-en.yml | 2 - .../modules/tour/config/tour.tooltip.search-en.yml | 2 - core/modules/tour/css/tour-rtl.css | 13 + core/modules/tour/css/tour.css | 35 ++- core/modules/tour/js/tour.js | 326 ++++++++++++-------- core/modules/tour/tour.info | 2 +- core/modules/tour/tour.module | 68 ++-- 7 files changed, 281 insertions(+), 167 deletions(-) diff --git a/core/modules/tour/config/tour.tooltip.admin-content-en.yml b/core/modules/tour/config/tour.tooltip.admin-content-en.yml index 064051a..bae9577 100644 --- a/core/modules/tour/config/tour.tooltip.admin-content-en.yml +++ b/core/modules/tour/config/tour.tooltip.admin-content-en.yml @@ -9,5 +9,3 @@ attributes: data-options: tipLocation:top data-id: node-admin-content data-text: Next - class: - - custom diff --git a/core/modules/tour/config/tour.tooltip.search-en.yml b/core/modules/tour/config/tour.tooltip.search-en.yml index 51f824b..7593177 100644 --- a/core/modules/tour/config/tour.tooltip.search-en.yml +++ b/core/modules/tour/config/tour.tooltip.search-en.yml @@ -9,5 +9,3 @@ weight: "1" attributes: data-id: search-block-form data-text: Next - class: - - custom diff --git a/core/modules/tour/css/tour-rtl.css b/core/modules/tour/css/tour-rtl.css new file mode 100644 index 0000000..5e19f7b --- /dev/null +++ b/core/modules/tour/css/tour-rtl.css @@ -0,0 +1,13 @@ +/** + * @file + * RTL styling for tour module. + */ + +.js .toolbar .bar .tour-toolbar-tab.tab { + float: left; +} + +.tour-progress { + right: 0; + left: 15px; +} diff --git a/core/modules/tour/css/tour.css b/core/modules/tour/css/tour.css index 9125c61..9f71ccb 100644 --- a/core/modules/tour/css/tour.css +++ b/core/modules/tour/css/tour.css @@ -1,21 +1,36 @@ -#tour-items { - display: none; -} - -.toolbar .icon-help.tour-no-items-hidden { - display: none; -} +/** + * @file + * Styling for tour module. + */ +/* Tab appearance. */ .js .toolbar .bar .tour-toolbar-tab.tab { - float: right; + float: right; /* LTR */ +} +.js .toolbar .bar .tour-toolbar-tab button { + padding-bottom: 1em; + padding-top: 1em; + color: #fff; + font-weight: bold; +} +.js .toolbar .bar .tour-toolbar-tab button.active { + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%); + background-image: linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%); } +/* Joyride tips should always be on top of everything else. */ .joyride-tip-guide { - z-index: 502 + z-index: 999; } +/* Override placement of the tour progress indicator. */ .tour-progress { position: absolute; bottom: 10px; - right: 15px; + right: 15px; /* LTR */ +} + +/* @todo Remove once http://drupal.org/node/1916690 is resolved. */ +.js .toolbar .bar .tour-toolbar-tab.tab.element-hidden { + display: none; } diff --git a/core/modules/tour/js/tour.js b/core/modules/tour/js/tour.js index 70e97f5..975a258 100644 --- a/core/modules/tour/js/tour.js +++ b/core/modules/tour/js/tour.js @@ -1,130 +1,202 @@ -(function ($, Drupal) { - - "use strict"; - - Drupal.behaviors.tour = { - attach: function (context) { - - var toolbar_id = "#toolbar-tab-tour"; - var toolbar_class = "tour-no-items-hidden"; - var tour_items = "#tour-items"; - var overlay_content = "#overlay-content " + tour_items; - - /** - * Initializes the tour. - */ - function setupTour(items, scope) { - var $toolbar = $(toolbar_id, scope); - - if ($(items).length) { - $toolbar.removeClass(toolbar_class); - - // Loop over items and remove missing tour items. - $("li", items).each(function (index) { - var tour_item_id = $(this).data("id"); - var tour_item_class = $(this).data("class"); - if (!$("#" + tour_item_id).length && !$("." + tour_item_class).length) { - $(this).remove(); - } - else { - $(this).data("text", Drupal.t("Next")); - } - }); - - // Update the last item to have "End tour" as the button. - $("li", items).last().data("text", Drupal.t("End tour")); - } - else { - $toolbar.addClass(toolbar_class); - } - $toolbar.bind("click.tour", function() { - if ((Drupal.overlay && Drupal.overlay.isOpen)|| !window) { - return false; - } - if ($(toolbar_id + ".touring", scope).length) { - // Allow other scripts to respond to this event. - $(document).trigger('drupalTourBeforeClose'); - - $(items).prop("hidden", true).joyride("destroy"); - closeTour(scope); - } - else { - // Allow other scripts to respond to this event. - $(document).trigger('drupalTourBeforeOpen'); - - $toolbar.addClass("touring"); - $(items).prop("hidden", false).joyride({ - "postRideCallback": function() { - $toolbar.removeClass("touring"); - $(document).trigger('drupalTourFinished'); - } - }); - } - return false; - }); - } - - /** - * Cleans up the tour. - */ - function closeTour(scope) { - $(toolbar_id, scope).removeClass("touring"); - } - - /** - * Detaches up the tour. - */ - function detachTour(scope) { - closeTour(scope); - $(toolbar_id, scope).unbind("click.tour"); - } - - /** - * Initialize the appropriate tour. - */ - if ($(overlay_content).length) { - $(document).bind('drupalOverlayBeforeLoad.drupal-overlay.drupal-overlay-child-loading', function() { - // Remove tour events associated with overlay. - detachTour(window.parent.document); - }); - - // Remove tour events associated with overlay. - detachTour(window.parent.document); - // Attach it based on outer content. - setupTour(overlay_content, window.parent.document); - } - else { - $(document).bind("drupalOverlayBeforeClose", function() { - // Remove tour events associated with overlay. - detachTour(window.document); - // Attach it based on outer content. - setupTour(tour_items, window.document); - }); - $(document).bind("drupalOverlayBeforeLoad", function() { - // Remove tour events associated with overlay. - detachTour(window.document); - }); - $(document).bind("drupalOverlayReady", function() { - if ($(tour_items).length) { - $(toolbar_id, window.parent.document).addClass(toolbar_class); - } - else { - $(toolbar_id, window.parent.document).removeClass(toolbar_class); - } - }); - - // Don't run this in overlay. - // This only runs once in the parent. - if (window == window.parent) { - $("body").once("tour", function() { - setupTour(tour_items, window.document); - }); - } - else { - setupTour(tour_items, window.document); +/** + * @file + * Attaches behaviors for the Tour module's toolbar tab. + */ + +(function ($, Backbone, Drupal, document) { + +"use strict"; + +/** + * Attaches the tour's toolbar tab behavior. + */ +Drupal.behaviors.tour = { + attach: function (context) { + var model = new Drupal.tour.models.StateModel(); + var view = new Drupal.tour.views.ToggleTourView({ + el: $(context).find('#toolbar-tab-tour'), + model: model + }); + + // Update the model based on Overlay events. + $(document) + // Overlay is opening: cancel tour if active and mark overlay as open. + .on('drupalOverlayOpen.tour', function () { + model.set({ isActive: false, overlayIsOpen: true }); + }) + // Overlay is loading a new URL: clear tour & cancel if active. + .on('drupalOverlayBeforeLoad.tour', function () { + model.set({ isActive: false, overlayTour: [] }); + }) + // Overlay is closing: clear tour & cancel if active, mark overlay closed. + .on('drupalOverlayClose.tour', function () { + model.set({ isActive: false, overlayIsOpen: false, overlayTour: [] }); + }) + // Overlay has loaded DOM: check whether a tour is available. + .on('drupalOverlayReady.tour', function () { + // We must select the tour in the Overlay's window using the Overlay's + // jQuery, because the joyride plugin only works for the window in which + // it was loaded. + // @todo Make upstream contribution so this can be simplified, which + // should also allow us to *not* load jquery.joyride.js in the Overlay, + // resulting in better front-end performance. + var overlay = Drupal.overlay.iframeWindow; + var $overlayContext = overlay.jQuery(overlay.document); + model.set('overlayTour', $overlayContext.find('#tour')); + }); + + model + // Allow other scripts to respond to tour events. + .on('change:isActive', function (model, isActive) { + $(document).trigger((isActive) ? 'drupalTourStarted' : 'drupalTourStopped'); + }) + // Initialization: check whether a tour is available on the current page. + .set('tour', $(context).find('#tour')); + } +}; + +Drupal.tour = Drupal.tour || { models: {}, views: {}}; + +/** + * Backbone Model for tours. + */ +Drupal.tour.models.StateModel = Backbone.Model.extend({ + defaults: { + // Indicates whether the Drupal root window has a tour. + tour: [], + // Indicates whether the Overlay is open. + overlayIsOpen: false, + // Indicates whether the Overlay window has a tour. + overlayTour: [], + // Indicates whether the tour is currently running. + isActive: false, + // Indicates which tour is the active one (necessary to cleanly stop). + activeTour: [] + } +}); + +/** + * Handles edit mode toggle interactions. + */ +Drupal.tour.views.ToggleTourView = Backbone.View.extend({ + + events: { 'click': 'onClick' }, + + /** + * Implements Backbone Views' initialize(). + */ + initialize: function () { + this.model.on('change:tour change:overlayTour change:overlayIsOpen change:isActive', this.render, this); + this.model.on('change:isActive', this.toggleTour, this); + }, + + /** + * Implements Backbone Views' render(). + */ + render: function () { + // Render the visibility. + this.$el.toggleClass('element-hidden', this._getTour().length === 0); + // Render the state. + var isActive = this.model.get('isActive'); + this.$el.find('button') + .toggleClass('active', isActive) + .attr('aria-pressed', isActive); + return this; + }, + + /** + * Model change handler; starts or stops the tour. + */ + toggleTour: function() { + if (this.model.get('isActive')) { + var $tour = this._getTour(); + this._removeIrrelevantTourItems($tour, this._getDocument()); + var that = this; + $tour.joyride({ + postRideCallback: function () { that.model.set('isActive', false); } + }); + this.model.set({ isActive: true, activeTour: $tour }); + } + else { + this.model.get('activeTour').joyride('destroy'); + this.model.set({ isActive: false, activeTour: [] }); + } + }, + + /** + * Toolbar tab click event handler; toggles isActive. + */ + onClick: function (event) { + this.model.set('isActive', !this.model.get('isActive')); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Gets the tour. + * + * @return jQuery + * A jQuery element pointing to a