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