1 |
efrain |
1 |
// This file is part of Moodle - http://moodle.org/
|
|
|
2 |
//
|
|
|
3 |
// Moodle is free software: you can redistribute it and/or modify
|
|
|
4 |
// it under the terms of the GNU General Public License as published by
|
|
|
5 |
// the Free Software Foundation, either version 3 of the License, or
|
|
|
6 |
// (at your option) any later version.
|
|
|
7 |
//
|
|
|
8 |
// Moodle is distributed in the hope that it will be useful,
|
|
|
9 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
10 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
11 |
// GNU General Public License for more details.
|
|
|
12 |
//
|
|
|
13 |
// You should have received a copy of the GNU General Public License
|
|
|
14 |
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
15 |
|
|
|
16 |
/**
|
|
|
17 |
* A user tour.
|
|
|
18 |
*
|
|
|
19 |
* @module tool_usertours/tour
|
|
|
20 |
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
|
|
|
21 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
22 |
*/
|
|
|
23 |
|
|
|
24 |
/**
|
|
|
25 |
* A list of steps.
|
|
|
26 |
*
|
|
|
27 |
* @typedef {Object[]} StepList
|
|
|
28 |
* @property {Number} stepId The id of the step in the database
|
|
|
29 |
* @property {Number} position The position of the step within the tour (zero-indexed)
|
|
|
30 |
*/
|
|
|
31 |
|
|
|
32 |
import $ from 'jquery';
|
|
|
33 |
import * as Aria from 'core/aria';
|
|
|
34 |
import Popper from 'core/popper';
|
|
|
35 |
import {dispatchEvent} from 'core/event_dispatcher';
|
|
|
36 |
import {eventTypes} from './events';
|
|
|
37 |
import {getString} from 'core/str';
|
|
|
38 |
import {prefetchStrings} from 'core/prefetch';
|
|
|
39 |
import {notifyFilterContentUpdated} from 'core/event';
|
|
|
40 |
|
|
|
41 |
/**
|
|
|
42 |
* The minimum spacing for tour step to display.
|
|
|
43 |
*
|
|
|
44 |
* @private
|
|
|
45 |
* @constant
|
|
|
46 |
* @type {number}
|
|
|
47 |
*/
|
|
|
48 |
const MINSPACING = 10;
|
|
|
49 |
|
|
|
50 |
/**
|
|
|
51 |
* A user tour.
|
|
|
52 |
*
|
|
|
53 |
* @class tool_usertours/tour
|
|
|
54 |
* @property {boolean} tourRunning Whether the tour is currently running.
|
|
|
55 |
*/
|
|
|
56 |
const Tour = class {
|
|
|
57 |
tourRunning = false;
|
|
|
58 |
|
|
|
59 |
/**
|
|
|
60 |
* @param {object} config The configuration object.
|
|
|
61 |
*/
|
|
|
62 |
constructor(config) {
|
|
|
63 |
this.init(config);
|
|
|
64 |
}
|
|
|
65 |
|
|
|
66 |
/**
|
|
|
67 |
* Initialise the tour.
|
|
|
68 |
*
|
|
|
69 |
* @method init
|
|
|
70 |
* @param {Object} config The configuration object.
|
|
|
71 |
* @chainable
|
|
|
72 |
* @return {Object} this.
|
|
|
73 |
*/
|
|
|
74 |
init(config) {
|
|
|
75 |
// Unset all handlers.
|
|
|
76 |
this.eventHandlers = {};
|
|
|
77 |
|
|
|
78 |
// Reset the current tour states.
|
|
|
79 |
this.reset();
|
|
|
80 |
|
|
|
81 |
// Store the initial configuration.
|
|
|
82 |
this.originalConfiguration = config || {};
|
|
|
83 |
|
|
|
84 |
// Apply configuration.
|
|
|
85 |
this.configure.apply(this, arguments);
|
|
|
86 |
|
|
|
87 |
// Unset recalculate state.
|
|
|
88 |
this.possitionNeedToBeRecalculated = false;
|
|
|
89 |
|
|
|
90 |
// Unset recalculate count.
|
|
|
91 |
this.recalculatedNo = 0;
|
|
|
92 |
|
|
|
93 |
try {
|
|
|
94 |
this.storage = window.sessionStorage;
|
|
|
95 |
this.storageKey = 'tourstate_' + this.tourName;
|
|
|
96 |
} catch (e) {
|
|
|
97 |
this.storage = false;
|
|
|
98 |
this.storageKey = '';
|
|
|
99 |
}
|
|
|
100 |
|
|
|
101 |
prefetchStrings('tool_usertours', [
|
|
|
102 |
'nextstep_sequence',
|
|
|
103 |
'skip_tour'
|
|
|
104 |
]);
|
|
|
105 |
|
|
|
106 |
return this;
|
|
|
107 |
}
|
|
|
108 |
|
|
|
109 |
/**
|
|
|
110 |
* Reset the current tour state.
|
|
|
111 |
*
|
|
|
112 |
* @method reset
|
|
|
113 |
* @chainable
|
|
|
114 |
* @return {Object} this.
|
|
|
115 |
*/
|
|
|
116 |
reset() {
|
|
|
117 |
// Hide the current step.
|
|
|
118 |
this.hide();
|
|
|
119 |
|
|
|
120 |
// Unset all handlers.
|
|
|
121 |
this.eventHandlers = [];
|
|
|
122 |
|
|
|
123 |
// Unset all listeners.
|
|
|
124 |
this.resetStepListeners();
|
|
|
125 |
|
|
|
126 |
// Unset the original configuration.
|
|
|
127 |
this.originalConfiguration = {};
|
|
|
128 |
|
|
|
129 |
// Reset the current step number and list of steps.
|
|
|
130 |
this.steps = [];
|
|
|
131 |
|
|
|
132 |
// Reset the current step number.
|
|
|
133 |
this.currentStepNumber = 0;
|
|
|
134 |
|
|
|
135 |
return this;
|
|
|
136 |
}
|
|
|
137 |
|
|
|
138 |
/**
|
|
|
139 |
* Prepare tour configuration.
|
|
|
140 |
*
|
|
|
141 |
* @method configure
|
|
|
142 |
* @param {Object} config The configuration object.
|
|
|
143 |
* @chainable
|
|
|
144 |
* @return {Object} this.
|
|
|
145 |
*/
|
|
|
146 |
configure(config) {
|
|
|
147 |
if (typeof config === 'object') {
|
|
|
148 |
// Tour name.
|
|
|
149 |
if (typeof config.tourName !== 'undefined') {
|
|
|
150 |
this.tourName = config.tourName;
|
|
|
151 |
}
|
|
|
152 |
|
|
|
153 |
// Set up eventHandlers.
|
|
|
154 |
if (config.eventHandlers) {
|
|
|
155 |
for (let eventName in config.eventHandlers) {
|
|
|
156 |
config.eventHandlers[eventName].forEach(function(handler) {
|
|
|
157 |
this.addEventHandler(eventName, handler);
|
|
|
158 |
}, this);
|
|
|
159 |
}
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
// Reset the step configuration.
|
|
|
163 |
this.resetStepDefaults(true);
|
|
|
164 |
|
|
|
165 |
// Configure the steps.
|
|
|
166 |
if (typeof config.steps === 'object') {
|
|
|
167 |
this.steps = config.steps;
|
|
|
168 |
}
|
|
|
169 |
|
|
|
170 |
if (typeof config.template !== 'undefined') {
|
|
|
171 |
this.templateContent = config.template;
|
|
|
172 |
}
|
|
|
173 |
}
|
|
|
174 |
|
|
|
175 |
// Check that we have enough to start the tour.
|
|
|
176 |
this.checkMinimumRequirements();
|
|
|
177 |
|
|
|
178 |
return this;
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
/**
|
|
|
182 |
* Check that the configuration meets the minimum requirements.
|
|
|
183 |
*
|
|
|
184 |
* @method checkMinimumRequirements
|
|
|
185 |
*/
|
|
|
186 |
checkMinimumRequirements() {
|
|
|
187 |
// Need a tourName.
|
|
|
188 |
if (!this.tourName) {
|
|
|
189 |
throw new Error("Tour Name required");
|
|
|
190 |
}
|
|
|
191 |
|
|
|
192 |
// Need a minimum of one step.
|
|
|
193 |
if (!this.steps || !this.steps.length) {
|
|
|
194 |
throw new Error("Steps must be specified");
|
|
|
195 |
}
|
|
|
196 |
}
|
|
|
197 |
|
|
|
198 |
/**
|
|
|
199 |
* Reset step default configuration.
|
|
|
200 |
*
|
|
|
201 |
* @method resetStepDefaults
|
|
|
202 |
* @param {Boolean} loadOriginalConfiguration Whether to load the original configuration supplied with the Tour.
|
|
|
203 |
* @chainable
|
|
|
204 |
* @return {Object} this.
|
|
|
205 |
*/
|
|
|
206 |
resetStepDefaults(loadOriginalConfiguration) {
|
|
|
207 |
if (typeof loadOriginalConfiguration === 'undefined') {
|
|
|
208 |
loadOriginalConfiguration = true;
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
this.stepDefaults = {};
|
|
|
212 |
if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {
|
|
|
213 |
this.setStepDefaults({});
|
|
|
214 |
} else {
|
|
|
215 |
this.setStepDefaults(this.originalConfiguration.stepDefaults);
|
|
|
216 |
}
|
|
|
217 |
|
|
|
218 |
return this;
|
|
|
219 |
}
|
|
|
220 |
|
|
|
221 |
/**
|
|
|
222 |
* Set the step defaults.
|
|
|
223 |
*
|
|
|
224 |
* @method setStepDefaults
|
|
|
225 |
* @param {Object} stepDefaults The step defaults to apply to all steps
|
|
|
226 |
* @chainable
|
|
|
227 |
* @return {Object} this.
|
|
|
228 |
*/
|
|
|
229 |
setStepDefaults(stepDefaults) {
|
|
|
230 |
if (!this.stepDefaults) {
|
|
|
231 |
this.stepDefaults = {};
|
|
|
232 |
}
|
|
|
233 |
$.extend(
|
|
|
234 |
this.stepDefaults,
|
|
|
235 |
{
|
|
|
236 |
element: '',
|
|
|
237 |
placement: 'top',
|
|
|
238 |
delay: 0,
|
|
|
239 |
moveOnClick: false,
|
|
|
240 |
moveAfterTime: 0,
|
|
|
241 |
orphan: false,
|
|
|
242 |
direction: 1,
|
|
|
243 |
},
|
|
|
244 |
stepDefaults
|
|
|
245 |
);
|
|
|
246 |
|
|
|
247 |
return this;
|
|
|
248 |
}
|
|
|
249 |
|
|
|
250 |
/**
|
|
|
251 |
* Retrieve the current step number.
|
|
|
252 |
*
|
|
|
253 |
* @method getCurrentStepNumber
|
|
|
254 |
* @return {Number} The current step number
|
|
|
255 |
*/
|
|
|
256 |
getCurrentStepNumber() {
|
|
|
257 |
return parseInt(this.currentStepNumber, 10);
|
|
|
258 |
}
|
|
|
259 |
|
|
|
260 |
/**
|
|
|
261 |
* Store the current step number.
|
|
|
262 |
*
|
|
|
263 |
* @method setCurrentStepNumber
|
|
|
264 |
* @param {Number} stepNumber The current step number
|
|
|
265 |
* @chainable
|
|
|
266 |
*/
|
|
|
267 |
setCurrentStepNumber(stepNumber) {
|
|
|
268 |
this.currentStepNumber = stepNumber;
|
|
|
269 |
if (this.storage) {
|
|
|
270 |
try {
|
|
|
271 |
this.storage.setItem(this.storageKey, stepNumber);
|
|
|
272 |
} catch (e) {
|
|
|
273 |
if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
|
|
|
274 |
this.storage.removeItem(this.storageKey);
|
|
|
275 |
}
|
|
|
276 |
}
|
|
|
277 |
}
|
|
|
278 |
}
|
|
|
279 |
|
|
|
280 |
/**
|
|
|
281 |
* Get the next step number after the currently displayed step.
|
|
|
282 |
*
|
|
|
283 |
* @method getNextStepNumber
|
|
|
284 |
* @param {Number} stepNumber The current step number
|
|
|
285 |
* @return {Number} The next step number to display
|
|
|
286 |
*/
|
|
|
287 |
getNextStepNumber(stepNumber) {
|
|
|
288 |
if (typeof stepNumber === 'undefined') {
|
|
|
289 |
stepNumber = this.getCurrentStepNumber();
|
|
|
290 |
}
|
|
|
291 |
let nextStepNumber = stepNumber + 1;
|
|
|
292 |
|
|
|
293 |
// Keep checking the remaining steps.
|
|
|
294 |
while (nextStepNumber <= this.steps.length) {
|
|
|
295 |
if (this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber))) {
|
|
|
296 |
return nextStepNumber;
|
|
|
297 |
}
|
|
|
298 |
nextStepNumber++;
|
|
|
299 |
}
|
|
|
300 |
|
|
|
301 |
return null;
|
|
|
302 |
}
|
|
|
303 |
|
|
|
304 |
/**
|
|
|
305 |
* Get the previous step number before the currently displayed step.
|
|
|
306 |
*
|
|
|
307 |
* @method getPreviousStepNumber
|
|
|
308 |
* @param {Number} stepNumber The current step number
|
|
|
309 |
* @return {Number} The previous step number to display
|
|
|
310 |
*/
|
|
|
311 |
getPreviousStepNumber(stepNumber) {
|
|
|
312 |
if (typeof stepNumber === 'undefined') {
|
|
|
313 |
stepNumber = this.getCurrentStepNumber();
|
|
|
314 |
}
|
|
|
315 |
let previousStepNumber = stepNumber - 1;
|
|
|
316 |
|
|
|
317 |
// Keep checking the remaining steps.
|
|
|
318 |
while (previousStepNumber >= 0) {
|
|
|
319 |
if (this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber))) {
|
|
|
320 |
return previousStepNumber;
|
|
|
321 |
}
|
|
|
322 |
previousStepNumber--;
|
|
|
323 |
}
|
|
|
324 |
|
|
|
325 |
return null;
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
/**
|
|
|
329 |
* Is the step the final step number?
|
|
|
330 |
*
|
|
|
331 |
* @method isLastStep
|
|
|
332 |
* @param {Number} stepNumber Step number to test
|
|
|
333 |
* @return {Boolean} Whether the step is the final step
|
|
|
334 |
*/
|
|
|
335 |
isLastStep(stepNumber) {
|
|
|
336 |
let nextStepNumber = this.getNextStepNumber(stepNumber);
|
|
|
337 |
|
|
|
338 |
return nextStepNumber === null;
|
|
|
339 |
}
|
|
|
340 |
|
|
|
341 |
/**
|
|
|
342 |
* Is this step potentially visible?
|
|
|
343 |
*
|
|
|
344 |
* @method isStepPotentiallyVisible
|
|
|
345 |
* @param {Object} stepConfig The step configuration to normalise
|
|
|
346 |
* @return {Boolean} Whether the step is the potentially visible
|
|
|
347 |
*/
|
|
|
348 |
isStepPotentiallyVisible(stepConfig) {
|
|
|
349 |
if (!stepConfig) {
|
|
|
350 |
// Without step config, there can be no step.
|
|
|
351 |
return false;
|
|
|
352 |
}
|
|
|
353 |
|
|
|
354 |
if (this.isStepActuallyVisible(stepConfig)) {
|
|
|
355 |
// If it is actually visible, it is already potentially visible.
|
|
|
356 |
return true;
|
|
|
357 |
}
|
|
|
358 |
|
|
|
359 |
if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {
|
|
|
360 |
// Orphan steps have no target. They are always visible.
|
|
|
361 |
return true;
|
|
|
362 |
}
|
|
|
363 |
|
|
|
364 |
if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {
|
|
|
365 |
// Only return true if the activated has not been used yet.
|
|
|
366 |
return true;
|
|
|
367 |
}
|
|
|
368 |
|
|
|
369 |
// Not theoretically, or actually visible.
|
|
|
370 |
return false;
|
|
|
371 |
}
|
|
|
372 |
|
|
|
373 |
/**
|
|
|
374 |
* Get potentially visible steps in a tour.
|
|
|
375 |
*
|
|
|
376 |
* @returns {StepList} A list of ordered steps
|
|
|
377 |
*/
|
|
|
378 |
getPotentiallyVisibleSteps() {
|
|
|
379 |
let position = 1;
|
|
|
380 |
let result = [];
|
|
|
381 |
// Checking the total steps.
|
|
|
382 |
for (let stepNumber = 0; stepNumber < this.steps.length; stepNumber++) {
|
|
|
383 |
const stepConfig = this.getStepConfig(stepNumber);
|
|
|
384 |
if (this.isStepPotentiallyVisible(stepConfig)) {
|
|
|
385 |
result[stepNumber] = {stepId: stepConfig.stepid, position: position};
|
|
|
386 |
position++;
|
|
|
387 |
}
|
|
|
388 |
}
|
|
|
389 |
|
|
|
390 |
return result;
|
|
|
391 |
}
|
|
|
392 |
|
|
|
393 |
/**
|
|
|
394 |
* Is this step actually visible?
|
|
|
395 |
*
|
|
|
396 |
* @method isStepActuallyVisible
|
|
|
397 |
* @param {Object} stepConfig The step configuration to normalise
|
|
|
398 |
* @return {Boolean} Whether the step is actually visible
|
|
|
399 |
*/
|
|
|
400 |
isStepActuallyVisible(stepConfig) {
|
|
|
401 |
if (!stepConfig) {
|
|
|
402 |
// Without step config, there can be no step.
|
|
|
403 |
return false;
|
|
|
404 |
}
|
|
|
405 |
|
|
|
406 |
// Check if the CSS styles are allowed on the browser or not.
|
|
|
407 |
if (!this.isCSSAllowed()) {
|
|
|
408 |
return false;
|
|
|
409 |
}
|
|
|
410 |
|
|
|
411 |
let target = this.getStepTarget(stepConfig);
|
|
|
412 |
if (target && target.length && target.is(':visible')) {
|
|
|
413 |
// Without a target, there can be no step.
|
|
|
414 |
return !!target.length;
|
|
|
415 |
}
|
|
|
416 |
|
|
|
417 |
return false;
|
|
|
418 |
}
|
|
|
419 |
|
|
|
420 |
/**
|
|
|
421 |
* Is the browser actually allow CSS styles?
|
|
|
422 |
*
|
|
|
423 |
* @returns {boolean} True if the browser is allowing CSS styles
|
|
|
424 |
*/
|
|
|
425 |
isCSSAllowed() {
|
|
|
426 |
const testCSSElement = document.createElement('div');
|
|
|
427 |
testCSSElement.classList.add('hide');
|
|
|
428 |
document.body.appendChild(testCSSElement);
|
|
|
429 |
const styles = window.getComputedStyle(testCSSElement);
|
|
|
430 |
const isAllowed = styles.display === 'none';
|
|
|
431 |
testCSSElement.remove();
|
|
|
432 |
|
|
|
433 |
return isAllowed;
|
|
|
434 |
}
|
|
|
435 |
|
|
|
436 |
/**
|
|
|
437 |
* Go to the next step in the tour.
|
|
|
438 |
*
|
|
|
439 |
* @method next
|
|
|
440 |
* @chainable
|
|
|
441 |
* @return {Object} this.
|
|
|
442 |
*/
|
|
|
443 |
next() {
|
|
|
444 |
return this.gotoStep(this.getNextStepNumber());
|
|
|
445 |
}
|
|
|
446 |
|
|
|
447 |
/**
|
|
|
448 |
* Go to the previous step in the tour.
|
|
|
449 |
*
|
|
|
450 |
* @method previous
|
|
|
451 |
* @chainable
|
|
|
452 |
* @return {Object} this.
|
|
|
453 |
*/
|
|
|
454 |
previous() {
|
|
|
455 |
return this.gotoStep(this.getPreviousStepNumber(), -1);
|
|
|
456 |
}
|
|
|
457 |
|
|
|
458 |
/**
|
|
|
459 |
* Go to the specified step in the tour.
|
|
|
460 |
*
|
|
|
461 |
* @method gotoStep
|
|
|
462 |
* @param {Number} stepNumber The step number to display
|
|
|
463 |
* @param {Number} direction Next or previous step
|
|
|
464 |
* @chainable
|
|
|
465 |
* @return {Object} this.
|
|
|
466 |
* @fires tool_usertours/stepRender
|
|
|
467 |
* @fires tool_usertours/stepRendered
|
|
|
468 |
* @fires tool_usertours/stepHide
|
|
|
469 |
* @fires tool_usertours/stepHidden
|
|
|
470 |
*/
|
|
|
471 |
gotoStep(stepNumber, direction) {
|
|
|
472 |
if (stepNumber < 0) {
|
|
|
473 |
return this.endTour();
|
|
|
474 |
}
|
|
|
475 |
|
|
|
476 |
let stepConfig = this.getStepConfig(stepNumber);
|
|
|
477 |
if (stepConfig === null) {
|
|
|
478 |
return this.endTour();
|
|
|
479 |
}
|
|
|
480 |
|
|
|
481 |
return this._gotoStep(stepConfig, direction);
|
|
|
482 |
}
|
|
|
483 |
|
|
|
484 |
_gotoStep(stepConfig, direction) {
|
|
|
485 |
if (!stepConfig) {
|
|
|
486 |
return this.endTour();
|
|
|
487 |
}
|
|
|
488 |
|
|
|
489 |
if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {
|
|
|
490 |
stepConfig.delayed = true;
|
|
|
491 |
window.setTimeout(this._gotoStep.bind(this), stepConfig.delay, stepConfig, direction);
|
|
|
492 |
|
|
|
493 |
return this;
|
|
|
494 |
} else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {
|
|
|
495 |
let fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
|
|
|
496 |
return this.gotoStep(this[fn](stepConfig.stepNumber), direction);
|
|
|
497 |
}
|
|
|
498 |
|
|
|
499 |
this.hide();
|
|
|
500 |
|
|
|
501 |
const stepRenderEvent = this.dispatchEvent(eventTypes.stepRender, {stepConfig}, true);
|
|
|
502 |
if (!stepRenderEvent.defaultPrevented) {
|
|
|
503 |
this.renderStep(stepConfig);
|
|
|
504 |
this.dispatchEvent(eventTypes.stepRendered, {stepConfig});
|
|
|
505 |
}
|
|
|
506 |
|
|
|
507 |
return this;
|
|
|
508 |
}
|
|
|
509 |
|
|
|
510 |
/**
|
|
|
511 |
* Fetch the normalised step configuration for the specified step number.
|
|
|
512 |
*
|
|
|
513 |
* @method getStepConfig
|
|
|
514 |
* @param {Number} stepNumber The step number to fetch configuration for
|
|
|
515 |
* @return {Object} The step configuration
|
|
|
516 |
*/
|
|
|
517 |
getStepConfig(stepNumber) {
|
|
|
518 |
if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {
|
|
|
519 |
return null;
|
|
|
520 |
}
|
|
|
521 |
|
|
|
522 |
// Normalise the step configuration.
|
|
|
523 |
let stepConfig = this.normalizeStepConfig(this.steps[stepNumber]);
|
|
|
524 |
|
|
|
525 |
// Add the stepNumber to the stepConfig.
|
|
|
526 |
stepConfig = $.extend(stepConfig, {stepNumber: stepNumber});
|
|
|
527 |
|
|
|
528 |
return stepConfig;
|
|
|
529 |
}
|
|
|
530 |
|
|
|
531 |
/**
|
|
|
532 |
* Normalise the supplied step configuration.
|
|
|
533 |
*
|
|
|
534 |
* @method normalizeStepConfig
|
|
|
535 |
* @param {Object} stepConfig The step configuration to normalise
|
|
|
536 |
* @return {Object} The normalised step configuration
|
|
|
537 |
*/
|
|
|
538 |
normalizeStepConfig(stepConfig) {
|
|
|
539 |
|
|
|
540 |
if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {
|
|
|
541 |
stepConfig.moveAfterClick = stepConfig.reflex;
|
|
|
542 |
}
|
|
|
543 |
|
|
|
544 |
if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {
|
|
|
545 |
stepConfig.target = stepConfig.element;
|
|
|
546 |
}
|
|
|
547 |
|
|
|
548 |
if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {
|
|
|
549 |
stepConfig.body = stepConfig.content;
|
|
|
550 |
}
|
|
|
551 |
|
|
|
552 |
stepConfig = $.extend({}, this.stepDefaults, stepConfig);
|
|
|
553 |
|
|
|
554 |
stepConfig = $.extend({}, {
|
|
|
555 |
attachTo: stepConfig.target,
|
|
|
556 |
attachPoint: 'after',
|
|
|
557 |
}, stepConfig);
|
|
|
558 |
|
|
|
559 |
if (stepConfig.attachTo) {
|
|
|
560 |
stepConfig.attachTo = $(stepConfig.attachTo).first();
|
|
|
561 |
}
|
|
|
562 |
|
|
|
563 |
return stepConfig;
|
|
|
564 |
}
|
|
|
565 |
|
|
|
566 |
/**
|
|
|
567 |
* Fetch the actual step target from the selector.
|
|
|
568 |
*
|
|
|
569 |
* This should not be called until after any delay has completed.
|
|
|
570 |
*
|
|
|
571 |
* @method getStepTarget
|
|
|
572 |
* @param {Object} stepConfig The step configuration
|
|
|
573 |
* @return {$}
|
|
|
574 |
*/
|
|
|
575 |
getStepTarget(stepConfig) {
|
|
|
576 |
if (stepConfig.target) {
|
|
|
577 |
return $(stepConfig.target);
|
|
|
578 |
}
|
|
|
579 |
|
|
|
580 |
return null;
|
|
|
581 |
}
|
|
|
582 |
|
|
|
583 |
/**
|
|
|
584 |
* Fire any event handlers for the specified event.
|
|
|
585 |
*
|
|
|
586 |
* @param {String} eventName The name of the event
|
|
|
587 |
* @param {Object} [detail={}] Any additional details to pass into the eveent
|
|
|
588 |
* @param {Boolean} [cancelable=false] Whether preventDefault() can be called
|
|
|
589 |
* @returns {CustomEvent}
|
|
|
590 |
*/
|
|
|
591 |
dispatchEvent(
|
|
|
592 |
eventName,
|
|
|
593 |
detail = {},
|
|
|
594 |
cancelable = false
|
|
|
595 |
) {
|
|
|
596 |
return dispatchEvent(eventName, {
|
|
|
597 |
// Add the tour to the detail.
|
|
|
598 |
tour: this,
|
|
|
599 |
...detail,
|
|
|
600 |
}, document, {
|
|
|
601 |
cancelable,
|
|
|
602 |
});
|
|
|
603 |
}
|
|
|
604 |
|
|
|
605 |
/**
|
|
|
606 |
* @method addEventHandler
|
|
|
607 |
* @param {string} eventName The name of the event to listen for
|
|
|
608 |
* @param {function} handler The event handler to call
|
|
|
609 |
* @return {Object} this.
|
|
|
610 |
*/
|
|
|
611 |
addEventHandler(eventName, handler) {
|
|
|
612 |
if (typeof this.eventHandlers[eventName] === 'undefined') {
|
|
|
613 |
this.eventHandlers[eventName] = [];
|
|
|
614 |
}
|
|
|
615 |
|
|
|
616 |
this.eventHandlers[eventName].push(handler);
|
|
|
617 |
|
|
|
618 |
return this;
|
|
|
619 |
}
|
|
|
620 |
|
|
|
621 |
/**
|
|
|
622 |
* Process listeners for the step being shown.
|
|
|
623 |
*
|
|
|
624 |
* @method processStepListeners
|
|
|
625 |
* @param {object} stepConfig The configuration for the step
|
|
|
626 |
* @chainable
|
|
|
627 |
* @return {Object} this.
|
|
|
628 |
*/
|
|
|
629 |
processStepListeners(stepConfig) {
|
|
|
630 |
this.listeners.push(
|
|
|
631 |
// Next button.
|
|
|
632 |
{
|
|
|
633 |
node: this.currentStepNode,
|
|
|
634 |
args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
|
|
|
635 |
},
|
|
|
636 |
|
|
|
637 |
// Close and end tour buttons.
|
|
|
638 |
{
|
|
|
639 |
node: this.currentStepNode,
|
|
|
640 |
args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
|
|
|
641 |
},
|
|
|
642 |
|
|
|
643 |
// Click backdrop and hide tour.
|
|
|
644 |
{
|
|
|
645 |
node: $('[data-flexitour="backdrop"]'),
|
|
|
646 |
args: ['click', $.proxy(this.hide, this)]
|
|
|
647 |
},
|
|
|
648 |
|
|
|
649 |
// Keypresses.
|
|
|
650 |
{
|
|
|
651 |
node: $('body'),
|
|
|
652 |
args: ['keydown', $.proxy(this.handleKeyDown, this)]
|
|
|
653 |
});
|
|
|
654 |
|
|
|
655 |
if (stepConfig.moveOnClick) {
|
|
|
656 |
var targetNode = this.getStepTarget(stepConfig);
|
|
|
657 |
this.listeners.push({
|
|
|
658 |
node: targetNode,
|
|
|
659 |
args: ['click', $.proxy(function(e) {
|
|
|
660 |
if ($(e.target).parents('[data-flexitour="container"]').length === 0) {
|
|
|
661 |
// Ignore clicks when they are in the flexitour.
|
|
|
662 |
window.setTimeout($.proxy(this.next, this), 500);
|
|
|
663 |
}
|
|
|
664 |
}, this)]
|
|
|
665 |
});
|
|
|
666 |
}
|
|
|
667 |
|
|
|
668 |
this.listeners.forEach(function(listener) {
|
|
|
669 |
listener.node.on.apply(listener.node, listener.args);
|
|
|
670 |
});
|
|
|
671 |
|
|
|
672 |
return this;
|
|
|
673 |
}
|
|
|
674 |
|
|
|
675 |
/**
|
|
|
676 |
* Reset step listeners.
|
|
|
677 |
*
|
|
|
678 |
* @method resetStepListeners
|
|
|
679 |
* @chainable
|
|
|
680 |
* @return {Object} this.
|
|
|
681 |
*/
|
|
|
682 |
resetStepListeners() {
|
|
|
683 |
// Stop listening to all external handlers.
|
|
|
684 |
if (this.listeners) {
|
|
|
685 |
this.listeners.forEach(function(listener) {
|
|
|
686 |
listener.node.off.apply(listener.node, listener.args);
|
|
|
687 |
});
|
|
|
688 |
}
|
|
|
689 |
this.listeners = [];
|
|
|
690 |
|
|
|
691 |
return this;
|
|
|
692 |
}
|
|
|
693 |
|
|
|
694 |
/**
|
|
|
695 |
* The standard step renderer.
|
|
|
696 |
*
|
|
|
697 |
* @method renderStep
|
|
|
698 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
699 |
* @chainable
|
|
|
700 |
* @return {Object} this.
|
|
|
701 |
*/
|
|
|
702 |
renderStep(stepConfig) {
|
|
|
703 |
// Store the current step configuration for later.
|
|
|
704 |
this.currentStepConfig = stepConfig;
|
|
|
705 |
this.setCurrentStepNumber(stepConfig.stepNumber);
|
|
|
706 |
|
|
|
707 |
// Fetch the template and convert it to a $ object.
|
|
|
708 |
let template = $(this.getTemplateContent());
|
|
|
709 |
|
|
|
710 |
// Title.
|
|
|
711 |
template.find('[data-placeholder="title"]')
|
|
|
712 |
.html(stepConfig.title);
|
|
|
713 |
|
|
|
714 |
// Body.
|
|
|
715 |
template.find('[data-placeholder="body"]')
|
|
|
716 |
.html(stepConfig.body);
|
|
|
717 |
|
|
|
718 |
// Buttons.
|
|
|
719 |
const nextBtn = template.find('[data-role="next"]');
|
|
|
720 |
const endBtn = template.find('[data-role="end"]');
|
|
|
721 |
|
|
|
722 |
// Is this the final step?
|
|
|
723 |
if (this.isLastStep(stepConfig.stepNumber)) {
|
|
|
724 |
nextBtn.hide();
|
|
|
725 |
endBtn.removeClass("btn-secondary").addClass("btn-primary");
|
|
|
726 |
} else {
|
|
|
727 |
nextBtn.prop('disabled', false);
|
|
|
728 |
// Use Skip tour label for the End tour button.
|
|
|
729 |
getString('skip_tour', 'tool_usertours').then(value => {
|
|
|
730 |
endBtn.html(value);
|
|
|
731 |
return;
|
|
|
732 |
}).catch();
|
|
|
733 |
}
|
|
|
734 |
|
|
|
735 |
nextBtn.attr('role', 'button');
|
|
|
736 |
endBtn.attr('role', 'button');
|
|
|
737 |
|
|
|
738 |
if (this.originalConfiguration.displaystepnumbers) {
|
|
|
739 |
const stepsPotentiallyVisible = this.getPotentiallyVisibleSteps();
|
|
|
740 |
const totalStepsPotentiallyVisible = stepsPotentiallyVisible.length;
|
|
|
741 |
const position = stepsPotentiallyVisible[stepConfig.stepNumber].position;
|
|
|
742 |
if (totalStepsPotentiallyVisible > 1) {
|
|
|
743 |
// Change the label of the Next button to include the sequence.
|
|
|
744 |
getString('nextstep_sequence', 'tool_usertours',
|
|
|
745 |
{position: position, total: totalStepsPotentiallyVisible}).then(value => {
|
|
|
746 |
nextBtn.html(value);
|
|
|
747 |
return;
|
|
|
748 |
}).catch();
|
|
|
749 |
}
|
|
|
750 |
}
|
|
|
751 |
|
|
|
752 |
// Replace the template with the updated version.
|
|
|
753 |
stepConfig.template = template;
|
|
|
754 |
|
|
|
755 |
// Add to the page.
|
|
|
756 |
this.addStepToPage(stepConfig);
|
|
|
757 |
|
|
|
758 |
// Process step listeners after adding to the page.
|
|
|
759 |
// This uses the currentNode.
|
|
|
760 |
this.processStepListeners(stepConfig);
|
|
|
761 |
|
|
|
762 |
return this;
|
|
|
763 |
}
|
|
|
764 |
|
|
|
765 |
/**
|
|
|
766 |
* Getter for the template content.
|
|
|
767 |
*
|
|
|
768 |
* @method getTemplateContent
|
|
|
769 |
* @return {$}
|
|
|
770 |
*/
|
|
|
771 |
getTemplateContent() {
|
|
|
772 |
return $(this.templateContent).clone();
|
|
|
773 |
}
|
|
|
774 |
|
|
|
775 |
/**
|
|
|
776 |
* Helper to add a step to the page.
|
|
|
777 |
*
|
|
|
778 |
* @method addStepToPage
|
|
|
779 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
780 |
* @chainable
|
|
|
781 |
* @return {Object} this.
|
|
|
782 |
*/
|
|
|
783 |
addStepToPage(stepConfig) {
|
|
|
784 |
// Create the stepNode from the template data.
|
|
|
785 |
let currentStepNode = $('<span data-flexitour="container"></span>')
|
|
|
786 |
.html(stepConfig.template)
|
|
|
787 |
.hide();
|
|
|
788 |
// Trigger the Moodle filters.
|
|
|
789 |
notifyFilterContentUpdated(currentStepNode);
|
|
|
790 |
|
|
|
791 |
// The scroll animation occurs on the body or html.
|
|
|
792 |
let animationTarget = $('body, html')
|
|
|
793 |
.stop(true, true);
|
|
|
794 |
|
|
|
795 |
if (this.isStepActuallyVisible(stepConfig)) {
|
|
|
796 |
let targetNode = this.getStepTarget(stepConfig);
|
|
|
797 |
|
|
|
798 |
if (targetNode.parents('[data-usertour="scroller"]').length) {
|
|
|
799 |
animationTarget = targetNode.parents('[data-usertour="scroller"]');
|
|
|
800 |
}
|
|
|
801 |
|
|
|
802 |
targetNode.data('flexitour', 'target');
|
|
|
803 |
|
|
|
804 |
let zIndex = this.calculateZIndex(targetNode);
|
|
|
805 |
if (zIndex) {
|
|
|
806 |
stepConfig.zIndex = zIndex + 1;
|
|
|
807 |
}
|
|
|
808 |
|
|
|
809 |
if (stepConfig.zIndex) {
|
|
|
810 |
currentStepNode.css('zIndex', stepConfig.zIndex + 1);
|
|
|
811 |
}
|
|
|
812 |
|
|
|
813 |
// Add the backdrop.
|
|
|
814 |
this.positionBackdrop(stepConfig);
|
|
|
815 |
|
|
|
816 |
$(document.body).append(currentStepNode);
|
|
|
817 |
this.currentStepNode = currentStepNode;
|
|
|
818 |
|
|
|
819 |
// Ensure that the step node is positioned.
|
|
|
820 |
// Some situations mean that the value is not properly calculated without this step.
|
|
|
821 |
this.currentStepNode.css({
|
|
|
822 |
top: 0,
|
|
|
823 |
left: 0,
|
|
|
824 |
});
|
|
|
825 |
|
|
|
826 |
animationTarget
|
|
|
827 |
.animate({
|
|
|
828 |
scrollTop: this.calculateScrollTop(stepConfig),
|
|
|
829 |
}).promise().then(function() {
|
|
|
830 |
this.positionStep(stepConfig);
|
|
|
831 |
this.revealStep(stepConfig);
|
|
|
832 |
return;
|
|
|
833 |
}.bind(this))
|
|
|
834 |
.catch(function() {
|
|
|
835 |
// Silently fail.
|
|
|
836 |
});
|
|
|
837 |
|
|
|
838 |
} else if (stepConfig.orphan) {
|
|
|
839 |
stepConfig.isOrphan = true;
|
|
|
840 |
|
|
|
841 |
// This will be appended to the body instead.
|
|
|
842 |
stepConfig.attachTo = $('body').first();
|
|
|
843 |
stepConfig.attachPoint = 'append';
|
|
|
844 |
|
|
|
845 |
// Add the backdrop.
|
|
|
846 |
this.positionBackdrop(stepConfig);
|
|
|
847 |
|
|
|
848 |
// This is an orphaned step.
|
|
|
849 |
currentStepNode.addClass('orphan');
|
|
|
850 |
|
|
|
851 |
// It lives in the body.
|
|
|
852 |
$(document.body).append(currentStepNode);
|
|
|
853 |
this.currentStepNode = currentStepNode;
|
|
|
854 |
|
|
|
855 |
this.currentStepNode.css('position', 'fixed');
|
|
|
856 |
|
|
|
857 |
this.currentStepPopper = new Popper(
|
|
|
858 |
$('body'),
|
|
|
859 |
this.currentStepNode[0], {
|
|
|
860 |
removeOnDestroy: true,
|
|
|
861 |
placement: stepConfig.placement + '-start',
|
|
|
862 |
arrowElement: '[data-role="arrow"]',
|
|
|
863 |
// Empty the modifiers. We've already placed the step and don't want it moved.
|
|
|
864 |
modifiers: {
|
|
|
865 |
hide: {
|
|
|
866 |
enabled: false,
|
|
|
867 |
},
|
|
|
868 |
applyStyle: {
|
|
|
869 |
onLoad: null,
|
|
|
870 |
enabled: false,
|
|
|
871 |
},
|
|
|
872 |
},
|
|
|
873 |
onCreate: () => {
|
|
|
874 |
// First, we need to check if the step's content contains any images.
|
|
|
875 |
const images = this.currentStepNode.find('img');
|
|
|
876 |
if (images.length) {
|
|
|
877 |
// Images found, need to calculate the position when the image is loaded.
|
|
|
878 |
images.on('load', () => {
|
|
|
879 |
this.calculateStepPositionInPage(currentStepNode);
|
|
|
880 |
});
|
|
|
881 |
}
|
|
|
882 |
this.calculateStepPositionInPage(currentStepNode);
|
|
|
883 |
}
|
|
|
884 |
}
|
|
|
885 |
);
|
|
|
886 |
|
|
|
887 |
this.revealStep(stepConfig);
|
|
|
888 |
}
|
|
|
889 |
|
|
|
890 |
return this;
|
|
|
891 |
}
|
|
|
892 |
|
|
|
893 |
/**
|
|
|
894 |
* Make the given step visible.
|
|
|
895 |
*
|
|
|
896 |
* @method revealStep
|
|
|
897 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
898 |
* @chainable
|
|
|
899 |
* @return {Object} this.
|
|
|
900 |
*/
|
|
|
901 |
revealStep(stepConfig) {
|
|
|
902 |
// Fade the step in.
|
|
|
903 |
this.currentStepNode.fadeIn('', $.proxy(function() {
|
|
|
904 |
// Announce via ARIA.
|
|
|
905 |
this.announceStep(stepConfig);
|
|
|
906 |
|
|
|
907 |
// Focus on the current step Node.
|
|
|
908 |
this.currentStepNode.focus();
|
|
|
909 |
window.setTimeout($.proxy(function() {
|
|
|
910 |
// After a brief delay, focus again.
|
|
|
911 |
// There seems to be an issue with Jaws where it only reads the dialogue title initially.
|
|
|
912 |
// This second focus helps it to read the full dialogue.
|
|
|
913 |
if (this.currentStepNode) {
|
|
|
914 |
this.currentStepNode.focus();
|
|
|
915 |
}
|
|
|
916 |
}, this), 100);
|
|
|
917 |
|
|
|
918 |
}, this));
|
|
|
919 |
|
|
|
920 |
return this;
|
|
|
921 |
}
|
|
|
922 |
|
|
|
923 |
/**
|
|
|
924 |
* Helper to announce the step on the page.
|
|
|
925 |
*
|
|
|
926 |
* @method announceStep
|
|
|
927 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
928 |
* @chainable
|
|
|
929 |
* @return {Object} this.
|
|
|
930 |
*/
|
|
|
931 |
announceStep(stepConfig) {
|
|
|
932 |
// Setup the step Dialogue as per:
|
|
|
933 |
// * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal
|
|
|
934 |
// * https://www.w3.org/TR/wai-aria-practices/#dialog_modal
|
|
|
935 |
|
|
|
936 |
// Generate an ID for the current step node.
|
|
|
937 |
let stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;
|
|
|
938 |
this.currentStepNode.attr('id', stepId);
|
|
|
939 |
|
|
|
940 |
let bodyRegion = this.currentStepNode.find('[data-placeholder="body"]').first();
|
|
|
941 |
bodyRegion.attr('id', stepId + '-body');
|
|
|
942 |
bodyRegion.attr('role', 'document');
|
|
|
943 |
|
|
|
944 |
let headerRegion = this.currentStepNode.find('[data-placeholder="title"]').first();
|
|
|
945 |
headerRegion.attr('id', stepId + '-title');
|
|
|
946 |
headerRegion.attr('aria-labelledby', stepId + '-body');
|
|
|
947 |
|
|
|
948 |
// Generally, a modal dialog has a role of dialog.
|
|
|
949 |
this.currentStepNode.attr('role', 'dialog');
|
|
|
950 |
this.currentStepNode.attr('tabindex', 0);
|
|
|
951 |
this.currentStepNode.attr('aria-labelledby', stepId + '-title');
|
|
|
952 |
this.currentStepNode.attr('aria-describedby', stepId + '-body');
|
|
|
953 |
|
|
|
954 |
// Configure ARIA attributes on the target.
|
|
|
955 |
let target = this.getStepTarget(stepConfig);
|
|
|
956 |
if (target) {
|
|
|
957 |
target.data('original-tabindex', target.attr('tabindex'));
|
|
|
958 |
if (!target.attr('tabindex')) {
|
|
|
959 |
target.attr('tabindex', 0);
|
|
|
960 |
}
|
|
|
961 |
|
|
|
962 |
target
|
|
|
963 |
.data('original-describedby', target.attr('aria-describedby'))
|
|
|
964 |
.attr('aria-describedby', stepId + '-body')
|
|
|
965 |
;
|
|
|
966 |
}
|
|
|
967 |
|
|
|
968 |
this.accessibilityShow(stepConfig);
|
|
|
969 |
|
|
|
970 |
return this;
|
|
|
971 |
}
|
|
|
972 |
|
|
|
973 |
/**
|
|
|
974 |
* Handle key down events.
|
|
|
975 |
*
|
|
|
976 |
* @method handleKeyDown
|
|
|
977 |
* @param {EventFacade} e
|
|
|
978 |
*/
|
|
|
979 |
handleKeyDown(e) {
|
|
|
980 |
let tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], ';
|
|
|
981 |
tabbableSelector += ':input:enabled, [tabindex], button:enabled';
|
|
|
982 |
switch (e.keyCode) {
|
|
|
983 |
case 27:
|
|
|
984 |
this.endTour();
|
|
|
985 |
break;
|
|
|
986 |
|
|
|
987 |
// 9 == Tab - trap focus for items with a backdrop.
|
|
|
988 |
case 9:
|
|
|
989 |
// Tab must be handled on key up only in this instance.
|
|
|
990 |
(function() {
|
|
|
991 |
if (!this.currentStepConfig.hasBackdrop) {
|
|
|
992 |
// Trapping tab focus is only handled for those steps with a backdrop.
|
|
|
993 |
return;
|
|
|
994 |
}
|
|
|
995 |
|
|
|
996 |
// Find all tabbable locations.
|
|
|
997 |
let activeElement = $(document.activeElement);
|
|
|
998 |
let stepTarget = this.getStepTarget(this.currentStepConfig);
|
|
|
999 |
let tabbableNodes = $(tabbableSelector);
|
|
|
1000 |
let dialogContainer = $('span[data-flexitour="container"]');
|
|
|
1001 |
let currentIndex;
|
|
|
1002 |
// Filter out element which is not belong to target section or dialogue.
|
|
|
1003 |
if (stepTarget) {
|
|
|
1004 |
tabbableNodes = tabbableNodes.filter(function(index, element) {
|
|
|
1005 |
return stepTarget !== null
|
|
|
1006 |
&& (stepTarget.has(element).length
|
|
|
1007 |
|| dialogContainer.has(element).length
|
|
|
1008 |
|| stepTarget.is(element)
|
|
|
1009 |
|| dialogContainer.is(element));
|
|
|
1010 |
});
|
|
|
1011 |
}
|
|
|
1012 |
|
|
|
1013 |
// Find index of focusing element.
|
|
|
1014 |
tabbableNodes.each(function(index, element) {
|
|
|
1015 |
if (activeElement.is(element)) {
|
|
|
1016 |
currentIndex = index;
|
|
|
1017 |
return false;
|
|
|
1018 |
}
|
|
|
1019 |
// Keep looping.
|
|
|
1020 |
return true;
|
|
|
1021 |
});
|
|
|
1022 |
|
|
|
1023 |
let nextIndex;
|
|
|
1024 |
let nextNode;
|
|
|
1025 |
let focusRelevant;
|
|
|
1026 |
if (currentIndex != void 0) {
|
|
|
1027 |
let direction = 1;
|
|
|
1028 |
if (e.shiftKey) {
|
|
|
1029 |
direction = -1;
|
|
|
1030 |
}
|
|
|
1031 |
nextIndex = currentIndex;
|
|
|
1032 |
do {
|
|
|
1033 |
nextIndex += direction;
|
|
|
1034 |
nextNode = $(tabbableNodes[nextIndex]);
|
|
|
1035 |
} while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));
|
|
|
1036 |
if (nextNode.length) {
|
|
|
1037 |
// A new f
|
|
|
1038 |
focusRelevant = nextNode.closest(stepTarget).length;
|
|
|
1039 |
focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;
|
|
|
1040 |
} else {
|
|
|
1041 |
// Unable to find the target somehow.
|
|
|
1042 |
focusRelevant = false;
|
|
|
1043 |
}
|
|
|
1044 |
}
|
|
|
1045 |
|
|
|
1046 |
if (focusRelevant) {
|
|
|
1047 |
nextNode.focus();
|
|
|
1048 |
} else {
|
|
|
1049 |
if (e.shiftKey) {
|
|
|
1050 |
// Focus on the last tabbable node in the step.
|
|
|
1051 |
this.currentStepNode.find(tabbableSelector).last().focus();
|
|
|
1052 |
} else {
|
|
|
1053 |
if (this.currentStepConfig.isOrphan) {
|
|
|
1054 |
// Focus on the step - there is no target.
|
|
|
1055 |
this.currentStepNode.focus();
|
|
|
1056 |
} else {
|
|
|
1057 |
// Focus on the step target.
|
|
|
1058 |
stepTarget.focus();
|
|
|
1059 |
}
|
|
|
1060 |
}
|
|
|
1061 |
}
|
|
|
1062 |
e.preventDefault();
|
|
|
1063 |
}).call(this);
|
|
|
1064 |
break;
|
|
|
1065 |
}
|
|
|
1066 |
}
|
|
|
1067 |
|
|
|
1068 |
/**
|
|
|
1069 |
* Start the current tour.
|
|
|
1070 |
*
|
|
|
1071 |
* @method startTour
|
|
|
1072 |
* @param {Number} startAt Which step number to start at. If not specified, starts at the last point.
|
|
|
1073 |
* @chainable
|
|
|
1074 |
* @return {Object} this.
|
|
|
1075 |
* @fires tool_usertours/tourStart
|
|
|
1076 |
* @fires tool_usertours/tourStarted
|
|
|
1077 |
*/
|
|
|
1078 |
startTour(startAt) {
|
|
|
1079 |
if (this.storage && typeof startAt === 'undefined') {
|
|
|
1080 |
let storageStartValue = this.storage.getItem(this.storageKey);
|
|
|
1081 |
if (storageStartValue) {
|
|
|
1082 |
let storageStartAt = parseInt(storageStartValue, 10);
|
|
|
1083 |
if (storageStartAt <= this.steps.length) {
|
|
|
1084 |
startAt = storageStartAt;
|
|
|
1085 |
}
|
|
|
1086 |
}
|
|
|
1087 |
}
|
|
|
1088 |
|
|
|
1089 |
if (typeof startAt === 'undefined') {
|
|
|
1090 |
startAt = this.getCurrentStepNumber();
|
|
|
1091 |
}
|
|
|
1092 |
|
|
|
1093 |
const tourStartEvent = this.dispatchEvent(eventTypes.tourStart, {startAt}, true);
|
|
|
1094 |
if (!tourStartEvent.defaultPrevented) {
|
|
|
1095 |
this.gotoStep(startAt);
|
|
|
1096 |
this.tourRunning = true;
|
|
|
1097 |
this.dispatchEvent(eventTypes.tourStarted, {startAt});
|
|
|
1098 |
}
|
|
|
1099 |
|
|
|
1100 |
return this;
|
|
|
1101 |
}
|
|
|
1102 |
|
|
|
1103 |
/**
|
|
|
1104 |
* Restart the tour from the beginning, resetting the completionlag.
|
|
|
1105 |
*
|
|
|
1106 |
* @method restartTour
|
|
|
1107 |
* @chainable
|
|
|
1108 |
* @return {Object} this.
|
|
|
1109 |
*/
|
|
|
1110 |
restartTour() {
|
|
|
1111 |
return this.startTour(0);
|
|
|
1112 |
}
|
|
|
1113 |
|
|
|
1114 |
/**
|
|
|
1115 |
* End the current tour.
|
|
|
1116 |
*
|
|
|
1117 |
* @method endTour
|
|
|
1118 |
* @chainable
|
|
|
1119 |
* @return {Object} this.
|
|
|
1120 |
* @fires tool_usertours/tourEnd
|
|
|
1121 |
* @fires tool_usertours/tourEnded
|
|
|
1122 |
*/
|
|
|
1123 |
endTour() {
|
|
|
1124 |
const tourEndEvent = this.dispatchEvent(eventTypes.tourEnd, {}, true);
|
|
|
1125 |
if (tourEndEvent.defaultPrevented) {
|
|
|
1126 |
return this;
|
|
|
1127 |
}
|
|
|
1128 |
|
|
|
1129 |
if (this.currentStepConfig) {
|
|
|
1130 |
let previousTarget = this.getStepTarget(this.currentStepConfig);
|
|
|
1131 |
if (previousTarget) {
|
|
|
1132 |
if (!previousTarget.attr('tabindex')) {
|
|
|
1133 |
previousTarget.attr('tabindex', '-1');
|
|
|
1134 |
}
|
|
|
1135 |
previousTarget.first().focus();
|
|
|
1136 |
}
|
|
|
1137 |
}
|
|
|
1138 |
|
|
|
1139 |
this.hide(true);
|
|
|
1140 |
|
|
|
1141 |
this.tourRunning = false;
|
|
|
1142 |
this.dispatchEvent(eventTypes.tourEnded);
|
|
|
1143 |
|
|
|
1144 |
return this;
|
|
|
1145 |
}
|
|
|
1146 |
|
|
|
1147 |
/**
|
|
|
1148 |
* Hide any currently visible steps.
|
|
|
1149 |
*
|
|
|
1150 |
* @method hide
|
|
|
1151 |
* @param {Bool} transition Animate the visibility change
|
|
|
1152 |
* @chainable
|
|
|
1153 |
* @return {Object} this.
|
|
|
1154 |
* @fires tool_usertours/stepHide
|
|
|
1155 |
* @fires tool_usertours/stepHidden
|
|
|
1156 |
*/
|
|
|
1157 |
hide(transition) {
|
|
|
1158 |
const stepHideEvent = this.dispatchEvent(eventTypes.stepHide, {}, true);
|
|
|
1159 |
if (stepHideEvent.defaultPrevented) {
|
|
|
1160 |
return this;
|
|
|
1161 |
}
|
|
|
1162 |
|
|
|
1163 |
if (this.currentStepNode && this.currentStepNode.length) {
|
|
|
1164 |
this.currentStepNode.hide();
|
|
|
1165 |
if (this.currentStepPopper) {
|
|
|
1166 |
this.currentStepPopper.destroy();
|
|
|
1167 |
}
|
|
|
1168 |
}
|
|
|
1169 |
|
|
|
1170 |
// Restore original target configuration.
|
|
|
1171 |
if (this.currentStepConfig) {
|
|
|
1172 |
let target = this.getStepTarget(this.currentStepConfig);
|
|
|
1173 |
if (target) {
|
|
|
1174 |
if (target.data('original-labelledby')) {
|
|
|
1175 |
target.attr('aria-labelledby', target.data('original-labelledby'));
|
|
|
1176 |
}
|
|
|
1177 |
|
|
|
1178 |
if (target.data('original-describedby')) {
|
|
|
1179 |
target.attr('aria-describedby', target.data('original-describedby'));
|
|
|
1180 |
}
|
|
|
1181 |
|
|
|
1182 |
if (target.data('original-tabindex')) {
|
|
|
1183 |
target.attr('tabindex', target.data('tabindex'));
|
|
|
1184 |
} else {
|
|
|
1185 |
// If the target does not have the tabindex attribute at the beginning. We need to remove it.
|
|
|
1186 |
// We should wait a little here before removing the attribute to prevent the browser from adding it again.
|
|
|
1187 |
window.setTimeout(() => {
|
|
|
1188 |
target.removeAttr('tabindex');
|
|
|
1189 |
}, 400);
|
|
|
1190 |
}
|
|
|
1191 |
}
|
|
|
1192 |
|
|
|
1193 |
// Clear the step configuration.
|
|
|
1194 |
this.currentStepConfig = null;
|
|
|
1195 |
}
|
|
|
1196 |
|
|
|
1197 |
let fadeTime = 0;
|
|
|
1198 |
if (transition) {
|
|
|
1199 |
fadeTime = 400;
|
|
|
1200 |
}
|
|
|
1201 |
|
|
|
1202 |
// Remove the backdrop features.
|
|
|
1203 |
$('[data-flexitour="step-background"]').remove();
|
|
|
1204 |
$('[data-flexitour="step-backdrop"]').removeAttr('data-flexitour');
|
|
|
1205 |
$('[data-flexitour="backdrop"]').fadeOut(fadeTime, function() {
|
|
|
1206 |
$(this).remove();
|
|
|
1207 |
});
|
|
|
1208 |
|
|
|
1209 |
// Remove aria-describedby and tabindex attributes.
|
|
|
1210 |
if (this.currentStepNode && this.currentStepNode.length) {
|
|
|
1211 |
let stepId = this.currentStepNode.attr('id');
|
|
|
1212 |
if (stepId) {
|
|
|
1213 |
let currentStepElement = '[aria-describedby="' + stepId + '-body"]';
|
|
|
1214 |
$(currentStepElement).removeAttr('tabindex');
|
|
|
1215 |
$(currentStepElement).removeAttr('aria-describedby');
|
|
|
1216 |
}
|
|
|
1217 |
}
|
|
|
1218 |
|
|
|
1219 |
// Reset the listeners.
|
|
|
1220 |
this.resetStepListeners();
|
|
|
1221 |
|
|
|
1222 |
this.accessibilityHide();
|
|
|
1223 |
|
|
|
1224 |
this.dispatchEvent(eventTypes.stepHidden);
|
|
|
1225 |
|
|
|
1226 |
this.currentStepNode = null;
|
|
|
1227 |
this.currentStepPopper = null;
|
|
|
1228 |
return this;
|
|
|
1229 |
}
|
|
|
1230 |
|
|
|
1231 |
/**
|
|
|
1232 |
* Show the current steps.
|
|
|
1233 |
*
|
|
|
1234 |
* @method show
|
|
|
1235 |
* @chainable
|
|
|
1236 |
* @return {Object} this.
|
|
|
1237 |
*/
|
|
|
1238 |
show() {
|
|
|
1239 |
// Show the current step.
|
|
|
1240 |
let startAt = this.getCurrentStepNumber();
|
|
|
1241 |
|
|
|
1242 |
return this.gotoStep(startAt);
|
|
|
1243 |
}
|
|
|
1244 |
|
|
|
1245 |
/**
|
|
|
1246 |
* Return the current step node.
|
|
|
1247 |
*
|
|
|
1248 |
* @method getStepContainer
|
|
|
1249 |
* @return {jQuery}
|
|
|
1250 |
*/
|
|
|
1251 |
getStepContainer() {
|
|
|
1252 |
return $(this.currentStepNode);
|
|
|
1253 |
}
|
|
|
1254 |
|
|
|
1255 |
/**
|
|
|
1256 |
* Calculate scrollTop.
|
|
|
1257 |
*
|
|
|
1258 |
* @method calculateScrollTop
|
|
|
1259 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
1260 |
* @return {Number}
|
|
|
1261 |
*/
|
|
|
1262 |
calculateScrollTop(stepConfig) {
|
|
|
1263 |
let viewportHeight = $(window).height();
|
|
|
1264 |
let targetNode = this.getStepTarget(stepConfig);
|
|
|
1265 |
|
|
|
1266 |
let scrollParent = $(window);
|
|
|
1267 |
if (targetNode.parents('[data-usertour="scroller"]').length) {
|
|
|
1268 |
scrollParent = targetNode.parents('[data-usertour="scroller"]');
|
|
|
1269 |
}
|
|
|
1270 |
let scrollTop = scrollParent.scrollTop();
|
|
|
1271 |
|
|
|
1272 |
if (stepConfig.placement === 'top') {
|
|
|
1273 |
// If the placement is top, center scroll at the top of the target.
|
|
|
1274 |
scrollTop = targetNode.offset().top - (viewportHeight / 2);
|
|
|
1275 |
} else if (stepConfig.placement === 'bottom') {
|
|
|
1276 |
// If the placement is bottom, center scroll at the bottom of the target.
|
|
|
1277 |
scrollTop = targetNode.offset().top + targetNode.height() + scrollTop - (viewportHeight / 2);
|
|
|
1278 |
} else if (targetNode.height() <= (viewportHeight * 0.8)) {
|
|
|
1279 |
// If the placement is left/right, and the target fits in the viewport, centre screen on the target
|
|
|
1280 |
scrollTop = targetNode.offset().top - ((viewportHeight - targetNode.height()) / 2);
|
|
|
1281 |
} else {
|
|
|
1282 |
// If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer
|
|
|
1283 |
// and change step attachmentTarget to top+.
|
|
|
1284 |
scrollTop = targetNode.offset().top - (viewportHeight * 0.2);
|
|
|
1285 |
}
|
|
|
1286 |
|
|
|
1287 |
// Never scroll over the top.
|
|
|
1288 |
scrollTop = Math.max(0, scrollTop);
|
|
|
1289 |
|
|
|
1290 |
// Never scroll beyond the bottom.
|
|
|
1291 |
scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);
|
|
|
1292 |
|
|
|
1293 |
return Math.ceil(scrollTop);
|
|
|
1294 |
}
|
|
|
1295 |
|
|
|
1296 |
/**
|
|
|
1297 |
* Calculate dialogue position for page middle.
|
|
|
1298 |
*
|
|
|
1299 |
* @param {jQuery} currentStepNode Current step node
|
|
|
1300 |
* @method calculateScrollTop
|
|
|
1301 |
*/
|
|
|
1302 |
calculateStepPositionInPage(currentStepNode) {
|
|
|
1303 |
let top = MINSPACING;
|
|
|
1304 |
const viewportHeight = $(window).height();
|
|
|
1305 |
const stepHeight = currentStepNode.height();
|
|
|
1306 |
const viewportWidth = $(window).width();
|
|
|
1307 |
const stepWidth = currentStepNode.width();
|
|
|
1308 |
if (viewportHeight >= (stepHeight + (MINSPACING * 2))) {
|
|
|
1309 |
top = Math.ceil((viewportHeight - stepHeight) / 2);
|
|
|
1310 |
} else {
|
|
|
1311 |
const headerHeight = currentStepNode.find('.modal-header').first().outerHeight() ?? 0;
|
|
|
1312 |
const footerHeight = currentStepNode.find('.modal-footer').first().outerHeight() ?? 0;
|
|
|
1313 |
const currentStepBody = currentStepNode.find('[data-placeholder="body"]').first();
|
|
|
1314 |
const maxHeight = viewportHeight - (MINSPACING * 2) - headerHeight - footerHeight;
|
|
|
1315 |
currentStepBody.css({
|
|
|
1316 |
'max-height': maxHeight + 'px',
|
|
|
1317 |
'overflow': 'auto',
|
|
|
1318 |
});
|
|
|
1319 |
}
|
|
|
1320 |
currentStepNode.offset({
|
|
|
1321 |
top: top,
|
|
|
1322 |
left: Math.ceil((viewportWidth - stepWidth) / 2)
|
|
|
1323 |
});
|
|
|
1324 |
}
|
|
|
1325 |
|
|
|
1326 |
/**
|
|
|
1327 |
* Position the step on the page.
|
|
|
1328 |
*
|
|
|
1329 |
* @method positionStep
|
|
|
1330 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
1331 |
* @chainable
|
|
|
1332 |
* @return {Object} this.
|
|
|
1333 |
*/
|
|
|
1334 |
positionStep(stepConfig) {
|
|
|
1335 |
let content = this.currentStepNode;
|
|
|
1336 |
let thisT = this;
|
|
|
1337 |
if (!content || !content.length) {
|
|
|
1338 |
// Unable to find the step node.
|
|
|
1339 |
return this;
|
|
|
1340 |
}
|
|
|
1341 |
|
|
|
1342 |
stepConfig.placement = this.recalculatePlacement(stepConfig);
|
|
|
1343 |
let flipBehavior;
|
|
|
1344 |
switch (stepConfig.placement) {
|
|
|
1345 |
case 'left':
|
|
|
1346 |
flipBehavior = ['left', 'right', 'top', 'bottom'];
|
|
|
1347 |
break;
|
|
|
1348 |
case 'right':
|
|
|
1349 |
flipBehavior = ['right', 'left', 'top', 'bottom'];
|
|
|
1350 |
break;
|
|
|
1351 |
case 'top':
|
|
|
1352 |
flipBehavior = ['top', 'bottom', 'right', 'left'];
|
|
|
1353 |
break;
|
|
|
1354 |
case 'bottom':
|
|
|
1355 |
flipBehavior = ['bottom', 'top', 'right', 'left'];
|
|
|
1356 |
break;
|
|
|
1357 |
default:
|
|
|
1358 |
flipBehavior = 'flip';
|
|
|
1359 |
break;
|
|
|
1360 |
}
|
|
|
1361 |
|
|
|
1362 |
let target = this.getStepTarget(stepConfig);
|
|
|
1363 |
var config = {
|
|
|
1364 |
placement: stepConfig.placement + '-start',
|
|
|
1365 |
removeOnDestroy: true,
|
|
|
1366 |
modifiers: {
|
|
|
1367 |
flip: {
|
|
|
1368 |
behaviour: flipBehavior,
|
|
|
1369 |
},
|
|
|
1370 |
arrow: {
|
|
|
1371 |
element: '[data-role="arrow"]',
|
|
|
1372 |
},
|
|
|
1373 |
},
|
|
|
1374 |
onCreate: function(data) {
|
|
|
1375 |
recalculateArrowPosition(data);
|
|
|
1376 |
recalculateStepPosition(data);
|
|
|
1377 |
},
|
|
|
1378 |
onUpdate: function(data) {
|
|
|
1379 |
recalculateArrowPosition(data);
|
|
|
1380 |
if (thisT.possitionNeedToBeRecalculated) {
|
|
|
1381 |
thisT.recalculatedNo++;
|
|
|
1382 |
thisT.possitionNeedToBeRecalculated = false;
|
|
|
1383 |
recalculateStepPosition(data);
|
|
|
1384 |
}
|
|
|
1385 |
},
|
|
|
1386 |
};
|
|
|
1387 |
|
|
|
1388 |
let recalculateArrowPosition = function(data) {
|
|
|
1389 |
let placement = data.placement.split('-')[0];
|
|
|
1390 |
const isVertical = ['left', 'right'].indexOf(placement) !== -1;
|
|
|
1391 |
const arrowElement = data.instance.popper.querySelector('[data-role="arrow"]');
|
|
|
1392 |
const stepElement = $(data.instance.popper.querySelector('[data-role="flexitour-step"]'));
|
|
|
1393 |
if (isVertical) {
|
|
|
1394 |
let arrowHeight = parseFloat(window.getComputedStyle(arrowElement).height);
|
|
|
1395 |
let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).top);
|
|
|
1396 |
let popperHeight = parseFloat(window.getComputedStyle(data.instance.popper).height);
|
|
|
1397 |
let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).top);
|
|
|
1398 |
let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));
|
|
|
1399 |
let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;
|
|
|
1400 |
let arrowPos = arrowOffset + (arrowHeight / 2);
|
|
|
1401 |
let maxPos = popperHeight + popperOffset - popperBorderWidth - popperBorderRadiusWidth;
|
|
|
1402 |
let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;
|
|
|
1403 |
if (arrowPos >= maxPos || arrowPos <= minPos) {
|
|
|
1404 |
let newArrowPos = 0;
|
|
|
1405 |
if (arrowPos > (popperHeight / 2)) {
|
|
|
1406 |
newArrowPos = maxPos - arrowHeight;
|
|
|
1407 |
} else {
|
|
|
1408 |
newArrowPos = minPos + arrowHeight;
|
|
|
1409 |
}
|
|
|
1410 |
$(arrowElement).css('top', newArrowPos);
|
|
|
1411 |
}
|
|
|
1412 |
} else {
|
|
|
1413 |
let arrowWidth = parseFloat(window.getComputedStyle(arrowElement).width);
|
|
|
1414 |
let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).left);
|
|
|
1415 |
let popperWidth = parseFloat(window.getComputedStyle(data.instance.popper).width);
|
|
|
1416 |
let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).left);
|
|
|
1417 |
let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));
|
|
|
1418 |
let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;
|
|
|
1419 |
let arrowPos = arrowOffset + (arrowWidth / 2);
|
|
|
1420 |
let maxPos = popperWidth + popperOffset - popperBorderWidth - popperBorderRadiusWidth;
|
|
|
1421 |
let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;
|
|
|
1422 |
if (arrowPos >= maxPos || arrowPos <= minPos) {
|
|
|
1423 |
let newArrowPos = 0;
|
|
|
1424 |
if (arrowPos > (popperWidth / 2)) {
|
|
|
1425 |
newArrowPos = maxPos - arrowWidth;
|
|
|
1426 |
} else {
|
|
|
1427 |
newArrowPos = minPos + arrowWidth;
|
|
|
1428 |
}
|
|
|
1429 |
$(arrowElement).css('left', newArrowPos);
|
|
|
1430 |
}
|
|
|
1431 |
}
|
|
|
1432 |
};
|
|
|
1433 |
|
|
|
1434 |
const recalculateStepPosition = function(data) {
|
|
|
1435 |
const placement = data.placement.split('-')[0];
|
|
|
1436 |
const isVertical = ['left', 'right'].indexOf(placement) !== -1;
|
|
|
1437 |
const popperElement = $(data.instance.popper);
|
|
|
1438 |
const targetElement = $(data.instance.reference);
|
|
|
1439 |
const arrowElement = popperElement.find('[data-role="arrow"]');
|
|
|
1440 |
const stepElement = popperElement.find('[data-role="flexitour-step"]');
|
|
|
1441 |
const viewportHeight = $(window).height();
|
|
|
1442 |
const viewportWidth = $(window).width();
|
|
|
1443 |
const arrowHeight = parseFloat(arrowElement.outerHeight(true));
|
|
|
1444 |
const popperHeight = parseFloat(popperElement.outerHeight(true));
|
|
|
1445 |
const targetHeight = parseFloat(targetElement.outerHeight(true));
|
|
|
1446 |
const arrowWidth = parseFloat(arrowElement.outerWidth(true));
|
|
|
1447 |
const popperWidth = parseFloat(popperElement.outerWidth(true));
|
|
|
1448 |
const targetWidth = parseFloat(targetElement.outerWidth(true));
|
|
|
1449 |
let maxHeight;
|
|
|
1450 |
|
|
|
1451 |
if (thisT.recalculatedNo > 1) {
|
|
|
1452 |
// The current screen is too small, and cannot fit with the original placement.
|
|
|
1453 |
// We should set the placement to auto so the PopperJS can calculate the perfect placement.
|
|
|
1454 |
thisT.currentStepPopper.options.placement = isVertical ? 'auto-left' : 'auto-bottom';
|
|
|
1455 |
}
|
|
|
1456 |
if (thisT.recalculatedNo > 2) {
|
|
|
1457 |
// Return here to prevent recursive calling.
|
|
|
1458 |
return;
|
|
|
1459 |
}
|
|
|
1460 |
|
|
|
1461 |
if (isVertical) {
|
|
|
1462 |
// Find the best place to put the tour: Left of right.
|
|
|
1463 |
const leftSpace = targetElement.offset().left > 0 ? targetElement.offset().left : 0;
|
|
|
1464 |
const rightSpace = viewportWidth - leftSpace - targetWidth;
|
|
|
1465 |
const remainingSpace = leftSpace >= rightSpace ? leftSpace : rightSpace;
|
|
|
1466 |
maxHeight = viewportHeight - MINSPACING * 2;
|
|
|
1467 |
if (remainingSpace < (popperWidth + arrowWidth)) {
|
|
|
1468 |
const maxWidth = remainingSpace - MINSPACING - arrowWidth;
|
|
|
1469 |
if (maxWidth > 0) {
|
|
|
1470 |
popperElement.css({
|
|
|
1471 |
'max-width': maxWidth + 'px',
|
|
|
1472 |
});
|
|
|
1473 |
// Not enough space, flag true to make Popper to recalculate the position.
|
|
|
1474 |
thisT.possitionNeedToBeRecalculated = true;
|
|
|
1475 |
}
|
|
|
1476 |
} else if (maxHeight < popperHeight) {
|
|
|
1477 |
// Check if the Popper's height can fit the viewport height or not.
|
|
|
1478 |
// If not, set the correct max-height value for the Popper element.
|
|
|
1479 |
popperElement.css({
|
|
|
1480 |
'max-height': maxHeight + 'px',
|
|
|
1481 |
});
|
|
|
1482 |
}
|
|
|
1483 |
} else {
|
|
|
1484 |
// Find the best place to put the tour: Top of bottom.
|
|
|
1485 |
const topSpace = targetElement.offset().top > 0 ? targetElement.offset().top : 0;
|
|
|
1486 |
const bottomSpace = viewportHeight - topSpace - targetHeight;
|
|
|
1487 |
const remainingSpace = topSpace >= bottomSpace ? topSpace : bottomSpace;
|
|
|
1488 |
maxHeight = remainingSpace - MINSPACING - arrowHeight;
|
|
|
1489 |
if (remainingSpace < (popperHeight + arrowHeight)) {
|
|
|
1490 |
// Not enough space, flag true to make Popper to recalculate the position.
|
|
|
1491 |
thisT.possitionNeedToBeRecalculated = true;
|
|
|
1492 |
}
|
|
|
1493 |
}
|
|
|
1494 |
|
|
|
1495 |
// Check if the Popper's height can fit the viewport height or not.
|
|
|
1496 |
// If not, set the correct max-height value for the body.
|
|
|
1497 |
const currentStepBody = stepElement.find('[data-placeholder="body"]').first();
|
|
|
1498 |
const headerEle = stepElement.find('.modal-header').first();
|
|
|
1499 |
const footerEle = stepElement.find('.modal-footer').first();
|
|
|
1500 |
const headerHeight = headerEle.outerHeight(true) ?? 0;
|
|
|
1501 |
const footerHeight = footerEle.outerHeight(true) ?? 0;
|
|
|
1502 |
maxHeight = maxHeight - headerHeight - footerHeight;
|
|
|
1503 |
if (maxHeight > 0) {
|
|
|
1504 |
headerEle.removeClass('minimal');
|
|
|
1505 |
footerEle.removeClass('minimal');
|
|
|
1506 |
currentStepBody.css({
|
|
|
1507 |
'max-height': maxHeight + 'px',
|
|
|
1508 |
'overflow': 'auto',
|
|
|
1509 |
});
|
|
|
1510 |
} else {
|
|
|
1511 |
headerEle.addClass('minimal');
|
|
|
1512 |
footerEle.addClass('minimal');
|
|
|
1513 |
}
|
|
|
1514 |
// Call the Popper update method to update the position.
|
|
|
1515 |
thisT.currentStepPopper.update();
|
|
|
1516 |
};
|
|
|
1517 |
|
|
|
1518 |
let background = $('[data-flexitour="step-background"]');
|
|
|
1519 |
if (background.length) {
|
|
|
1520 |
target = background;
|
|
|
1521 |
}
|
|
|
1522 |
this.currentStepPopper = new Popper(target, content[0], config);
|
|
|
1523 |
|
|
|
1524 |
return this;
|
|
|
1525 |
}
|
|
|
1526 |
|
|
|
1527 |
/**
|
|
|
1528 |
* For left/right placement, checks that there is room for the step at current window size.
|
|
|
1529 |
*
|
|
|
1530 |
* If there is not enough room, changes placement to 'top'.
|
|
|
1531 |
*
|
|
|
1532 |
* @method recalculatePlacement
|
|
|
1533 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
1534 |
* @return {String} The placement after recalculate
|
|
|
1535 |
*/
|
|
|
1536 |
recalculatePlacement(stepConfig) {
|
|
|
1537 |
const buffer = 10;
|
|
|
1538 |
const arrowWidth = 16;
|
|
|
1539 |
let target = this.getStepTarget(stepConfig);
|
|
|
1540 |
let widthContent = this.currentStepNode.width() + arrowWidth;
|
|
|
1541 |
let targetOffsetLeft = target.offset().left - buffer;
|
|
|
1542 |
let targetOffsetRight = target.offset().left + target.width() + buffer;
|
|
|
1543 |
let placement = stepConfig.placement;
|
|
|
1544 |
|
|
|
1545 |
if (['left', 'right'].indexOf(placement) !== -1) {
|
|
|
1546 |
if ((targetOffsetLeft < (widthContent + buffer)) &&
|
|
|
1547 |
((targetOffsetRight + widthContent + buffer) > document.documentElement.clientWidth)) {
|
|
|
1548 |
placement = 'top';
|
|
|
1549 |
}
|
|
|
1550 |
}
|
|
|
1551 |
return placement;
|
|
|
1552 |
}
|
|
|
1553 |
|
|
|
1554 |
/**
|
|
|
1555 |
* Add the backdrop.
|
|
|
1556 |
*
|
|
|
1557 |
* @method positionBackdrop
|
|
|
1558 |
* @param {Object} stepConfig The step configuration of the step
|
|
|
1559 |
* @chainable
|
|
|
1560 |
* @return {Object} this.
|
|
|
1561 |
*/
|
|
|
1562 |
positionBackdrop(stepConfig) {
|
|
|
1563 |
if (stepConfig.backdrop) {
|
|
|
1564 |
this.currentStepConfig.hasBackdrop = true;
|
|
|
1565 |
let backdrop = $('<div data-flexitour="backdrop"></div>');
|
|
|
1566 |
|
|
|
1567 |
if (stepConfig.zIndex) {
|
|
|
1568 |
if (stepConfig.attachPoint === 'append') {
|
|
|
1569 |
stepConfig.attachTo.append(backdrop);
|
|
|
1570 |
} else {
|
|
|
1571 |
backdrop.insertAfter(stepConfig.attachTo);
|
|
|
1572 |
}
|
|
|
1573 |
} else {
|
|
|
1574 |
$('body').append(backdrop);
|
|
|
1575 |
}
|
|
|
1576 |
|
|
|
1577 |
if (this.isStepActuallyVisible(stepConfig)) {
|
|
|
1578 |
// The step has a visible target.
|
|
|
1579 |
// Punch a hole through the backdrop.
|
|
|
1580 |
let background = $('[data-flexitour="step-background"]');
|
|
|
1581 |
if (!background.length) {
|
|
|
1582 |
background = $('<div data-flexitour="step-background"></div>');
|
|
|
1583 |
}
|
|
|
1584 |
|
|
|
1585 |
let targetNode = this.getStepTarget(stepConfig);
|
|
|
1586 |
|
|
|
1587 |
let buffer = 10;
|
|
|
1588 |
|
|
|
1589 |
let colorNode = targetNode;
|
|
|
1590 |
if (buffer) {
|
|
|
1591 |
colorNode = $('body');
|
|
|
1592 |
}
|
|
|
1593 |
|
|
|
1594 |
let drawertop = 0;
|
|
|
1595 |
if (targetNode.parents('[data-usertour="scroller"]').length) {
|
|
|
1596 |
const scrollerElement = targetNode.parents('[data-usertour="scroller"]');
|
|
|
1597 |
const navigationBuffer = scrollerElement.offset().top;
|
|
|
1598 |
if (scrollerElement.scrollTop() >= navigationBuffer) {
|
|
|
1599 |
drawertop = scrollerElement.scrollTop() - navigationBuffer;
|
|
|
1600 |
background.css({
|
|
|
1601 |
position: 'fixed'
|
|
|
1602 |
});
|
|
|
1603 |
}
|
|
|
1604 |
}
|
|
|
1605 |
|
|
|
1606 |
background.css({
|
|
|
1607 |
width: targetNode.outerWidth() + buffer + buffer,
|
|
|
1608 |
height: targetNode.outerHeight() + buffer + buffer,
|
|
|
1609 |
left: targetNode.offset().left - buffer,
|
|
|
1610 |
top: targetNode.offset().top + drawertop - buffer,
|
|
|
1611 |
backgroundColor: this.calculateInherittedBackgroundColor(colorNode),
|
|
|
1612 |
});
|
|
|
1613 |
|
|
|
1614 |
if (targetNode.offset().left < buffer) {
|
|
|
1615 |
background.css({
|
|
|
1616 |
width: targetNode.outerWidth() + targetNode.offset().left + buffer,
|
|
|
1617 |
left: targetNode.offset().left,
|
|
|
1618 |
});
|
|
|
1619 |
}
|
|
|
1620 |
|
|
|
1621 |
if ((targetNode.offset().top + drawertop) < buffer) {
|
|
|
1622 |
background.css({
|
|
|
1623 |
height: targetNode.outerHeight() + targetNode.offset().top + buffer,
|
|
|
1624 |
top: targetNode.offset().top,
|
|
|
1625 |
});
|
|
|
1626 |
}
|
|
|
1627 |
|
|
|
1628 |
let targetRadius = targetNode.css('borderRadius');
|
|
|
1629 |
if (targetRadius && targetRadius !== $('body').css('borderRadius')) {
|
|
|
1630 |
background.css('borderRadius', targetRadius);
|
|
|
1631 |
}
|
|
|
1632 |
|
|
|
1633 |
let targetPosition = this.calculatePosition(targetNode);
|
|
|
1634 |
if (targetPosition === 'absolute') {
|
|
|
1635 |
background.css('position', 'fixed');
|
|
|
1636 |
}
|
|
|
1637 |
|
|
|
1638 |
let fader = background.clone();
|
|
|
1639 |
fader.css({
|
|
|
1640 |
backgroundColor: backdrop.css('backgroundColor'),
|
|
|
1641 |
opacity: backdrop.css('opacity'),
|
|
|
1642 |
});
|
|
|
1643 |
fader.attr('data-flexitour', 'step-background-fader');
|
|
|
1644 |
|
|
|
1645 |
if (!stepConfig.zIndex) {
|
|
|
1646 |
let targetClone = targetNode.clone();
|
|
|
1647 |
background.append(targetClone.first());
|
|
|
1648 |
$('body').append(fader);
|
|
|
1649 |
$('body').append(background);
|
|
|
1650 |
} else {
|
|
|
1651 |
if (stepConfig.attachPoint === 'append') {
|
|
|
1652 |
stepConfig.attachTo.append(background);
|
|
|
1653 |
} else {
|
|
|
1654 |
fader.insertAfter(stepConfig.attachTo);
|
|
|
1655 |
background.insertAfter(stepConfig.attachTo);
|
|
|
1656 |
}
|
|
|
1657 |
}
|
|
|
1658 |
|
|
|
1659 |
// Add the backdrop data to the actual target.
|
|
|
1660 |
// This is the part which actually does the work.
|
|
|
1661 |
targetNode.attr('data-flexitour', 'step-backdrop');
|
|
|
1662 |
|
|
|
1663 |
if (stepConfig.zIndex) {
|
|
|
1664 |
backdrop.css('zIndex', stepConfig.zIndex);
|
|
|
1665 |
background.css('zIndex', stepConfig.zIndex + 1);
|
|
|
1666 |
targetNode.css('zIndex', stepConfig.zIndex + 2);
|
|
|
1667 |
}
|
|
|
1668 |
|
|
|
1669 |
fader.fadeOut('2000', function() {
|
|
|
1670 |
$(this).remove();
|
|
|
1671 |
});
|
|
|
1672 |
}
|
|
|
1673 |
}
|
|
|
1674 |
return this;
|
|
|
1675 |
}
|
|
|
1676 |
|
|
|
1677 |
/**
|
|
|
1678 |
* Calculate the inheritted z-index.
|
|
|
1679 |
*
|
|
|
1680 |
* @method calculateZIndex
|
|
|
1681 |
* @param {jQuery} elem The element to calculate z-index for
|
|
|
1682 |
* @return {Number} Calculated z-index
|
|
|
1683 |
*/
|
|
|
1684 |
calculateZIndex(elem) {
|
|
|
1685 |
elem = $(elem);
|
|
|
1686 |
if (this.requireDefaultTourZindex(elem)) {
|
|
|
1687 |
return 0;
|
|
|
1688 |
}
|
|
|
1689 |
while (elem.length && elem[0] !== document) {
|
|
|
1690 |
// Ignore z-index if position is set to a value where z-index is ignored by the browser
|
|
|
1691 |
// This makes behavior of this function consistent across browsers
|
|
|
1692 |
// WebKit always returns auto if the element is positioned.
|
|
|
1693 |
let position = elem.css("position");
|
|
|
1694 |
if (position === "absolute" || position === "fixed") {
|
|
|
1695 |
// IE returns 0 when zIndex is not specified
|
|
|
1696 |
// other browsers return a string
|
|
|
1697 |
// we ignore the case of nested elements with an explicit value of 0
|
|
|
1698 |
// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
|
|
|
1699 |
let value = parseInt(elem.css("zIndex"), 10);
|
|
|
1700 |
if (!isNaN(value) && value !== 0) {
|
|
|
1701 |
return value;
|
|
|
1702 |
}
|
|
|
1703 |
}
|
|
|
1704 |
elem = elem.parent();
|
|
|
1705 |
}
|
|
|
1706 |
|
|
|
1707 |
return 0;
|
|
|
1708 |
}
|
|
|
1709 |
|
|
|
1710 |
/**
|
|
|
1711 |
* Check if the element require the default tour z-index.
|
|
|
1712 |
*
|
|
|
1713 |
* Some page elements have fixed z-index. However, their weight is not enough to cover
|
|
|
1714 |
* other page elements like the top navbar or a sticky footer so they use the default
|
|
|
1715 |
* tour z-index instead.
|
|
|
1716 |
*
|
|
|
1717 |
* @param {jQuery} elem the page element to highlight
|
|
|
1718 |
* @return {Boolean} true if the element requires the default tour z-index instead of the calculated one
|
|
|
1719 |
*/
|
|
|
1720 |
requireDefaultTourZindex(elem) {
|
|
|
1721 |
if (elem.parents('[data-region="fixed-drawer"]').length !== 0) {
|
|
|
1722 |
return true;
|
|
|
1723 |
}
|
|
|
1724 |
return false;
|
|
|
1725 |
}
|
|
|
1726 |
|
|
|
1727 |
/**
|
|
|
1728 |
* Calculate the inheritted background colour.
|
|
|
1729 |
*
|
|
|
1730 |
* @method calculateInherittedBackgroundColor
|
|
|
1731 |
* @param {jQuery} elem The element to calculate colour for
|
|
|
1732 |
* @return {String} Calculated background colour
|
|
|
1733 |
*/
|
|
|
1734 |
calculateInherittedBackgroundColor(elem) {
|
|
|
1735 |
// Use a fake node to compare each element against.
|
|
|
1736 |
let fakeNode = $('<div>').hide();
|
|
|
1737 |
$('body').append(fakeNode);
|
|
|
1738 |
let fakeElemColor = fakeNode.css('backgroundColor');
|
|
|
1739 |
fakeNode.remove();
|
|
|
1740 |
|
|
|
1741 |
elem = $(elem);
|
|
|
1742 |
while (elem.length && elem[0] !== document) {
|
|
|
1743 |
let color = elem.css('backgroundColor');
|
|
|
1744 |
if (color !== fakeElemColor) {
|
|
|
1745 |
return color;
|
|
|
1746 |
}
|
|
|
1747 |
elem = elem.parent();
|
|
|
1748 |
}
|
|
|
1749 |
|
|
|
1750 |
return null;
|
|
|
1751 |
}
|
|
|
1752 |
|
|
|
1753 |
/**
|
|
|
1754 |
* Calculate the inheritted position.
|
|
|
1755 |
*
|
|
|
1756 |
* @method calculatePosition
|
|
|
1757 |
* @param {jQuery} elem The element to calculate position for
|
|
|
1758 |
* @return {String} Calculated position
|
|
|
1759 |
*/
|
|
|
1760 |
calculatePosition(elem) {
|
|
|
1761 |
elem = $(elem);
|
|
|
1762 |
while (elem.length && elem[0] !== document) {
|
|
|
1763 |
let position = elem.css('position');
|
|
|
1764 |
if (position !== 'static') {
|
|
|
1765 |
return position;
|
|
|
1766 |
}
|
|
|
1767 |
elem = elem.parent();
|
|
|
1768 |
}
|
|
|
1769 |
|
|
|
1770 |
return null;
|
|
|
1771 |
}
|
|
|
1772 |
|
|
|
1773 |
/**
|
|
|
1774 |
* Perform accessibility changes for step shown.
|
|
|
1775 |
*
|
|
|
1776 |
* This will add aria-hidden="true" to all siblings and parent siblings.
|
|
|
1777 |
*
|
|
|
1778 |
* @method accessibilityShow
|
|
|
1779 |
*/
|
|
|
1780 |
accessibilityShow() {
|
|
|
1781 |
let stateHolder = 'data-has-hidden';
|
|
|
1782 |
let attrName = 'aria-hidden';
|
|
|
1783 |
let hideFunction = function(child) {
|
|
|
1784 |
let flexitourRole = child.data('flexitour');
|
|
|
1785 |
if (flexitourRole) {
|
|
|
1786 |
switch (flexitourRole) {
|
|
|
1787 |
case 'container':
|
|
|
1788 |
case 'target':
|
|
|
1789 |
return;
|
|
|
1790 |
}
|
|
|
1791 |
}
|
|
|
1792 |
|
|
|
1793 |
let hidden = child.attr(attrName);
|
|
|
1794 |
if (!hidden) {
|
|
|
1795 |
child.attr(stateHolder, true);
|
|
|
1796 |
Aria.hide(child);
|
|
|
1797 |
}
|
|
|
1798 |
};
|
|
|
1799 |
|
|
|
1800 |
this.currentStepNode.siblings().each(function(index, node) {
|
|
|
1801 |
hideFunction($(node));
|
|
|
1802 |
});
|
|
|
1803 |
this.currentStepNode.parentsUntil('body').siblings().each(function(index, node) {
|
|
|
1804 |
hideFunction($(node));
|
|
|
1805 |
});
|
|
|
1806 |
}
|
|
|
1807 |
|
|
|
1808 |
/**
|
|
|
1809 |
* Perform accessibility changes for step hidden.
|
|
|
1810 |
*
|
|
|
1811 |
* This will remove any newly added aria-hidden="true".
|
|
|
1812 |
*
|
|
|
1813 |
* @method accessibilityHide
|
|
|
1814 |
*/
|
|
|
1815 |
accessibilityHide() {
|
|
|
1816 |
let stateHolder = 'data-has-hidden';
|
|
|
1817 |
let showFunction = function(child) {
|
|
|
1818 |
let hidden = child.attr(stateHolder);
|
|
|
1819 |
if (typeof hidden !== 'undefined') {
|
|
|
1820 |
child.removeAttr(stateHolder);
|
|
|
1821 |
Aria.unhide(child);
|
|
|
1822 |
}
|
|
|
1823 |
};
|
|
|
1824 |
|
|
|
1825 |
$('[' + stateHolder + ']').each(function(index, node) {
|
|
|
1826 |
showFunction($(node));
|
|
|
1827 |
});
|
|
|
1828 |
}
|
|
|
1829 |
};
|
|
|
1830 |
|
|
|
1831 |
export default Tour;
|