/** * @file * Attaches behaviors for the Tour module's toolbar tab. */ (function ($, Backbone, Drupal, document) { const queryString = decodeURI(window.location.search); /** * Attaches the tour's toolbar tab behavior. * * It uses the query string for: * - tour: When ?tour=1 is present, the tour will start automatically after * the page has loaded. * - tips: Pass ?tips=class in the url to filter the available tips to the * subset which match the given class. * * @example * http://example.com/foo?tour=1&tips=bar * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attach tour functionality on `tour` events. */ Drupal.behaviors.tour = { attach(context) { $('body').once('tour').each(() => { const model = new Drupal.tour.models.StateModel(); new Drupal.tour.views.ToggleTourView({ el: $(context).find('#toolbar-tab-tour'), model, }); model // Allow other scripts to respond to tour events. .on('change:isActive', (model, isActive) => { $(document).trigger((isActive) ? 'drupalTourStarted' : 'drupalTourStopped'); }) // Initialization: check whether a tour is available on the current // page. .set('tour', $(context).find('ol#tour')); // Start the tour immediately if toggled via query string. if (/tour=?/i.test(queryString)) { model.set('isActive', true); } }); }, }; /** * @namespace */ Drupal.tour = Drupal.tour || { /** * @namespace Drupal.tour.models */ models: {}, /** * @namespace Drupal.tour.views */ views: {}, }; /** * Backbone Model for tours. * * @constructor * * @augments Backbone.Model */ Drupal.tour.models.StateModel = Backbone.Model.extend(/** @lends Drupal.tour.models.StateModel# */{ /** * @type {object} */ defaults: /** @lends Drupal.tour.models.StateModel# */{ /** * Indicates whether the Drupal root window has a tour. * * @type {Array} */ tour: [], /** * Indicates whether the tour is currently running. * * @type {bool} */ isActive: false, /** * Indicates which tour is the active one (necessary to cleanly stop). * * @type {Array} */ activeTour: [], }, }); Drupal.tour.views.ToggleTourView = Backbone.View.extend(/** @lends Drupal.tour.views.ToggleTourView# */{ /** * @type {object} */ events: { click: 'onClick' }, /** * Handles edit mode toggle interactions. * * @constructs * * @augments Backbone.View */ initialize() { this.listenTo(this.model, 'change:tour change:isActive', this.render); this.listenTo(this.model, 'change:isActive', this.toggleTour); }, /** * @inheritdoc * * @return {Drupal.tour.views.ToggleTourView} * The `ToggleTourView` view. */ render() { // Render the visibility. this.$el.toggleClass('hidden', this._getTour().length === 0); // Render the state. const isActive = this.model.get('isActive'); this.$el.find('button') .toggleClass('is-active', isActive) .prop('aria-pressed', isActive); return this; }, /** * Model change handler; starts or stops the tour. */ toggleTour() { if (this.model.get('isActive')) { const $tour = this._getTour(); this._removeIrrelevantTourItems($tour, this._getDocument()); const that = this; const close = Drupal.t('Close'); if ($tour.find('li').length) { $tour.joyride({ autoStart: true, postRideCallback() { that.model.set('isActive', false); }, // HTML segments for tip layout. template: { link: `×`, button: '', }, }); 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. * * @param {jQuery.Event} event * The click event. */ onClick(event) { this.model.set('isActive', !this.model.get('isActive')); event.preventDefault(); event.stopPropagation(); }, /** * Gets the tour. * * @return {jQuery} * A jQuery element pointing to a `
    ` containing tour items. */ _getTour() { return this.model.get('tour'); }, /** * Gets the relevant document as a jQuery element. * * @return {jQuery} * A jQuery element pointing to the document within which a tour would be * started given the current state. */ _getDocument() { return $(document); }, /** * Removes tour items for elements that don't have matching page elements. * * Or that are explicitly filtered out via the 'tips' query string. * * @example * This will filter out tips that do not have a matching * page element or don't have the "bar" class. * http://example.com/foo?tips=bar * * @param {jQuery} $tour * A jQuery element pointing to a `
      ` containing tour items. * @param {jQuery} $document * A jQuery element pointing to the document within which the elements * should be sought. * * @see Drupal.tour.views.ToggleTourView#_getDocument */ _removeIrrelevantTourItems($tour, $document) { let removals = false; const tips = /tips=([^&]+)/.exec(queryString); $tour .find('li') .each(function () { const $this = $(this); const itemId = $this.attr('data-id'); const itemClass = $this.attr('data-class'); // If the query parameter 'tips' is set, remove all tips that don't // have the matching class. if (tips && !$(this).hasClass(tips[1])) { removals = true; $this.remove(); return; } // Remove tip from the DOM if there is no corresponding page element. if ((!itemId && !itemClass) || (itemId && $document.find(`#${itemId}`).length) || (itemClass && $document.find(`.${itemClass}`).length)) { return; } removals = true; $this.remove(); }); // If there were removals, we'll have to do some clean-up. if (removals) { const total = $tour.find('li').length; if (!total) { this.model.set({ tour: [] }); } $tour .find('li') // Rebuild the progress data. .each(function (index) { const progress = Drupal.t('!tour_item of !total', { '!tour_item': index + 1, '!total': total }); $(this).find('.tour-progress').text(progress); }) // Update the last item to have "End tour" as the button. .eq(-1) .attr('data-text', Drupal.t('End tour')); } }, }); }(jQuery, Backbone, Drupal, document));