415 lines
15 KiB
JavaScript
415 lines
15 KiB
JavaScript
/**
|
|
* @file
|
|
* Attaches behaviors for the Tour module's toolbar tab.
|
|
*/
|
|
|
|
(($, Backbone, Drupal, settings, document, Shepherd) => {
|
|
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) {
|
|
once('tour', 'body').forEach(() => {
|
|
const model = new Drupal.tour.models.StateModel();
|
|
// eslint-disable-next-line no-new
|
|
new Drupal.tour.views.ToggleTourView({
|
|
el: $(context).find('#toolbar-tab-tour'),
|
|
model,
|
|
});
|
|
|
|
model
|
|
// Allow other scripts to respond to tour events.
|
|
.on('change:isActive', (tourModel, isActive) => {
|
|
$(document).trigger(
|
|
isActive ? 'drupalTourStarted' : 'drupalTourStopped',
|
|
);
|
|
});
|
|
// Initialization: check whether a tour is available on the current
|
|
// page.
|
|
if (settings._tour_internal) {
|
|
model.set('tour', settings._tour_internal);
|
|
}
|
|
// 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)
|
|
.attr('aria-pressed', isActive);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Model change handler; starts or stops the tour.
|
|
*/
|
|
toggleTour() {
|
|
if (this.model.get('isActive')) {
|
|
this._removeIrrelevantTourItems(this._getTour());
|
|
const tourItems = this.model.get('tour');
|
|
const that = this;
|
|
|
|
if (tourItems.length) {
|
|
// If Joyride is positioned relative to the top or bottom of an
|
|
// element, and its secondary position is right or left, then the
|
|
// arrow is also positioned right or left. Shepherd defaults to
|
|
// center positioning the arrow.
|
|
//
|
|
// In most cases, this arrow positioning difference has
|
|
// little impact. However, tours built with Joyride may have tips
|
|
// using a higher level selector than the element the tip is
|
|
// expected to point to, and relied on Joyride's arrow positioning
|
|
// to align the arrow with the expected reference element. Joyride's
|
|
// arrow positioning behavior is replicated here to prevent those
|
|
// use cases from causing UI regressions.
|
|
//
|
|
// This modifier is provided here instead of TourViewBuilder (where
|
|
// most position modifications are) because it includes adding a
|
|
// JavaScript callback function.
|
|
settings.tourShepherdConfig.defaultStepOptions.popperOptions.modifiers.push(
|
|
{
|
|
name: 'moveArrowJoyridePosition',
|
|
enabled: true,
|
|
phase: 'write',
|
|
fn({ state }) {
|
|
const { arrow } = state.elements;
|
|
const { placement } = state;
|
|
if (
|
|
arrow &&
|
|
/^top|bottom/.test(placement) &&
|
|
/-start|-end$/.test(placement)
|
|
) {
|
|
const horizontalPosition = placement.split('-')[1];
|
|
const offset =
|
|
horizontalPosition === 'start'
|
|
? 28
|
|
: state.elements.popper.clientWidth - 56;
|
|
arrow.style.transform = `translate3d(${offset}px, 0px, 0px)`;
|
|
}
|
|
},
|
|
},
|
|
);
|
|
const shepherdTour = new Shepherd.Tour(settings.tourShepherdConfig);
|
|
shepherdTour.on('cancel', () => {
|
|
that.model.set('isActive', false);
|
|
});
|
|
shepherdTour.on('complete', () => {
|
|
that.model.set('isActive', false);
|
|
});
|
|
|
|
tourItems.forEach((tourStepConfig, index) => {
|
|
// Create the configuration for a given tour step by using values
|
|
// defined in TourViewBuilder.
|
|
// @see \Drupal\tour\TourViewBuilder::viewMultiple()
|
|
const tourItemOptions = {
|
|
title: tourStepConfig.title
|
|
? Drupal.checkPlain(tourStepConfig.title)
|
|
: null,
|
|
text: () => Drupal.theme('tourItemContent', tourStepConfig),
|
|
attachTo: tourStepConfig.attachTo,
|
|
buttons: [Drupal.tour.nextButton(shepherdTour, tourStepConfig)],
|
|
classes: tourStepConfig.classes,
|
|
index,
|
|
};
|
|
|
|
tourItemOptions.when = {
|
|
show() {
|
|
const nextButton =
|
|
shepherdTour.currentStep.el.querySelector('footer button');
|
|
|
|
// Drupal disables Shepherd's built in focus after item
|
|
// creation functionality due to focus being set on the tour
|
|
// item container after every scroll and resize event. In its
|
|
// place, the 'next' button is focused here.
|
|
nextButton.focus();
|
|
|
|
// When Stable or Stable 9 are part of the active theme, the
|
|
// Drupal.tour.convertToJoyrideMarkup() function is available.
|
|
// This function converts Shepherd markup to Joyride markup,
|
|
// facilitating the use of the Shepherd library that is
|
|
// backwards compatible with customizations intended for
|
|
// Joyride.
|
|
// The Drupal.tour.convertToJoyrideMarkup() function is
|
|
// internal, and will eventually be removed from Drupal core.
|
|
if (Drupal.tour.hasOwnProperty('convertToJoyrideMarkup')) {
|
|
Drupal.tour.convertToJoyrideMarkup(shepherdTour);
|
|
}
|
|
},
|
|
};
|
|
|
|
shepherdTour.addStep(tourItemOptions);
|
|
});
|
|
shepherdTour.start();
|
|
this.model.set({ isActive: true, activeTour: shepherdTour });
|
|
}
|
|
} else {
|
|
this.model.get('activeTour').cancel();
|
|
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 {array}
|
|
* An array of Shepherd tour item objects.
|
|
*/
|
|
_getTour() {
|
|
return this.model.get('tour');
|
|
},
|
|
|
|
/**
|
|
* Removes tour items for elements that don't have matching page elements.
|
|
*
|
|
* Or that are explicitly filtered out via the 'tips' query string.
|
|
*
|
|
* @example
|
|
* <caption>This will filter out tips that do not have a matching
|
|
* page element or don't have the "bar" class.</caption>
|
|
* http://example.com/foo?tips=bar
|
|
*
|
|
* @param {Object[]} tourItems
|
|
* An array containing tour Step config objects.
|
|
* The object properties relevant to this function:
|
|
* - classes {string}: A string of classes to be added to the tour step
|
|
* when rendered.
|
|
* - selector {string}: The selector a tour step is associated with.
|
|
*/
|
|
_removeIrrelevantTourItems(tourItems) {
|
|
const tips = /tips=([^&]+)/.exec(queryString);
|
|
const filteredTour = tourItems.filter((tourItem) => {
|
|
// If the query parameter 'tips' is set, remove all tips that don't
|
|
// have the matching class. The `tourItem` variable is a step config
|
|
// object, and the 'classes' property is a ShepherdJS Step() config
|
|
// option that provides a string.
|
|
if (
|
|
tips &&
|
|
tourItem.hasOwnProperty('classes') &&
|
|
tourItem.classes.indexOf(tips[1]) === -1
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// If a selector is configured but there isn't a matching element,
|
|
// return false.
|
|
return !(
|
|
tourItem.selector && !document.querySelector(tourItem.selector)
|
|
);
|
|
});
|
|
|
|
// If there are tours filtered, we'll have to update model.
|
|
if (tourItems.length !== filteredTour.length) {
|
|
filteredTour.forEach((filteredTourItem, filteredTourItemId) => {
|
|
filteredTour[filteredTourItemId].counter = Drupal.t(
|
|
'!tour_item of !total',
|
|
{
|
|
'!tour_item': filteredTourItemId + 1,
|
|
'!total': filteredTour.length,
|
|
},
|
|
);
|
|
|
|
if (filteredTourItemId === filteredTour.length - 1) {
|
|
filteredTour[filteredTourItemId].cancelText =
|
|
Drupal.t('End tour');
|
|
}
|
|
});
|
|
this.model.set('tour', filteredTour);
|
|
}
|
|
},
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Provides an object that will become the tour item's 'next' button.
|
|
*
|
|
* Similar to a theme function, themes can override this function to customize
|
|
* the resulting button. Unlike a theme function, it returns an object instead
|
|
* of a string, which is why it is not part of Drupal.theme.
|
|
*
|
|
* @param {Tour} shepherdTour
|
|
* A class representing a Shepherd site tour.
|
|
* @param {Object} tourStepConfig
|
|
* An object generated in TourViewBuilder used for creating the options
|
|
* passed to `Tour.addStep(options)`.
|
|
* Contains the following properties:
|
|
* - id {string}: The tour.tip ID specified by its config
|
|
* - selector {string|null}: The selector of the element the tour step is
|
|
* attaching to.
|
|
* - module {string}: The module providing the tip plugin used by this step.
|
|
* - counter {string}: A string indicating which tour step this is out of
|
|
* how many total steps.
|
|
* - attachTo {Object} This is directly mapped to the `attachTo` Step()
|
|
* option. It has two properties:
|
|
* - element {string}: The selector of the element the step attaches to.
|
|
* - on {string}: a PopperJS compatible string to specify step position.
|
|
* - classes {string}: Will be added to the class attribute of the step.
|
|
* - body {string}: Markup that is mapped to the `text` Step() option. Will
|
|
* become the step content.
|
|
* - title {string}: is mapped to the `title` Step() option.
|
|
*
|
|
* @return {{classes: string, action: string, text: string}}
|
|
* An object structured in the manner Shepherd requires to create the
|
|
* 'next' button.
|
|
*
|
|
* @see https://shepherdjs.dev/docs/Tour.html
|
|
* @see \Drupal\tour\TourViewBuilder::viewMultiple()
|
|
* @see https://shepherdjs.dev/docs/Step.html
|
|
*/
|
|
Drupal.tour.nextButton = (shepherdTour, tourStepConfig) => {
|
|
return {
|
|
classes: 'button button--primary',
|
|
text: tourStepConfig.cancelText
|
|
? tourStepConfig.cancelText
|
|
: Drupal.t('Next'),
|
|
action: tourStepConfig.cancelText
|
|
? shepherdTour.cancel
|
|
: shepherdTour.next,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Theme function for tour item content.
|
|
*
|
|
* @param {Object} tourStepConfig
|
|
* An object generated in TourViewBuilder used for creating the options
|
|
* passed to `Tour.addStep(options)`.
|
|
* Contains the following properties:
|
|
* - id {string}: The tour.tip ID specified by its config
|
|
* - selector {string|null}: The selector of the element the tour step is
|
|
* attaching to.
|
|
* - module {string}: The module providing the tip plugin used by this step.
|
|
* - counter {string}: A string indicating which tour step this is out of
|
|
* how many total steps.
|
|
* - attachTo {Object} This is directly mapped to the `attachTo` Step()
|
|
* option. It has two properties:
|
|
* - element {string}: The selector of the element the step attaches to.
|
|
* - on {string}: a PopperJS compatible string to specify step position.
|
|
* - classes {string}: Will be added to the class attribute of the step.
|
|
* - body {string}: Markup that is mapped to the `text` Step() option. Will
|
|
* become the step content.
|
|
* - title {string}: is mapped to the `title` Step() option.
|
|
*
|
|
* @return {string}
|
|
* The tour item content markup.
|
|
*
|
|
* @see \Drupal\tour\TourViewBuilder::viewMultiple()
|
|
* @see https://shepherdjs.dev/docs/Step.html
|
|
*/
|
|
Drupal.theme.tourItemContent = (tourStepConfig) =>
|
|
`${tourStepConfig.body}<div class="tour-progress">${tourStepConfig.counter}</div>`;
|
|
})(jQuery, Backbone, Drupal, drupalSettings, document, window.Shepherd);
|