MDL-68390 tool_usertours: Update to use aria-hidden module
[moodle.git] / admin / tool / usertours / amd / src / tour.js
CommitLineData
3095c3f6
RW
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/>.
001fc061
AN
15
16/**
3095c3f6 17 * Manage user tours in Moodle.
001fc061 18 *
3095c3f6
RW
19 * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
20 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
001fc061 21 */
001fc061 22
3095c3f6 23import $ from 'jquery';
7c56d558 24import * as Aria from 'core/aria';
3095c3f6 25import Popper from 'core/popper';
aabebc75 26
001fc061 27/**
3095c3f6 28 * A Tour.
001fc061 29 *
3095c3f6 30 * @class Tour
001fc061 31 */
3095c3f6
RW
32export default class Tour {
33 /**
34 * @param {object} config The configuration object.
35 */
36 constructor(config) {
37 this.init(config);
38 }
001fc061 39
3095c3f6
RW
40 /**
41 * Initialise the tour.
42 *
43 * @method init
44 * @param {Object} config The configuration object.
45 * @chainable
46 * @return {Object} this.
47 */
48 init(config) {
49 // Unset all handlers.
50 this.eventHandlers = {};
001fc061 51
3095c3f6
RW
52 // Reset the current tour states.
53 this.reset();
001fc061 54
3095c3f6
RW
55 // Store the initial configuration.
56 this.originalConfiguration = config || {};
001fc061 57
3095c3f6
RW
58 // Apply configuration.
59 this.configure.apply(this, arguments);
001fc061 60
3095c3f6
RW
61 try {
62 this.storage = window.sessionStorage;
63 this.storageKey = 'tourstate_' + this.tourName;
64 } catch (e) {
65 this.storage = false;
66 this.storageKey = '';
67 }
001fc061 68
3095c3f6
RW
69 return this;
70 }
001fc061 71
3095c3f6
RW
72 /**
73 * Reset the current tour state.
74 *
75 * @method reset
76 * @chainable
77 * @return {Object} this.
78 */
79 reset() {
80 // Hide the current step.
81 this.hide();
001fc061 82
3095c3f6
RW
83 // Unset all handlers.
84 this.eventHandlers = [];
001fc061 85
3095c3f6
RW
86 // Unset all listeners.
87 this.resetStepListeners();
001fc061 88
3095c3f6
RW
89 // Unset the original configuration.
90 this.originalConfiguration = {};
001fc061 91
3095c3f6
RW
92 // Reset the current step number and list of steps.
93 this.steps = [];
001fc061 94
3095c3f6
RW
95 // Reset the current step number.
96 this.currentStepNumber = 0;
001fc061 97
3095c3f6 98 return this;
aabebc75
AN
99 }
100
3095c3f6
RW
101 /**
102 * Prepare tour configuration.
103 *
104 * @method configure
105 * @param {Object} config The configuration object.
106 * @chainable
107 * @return {Object} this.
108 */
109 configure(config) {
110 if (typeof config === 'object') {
111 // Tour name.
112 if (typeof config.tourName !== 'undefined') {
113 this.tourName = config.tourName;
114 }
001fc061 115
3095c3f6
RW
116 // Set up eventHandlers.
117 if (config.eventHandlers) {
118 for (let eventName in config.eventHandlers) {
119 config.eventHandlers[eventName].forEach(function(handler) {
120 this.addEventHandler(eventName, handler);
121 }, this);
122 }
123 }
001fc061 124
3095c3f6
RW
125 // Reset the step configuration.
126 this.resetStepDefaults(true);
001fc061 127
3095c3f6
RW
128 // Configure the steps.
129 if (typeof config.steps === 'object') {
130 this.steps = config.steps;
131 }
001fc061 132
3095c3f6
RW
133 if (typeof config.template !== 'undefined') {
134 this.templateContent = config.template;
135 }
001fc061
AN
136 }
137
3095c3f6
RW
138 // Check that we have enough to start the tour.
139 this.checkMinimumRequirements();
001fc061 140
3095c3f6
RW
141 return this;
142 }
001fc061 143
3095c3f6
RW
144 /**
145 * Check that the configuration meets the minimum requirements.
146 *
147 * @method checkMinimumRequirements
148 */
149 checkMinimumRequirements() {
150 // Need a tourName.
151 if (!this.tourName) {
152 throw new Error("Tour Name required");
001fc061
AN
153 }
154
3095c3f6
RW
155 // Need a minimum of one step.
156 if (!this.steps || !this.steps.length) {
157 throw new Error("Steps must be specified");
001fc061
AN
158 }
159 }
160
3095c3f6
RW
161 /**
162 * Reset step default configuration.
163 *
164 * @method resetStepDefaults
165 * @param {Boolean} loadOriginalConfiguration Whether to load the original configuration supplied with the Tour.
166 * @chainable
167 * @return {Object} this.
168 */
169 resetStepDefaults(loadOriginalConfiguration) {
170 if (typeof loadOriginalConfiguration === 'undefined') {
171 loadOriginalConfiguration = true;
172 }
001fc061 173
3095c3f6
RW
174 this.stepDefaults = {};
175 if (!loadOriginalConfiguration || typeof this.originalConfiguration.stepDefaults === 'undefined') {
176 this.setStepDefaults({});
177 } else {
178 this.setStepDefaults(this.originalConfiguration.stepDefaults);
179 }
001fc061 180
3095c3f6 181 return this;
001fc061
AN
182 }
183
3095c3f6
RW
184 /**
185 * Set the step defaults.
186 *
187 * @method setStepDefaults
188 * @param {Object} stepDefaults The step defaults to apply to all steps
189 * @chainable
190 * @return {Object} this.
191 */
192 setStepDefaults(stepDefaults) {
193 if (!this.stepDefaults) {
194 this.stepDefaults = {};
195 }
196 $.extend(
197 this.stepDefaults,
198 {
199 element: '',
200 placement: 'top',
201 delay: 0,
202 moveOnClick: false,
203 moveAfterTime: 0,
204 orphan: false,
205 direction: 1,
206 },
207 stepDefaults
208 );
001fc061 209
3095c3f6 210 return this;
001fc061
AN
211 }
212
3095c3f6
RW
213 /**
214 * Retrieve the current step number.
215 *
216 * @method getCurrentStepNumber
217 * @return {Integer} The current step number
218 */
219 getCurrentStepNumber() {
220 return parseInt(this.currentStepNumber, 10);
001fc061
AN
221 }
222
3095c3f6
RW
223 /**
224 * Store the current step number.
225 *
226 * @method setCurrentStepNumber
227 * @param {Integer} stepNumber The current step number
228 * @chainable
229 */
230 setCurrentStepNumber(stepNumber) {
231 this.currentStepNumber = stepNumber;
232 if (this.storage) {
233 try {
234 this.storage.setItem(this.storageKey, stepNumber);
235 } catch (e) {
236 if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
237 this.storage.removeItem(this.storageKey);
238 }
239 }
240 }
001fc061 241 }
001fc061 242
3095c3f6
RW
243 /**
244 * Get the next step number after the currently displayed step.
245 *
246 * @method getNextStepNumber
247 * @param {Integer} stepNumber The current step number
248 * @return {Integer} The next step number to display
249 */
250 getNextStepNumber(stepNumber) {
251 if (typeof stepNumber === 'undefined') {
252 stepNumber = this.getCurrentStepNumber();
253 }
254 let nextStepNumber = stepNumber + 1;
001fc061 255
3095c3f6
RW
256 // Keep checking the remaining steps.
257 while (nextStepNumber <= this.steps.length) {
258 if (this.isStepPotentiallyVisible(this.getStepConfig(nextStepNumber))) {
259 return nextStepNumber;
aabebc75 260 }
3095c3f6 261 nextStepNumber++;
aabebc75 262 }
001fc061 263
3095c3f6 264 return null;
001fc061 265 }
001fc061 266
3095c3f6
RW
267 /**
268 * Get the previous step number before the currently displayed step.
269 *
270 * @method getPreviousStepNumber
271 * @param {Integer} stepNumber The current step number
272 * @return {Integer} The previous step number to display
273 */
274 getPreviousStepNumber(stepNumber) {
275 if (typeof stepNumber === 'undefined') {
276 stepNumber = this.getCurrentStepNumber();
001fc061 277 }
3095c3f6 278 let previousStepNumber = stepNumber - 1;
001fc061 279
3095c3f6
RW
280 // Keep checking the remaining steps.
281 while (previousStepNumber >= 0) {
282 if (this.isStepPotentiallyVisible(this.getStepConfig(previousStepNumber))) {
283 return previousStepNumber;
284 }
285 previousStepNumber--;
286 }
001fc061 287
3095c3f6 288 return null;
001fc061 289 }
001fc061 290
3095c3f6
RW
291 /**
292 * Is the step the final step number?
293 *
294 * @method isLastStep
295 * @param {Integer} stepNumber Step number to test
296 * @return {Boolean} Whether the step is the final step
297 */
298 isLastStep(stepNumber) {
299 let nextStepNumber = this.getNextStepNumber(stepNumber);
300
301 return nextStepNumber === null;
001fc061
AN
302 }
303
3095c3f6
RW
304 /**
305 * Is the step the first step number?
306 *
307 * @method isFirstStep
308 * @param {Integer} stepNumber Step number to test
309 * @return {Boolean} Whether the step is the first step
310 */
311 isFirstStep(stepNumber) {
312 let previousStepNumber = this.getPreviousStepNumber(stepNumber);
313
314 return previousStepNumber === null;
315 }
001fc061 316
3095c3f6
RW
317 /**
318 * Is this step potentially visible?
319 *
320 * @method isStepPotentiallyVisible
321 * @param {Object} stepConfig The step configuration to normalise
322 * @return {Boolean} Whether the step is the potentially visible
323 */
324 isStepPotentiallyVisible(stepConfig) {
325 if (!stepConfig) {
326 // Without step config, there can be no step.
327 return false;
328 }
001fc061 329
3095c3f6
RW
330 if (this.isStepActuallyVisible(stepConfig)) {
331 // If it is actually visible, it is already potentially visible.
332 return true;
333 }
001fc061 334
3095c3f6
RW
335 if (typeof stepConfig.orphan !== 'undefined' && stepConfig.orphan) {
336 // Orphan steps have no target. They are always visible.
337 return true;
338 }
001fc061 339
3095c3f6
RW
340 if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay) {
341 // Only return true if the activated has not been used yet.
342 return true;
343 }
001fc061 344
3095c3f6 345 // Not theoretically, or actually visible.
001fc061
AN
346 return false;
347 }
348
3095c3f6
RW
349 /**
350 * Is this step actually visible?
351 *
352 * @method isStepActuallyVisible
353 * @param {Object} stepConfig The step configuration to normalise
354 * @return {Boolean} Whether the step is actually visible
355 */
356 isStepActuallyVisible(stepConfig) {
357 if (!stepConfig) {
358 // Without step config, there can be no step.
359 return false;
360 }
001fc061 361
3095c3f6
RW
362 let target = this.getStepTarget(stepConfig);
363 if (target && target.length && target.is(':visible')) {
364 // Without a target, there can be no step.
365 return !!target.length;
366 }
001fc061 367
001fc061
AN
368 return false;
369 }
370
3095c3f6
RW
371 /**
372 * Go to the next step in the tour.
373 *
374 * @method next
375 * @chainable
376 * @return {Object} this.
377 */
378 next() {
379 return this.gotoStep(this.getNextStepNumber());
001fc061
AN
380 }
381
3095c3f6
RW
382 /**
383 * Go to the previous step in the tour.
384 *
385 * @method previous
386 * @chainable
387 * @return {Object} this.
388 */
389 previous() {
390 return this.gotoStep(this.getPreviousStepNumber(), -1);
391 }
001fc061 392
3095c3f6
RW
393 /**
394 * Go to the specified step in the tour.
395 *
396 * @method gotoStep
397 * @param {Integer} stepNumber The step number to display
398 * @param {Integer} direction Next or previous step
399 * @chainable
400 * @return {Object} this.
401 */
402 gotoStep(stepNumber, direction) {
403 if (stepNumber < 0) {
404 return this.endTour();
405 }
001fc061 406
3095c3f6
RW
407 let stepConfig = this.getStepConfig(stepNumber);
408 if (stepConfig === null) {
409 return this.endTour();
410 }
001fc061 411
3095c3f6 412 return this._gotoStep(stepConfig, direction);
001fc061
AN
413 }
414
3095c3f6
RW
415 _gotoStep(stepConfig, direction) {
416 if (!stepConfig) {
417 return this.endTour();
418 }
001fc061 419
3095c3f6
RW
420 if (typeof stepConfig.delay !== 'undefined' && stepConfig.delay && !stepConfig.delayed) {
421 stepConfig.delayed = true;
422 window.setTimeout(this._gotoStep.bind(this), stepConfig.delay, stepConfig, direction);
001fc061 423
3095c3f6
RW
424 return this;
425 } else if (!stepConfig.orphan && !this.isStepActuallyVisible(stepConfig)) {
426 let fn = direction == -1 ? 'getPreviousStepNumber' : 'getNextStepNumber';
427 return this.gotoStep(this[fn](stepConfig.stepNumber), direction);
428 }
001fc061 429
3095c3f6
RW
430 this.hide();
431
432 this.fireEventHandlers('beforeRender', stepConfig);
433 this.renderStep(stepConfig);
434 this.fireEventHandlers('afterRender', stepConfig);
001fc061
AN
435
436 return this;
001fc061
AN
437 }
438
3095c3f6
RW
439 /**
440 * Fetch the normalised step configuration for the specified step number.
441 *
442 * @method getStepConfig
443 * @param {Integer} stepNumber The step number to fetch configuration for
444 * @return {Object} The step configuration
445 */
446 getStepConfig(stepNumber) {
447 if (stepNumber === null || stepNumber < 0 || stepNumber >= this.steps.length) {
448 return null;
449 }
001fc061 450
3095c3f6
RW
451 // Normalise the step configuration.
452 let stepConfig = this.normalizeStepConfig(this.steps[stepNumber]);
001fc061 453
3095c3f6
RW
454 // Add the stepNumber to the stepConfig.
455 stepConfig = $.extend(stepConfig, {stepNumber: stepNumber});
001fc061 456
3095c3f6 457 return stepConfig;
001fc061
AN
458 }
459
3095c3f6
RW
460 /**
461 * Normalise the supplied step configuration.
462 *
463 * @method normalizeStepConfig
464 * @param {Object} stepConfig The step configuration to normalise
465 * @return {Object} The normalised step configuration
466 */
467 normalizeStepConfig(stepConfig) {
468
469 if (typeof stepConfig.reflex !== 'undefined' && typeof stepConfig.moveAfterClick === 'undefined') {
470 stepConfig.moveAfterClick = stepConfig.reflex;
471 }
001fc061 472
3095c3f6
RW
473 if (typeof stepConfig.element !== 'undefined' && typeof stepConfig.target === 'undefined') {
474 stepConfig.target = stepConfig.element;
475 }
001fc061 476
3095c3f6
RW
477 if (typeof stepConfig.content !== 'undefined' && typeof stepConfig.body === 'undefined') {
478 stepConfig.body = stepConfig.content;
479 }
001fc061 480
3095c3f6 481 stepConfig = $.extend({}, this.stepDefaults, stepConfig);
001fc061 482
3095c3f6
RW
483 stepConfig = $.extend({}, {
484 attachTo: stepConfig.target,
485 attachPoint: 'after',
486 }, stepConfig);
001fc061 487
3095c3f6
RW
488 if (stepConfig.attachTo) {
489 stepConfig.attachTo = $(stepConfig.attachTo).first();
490 }
001fc061 491
3095c3f6 492 return stepConfig;
001fc061
AN
493 }
494
3095c3f6
RW
495 /**
496 * Fetch the actual step target from the selector.
497 *
498 * This should not be called until after any delay has completed.
499 *
500 * @method getStepTarget
501 * @param {Object} stepConfig The step configuration
502 * @return {$}
503 */
504 getStepTarget(stepConfig) {
505 if (stepConfig.target) {
506 return $(stepConfig.target);
507 }
001fc061 508
3095c3f6 509 return null;
f2dab834
AN
510 }
511
3095c3f6
RW
512 /**
513 * Fire any event handlers for the specified event.
514 *
515 * @param {String} eventName The name of the event to handle
516 * @param {Object} data Any data to pass to the event
517 * @chainable
518 * @return {Object} this.
519 */
520 fireEventHandlers(eventName, data) {
521 if (typeof this.eventHandlers[eventName] === 'undefined') {
522 return this;
523 }
001fc061 524
3095c3f6
RW
525 this.eventHandlers[eventName].forEach(function(thisEvent) {
526 thisEvent.call(this, data);
527 }, this);
001fc061 528
001fc061
AN
529 return this;
530 }
531
3095c3f6
RW
532 /**
533 * @method addEventHandler
534 * @param {string} eventName The name of the event to listen for
535 * @param {function} handler The event handler to call
536 * @return {Object} this.
537 */
538 addEventHandler(eventName, handler) {
539 if (typeof this.eventHandlers[eventName] === 'undefined') {
540 this.eventHandlers[eventName] = [];
541 }
001fc061 542
3095c3f6 543 this.eventHandlers[eventName].push(handler);
001fc061 544
3095c3f6 545 return this;
001fc061
AN
546 }
547
3095c3f6
RW
548 /**
549 * Process listeners for the step being shown.
550 *
551 * @method processStepListeners
552 * @param {object} stepConfig The configuration for the step
553 * @chainable
554 * @return {Object} this.
555 */
556 processStepListeners(stepConfig) {
557 this.listeners.push(
558 // Next/Previous buttons.
559 {
560 node: this.currentStepNode,
561 args: ['click', '[data-role="next"]', $.proxy(this.next, this)]
562 }, {
563 node: this.currentStepNode,
564 args: ['click', '[data-role="previous"]', $.proxy(this.previous, this)]
565 },
001fc061 566
3095c3f6
RW
567 // Close and end tour buttons.
568 {
569 node: this.currentStepNode,
570 args: ['click', '[data-role="end"]', $.proxy(this.endTour, this)]
571 },
001fc061 572
3095c3f6
RW
573 // Click backdrop and hide tour.
574 {
575 node: $('[data-flexitour="backdrop"]'),
576 args: ['click', $.proxy(this.hide, this)]
577 },
001fc061 578
3095c3f6
RW
579 // Keypresses.
580 {
581 node: $('body'),
582 args: ['keydown', $.proxy(this.handleKeyDown, this)]
583 });
001fc061 584
3095c3f6
RW
585 if (stepConfig.moveOnClick) {
586 var targetNode = this.getStepTarget(stepConfig);
587 this.listeners.push({
588 node: targetNode,
589 args: ['click', $.proxy(function(e) {
590 if ($(e.target).parents('[data-flexitour="container"]').length === 0) {
591 // Ignore clicks when they are in the flexitour.
592 window.setTimeout($.proxy(this.next, this), 500);
593 }
594 }, this)]
595 });
596 }
001fc061 597
7cc18dc2 598 this.listeners.forEach(function(listener) {
3095c3f6 599 listener.node.on.apply(listener.node, listener.args);
001fc061 600 });
001fc061 601
3095c3f6 602 return this;
001fc061
AN
603 }
604
3095c3f6
RW
605 /**
606 * Reset step listeners.
607 *
608 * @method resetStepListeners
609 * @chainable
610 * @return {Object} this.
611 */
612 resetStepListeners() {
613 // Stop listening to all external handlers.
614 if (this.listeners) {
615 this.listeners.forEach(function(listener) {
616 listener.node.off.apply(listener.node, listener.args);
617 });
618 }
619 this.listeners = [];
001fc061 620
3095c3f6
RW
621 return this;
622 }
001fc061 623
3095c3f6
RW
624 /**
625 * The standard step renderer.
626 *
627 * @method renderStep
628 * @param {Object} stepConfig The step configuration of the step
629 * @chainable
630 * @return {Object} this.
631 */
632 renderStep(stepConfig) {
633 // Store the current step configuration for later.
634 this.currentStepConfig = stepConfig;
635 this.setCurrentStepNumber(stepConfig.stepNumber);
636
637 // Fetch the template and convert it to a $ object.
638 let template = $(this.getTemplateContent());
639
640 // Title.
641 template.find('[data-placeholder="title"]')
642 .html(stepConfig.title);
643
644 // Body.
645 template.find('[data-placeholder="body"]')
646 .html(stepConfig.body);
647
648 // Is this the first step?
649 if (this.isFirstStep(stepConfig.stepNumber)) {
3bd2e330 650 template.find('[data-role="previous"]').hide();
3095c3f6
RW
651 } else {
652 template.find('[data-role="previous"]').prop('disabled', false);
653 }
001fc061 654
3095c3f6
RW
655 // Is this the final step?
656 if (this.isLastStep(stepConfig.stepNumber)) {
3bd2e330
LB
657 template.find('[data-role="next"]').hide();
658 template.find('[data-role="end"]').removeClass("btn-secondary").addClass("btn-primary");
3095c3f6
RW
659 } else {
660 template.find('[data-role="next"]').prop('disabled', false);
661 }
001fc061 662
3095c3f6
RW
663 template.find('[data-role="previous"]').attr('role', 'button');
664 template.find('[data-role="next"]').attr('role', 'button');
665 template.find('[data-role="end"]').attr('role', 'button');
001fc061 666
3095c3f6
RW
667 // Replace the template with the updated version.
668 stepConfig.template = template;
001fc061 669
3095c3f6
RW
670 // Add to the page.
671 this.addStepToPage(stepConfig);
001fc061 672
3095c3f6
RW
673 // Process step listeners after adding to the page.
674 // This uses the currentNode.
675 this.processStepListeners(stepConfig);
001fc061 676
3095c3f6
RW
677 return this;
678 }
001fc061 679
3095c3f6
RW
680 /**
681 * Getter for the template content.
682 *
683 * @method getTemplateContent
684 * @return {$}
685 */
686 getTemplateContent() {
687 return $(this.templateContent).clone();
688 }
001fc061 689
3095c3f6
RW
690 /**
691 * Helper to add a step to the page.
692 *
693 * @method addStepToPage
694 * @param {Object} stepConfig The step configuration of the step
695 * @chainable
696 * @return {Object} this.
697 */
698 addStepToPage(stepConfig) {
699 // Create the stepNode from the template data.
700 let currentStepNode = $('<span data-flexitour="container"></span>')
701 .html(stepConfig.template)
702 .hide();
703
704 // The scroll animation occurs on the body or html.
705 let animationTarget = $('body, html')
706 .stop(true, true);
741c2d46 707
3095c3f6
RW
708 if (this.isStepActuallyVisible(stepConfig)) {
709 let targetNode = this.getStepTarget(stepConfig);
741c2d46 710
3095c3f6 711 targetNode.data('flexitour', 'target');
001fc061 712
3095c3f6
RW
713 let zIndex = this.calculateZIndex(targetNode);
714 if (zIndex) {
715 stepConfig.zIndex = zIndex + 1;
716 }
001fc061 717
3095c3f6
RW
718 if (stepConfig.zIndex) {
719 currentStepNode.css('zIndex', stepConfig.zIndex + 1);
720 }
001fc061 721
3095c3f6
RW
722 // Add the backdrop.
723 this.positionBackdrop(stepConfig);
001fc061 724
3095c3f6
RW
725 $(document.body).append(currentStepNode);
726 this.currentStepNode = currentStepNode;
001fc061 727
3095c3f6
RW
728 // Ensure that the step node is positioned.
729 // Some situations mean that the value is not properly calculated without this step.
730 this.currentStepNode.css({
731 top: 0,
732 left: 0,
733 });
001fc061 734
3095c3f6
RW
735 animationTarget
736 .animate({
737 scrollTop: this.calculateScrollTop(stepConfig),
738 }).promise().then(function() {
739 this.positionStep(stepConfig);
740 this.revealStep(stepConfig);
741 return;
742 }.bind(this))
743 .catch(function() {
744 // Silently fail.
745 });
001fc061 746
3095c3f6
RW
747 } else if (stepConfig.orphan) {
748 stepConfig.isOrphan = true;
749
750 // This will be appended to the body instead.
751 stepConfig.attachTo = $('body').first();
752 stepConfig.attachPoint = 'append';
753
754 // Add the backdrop.
755 this.positionBackdrop(stepConfig);
756
757 // This is an orphaned step.
758 currentStepNode.addClass('orphan');
759
760 // It lives in the body.
761 $(document.body).append(currentStepNode);
762 this.currentStepNode = currentStepNode;
763
764 this.currentStepNode.offset(this.calculateStepPositionInPage());
765 this.currentStepNode.css('position', 'fixed');
766
767 this.currentStepPopper = new Popper(
768 $('body'),
769 this.currentStepNode[0], {
770 removeOnDestroy: true,
771 placement: stepConfig.placement + '-start',
772 arrowElement: '[data-role="arrow"]',
773 // Empty the modifiers. We've already placed the step and don't want it moved.
774 modifiers: {
775 hide: {
776 enabled: false,
777 },
778 applyStyle: {
779 onLoad: null,
780 enabled: false,
781 },
782 }
783 }
784 );
001fc061 785
3095c3f6
RW
786 this.revealStep(stepConfig);
787 }
001fc061 788
3095c3f6
RW
789 return this;
790 }
001fc061 791
3095c3f6
RW
792 /**
793 * Make the given step visible.
794 *
795 * @method revealStep
796 * @param {Object} stepConfig The step configuration of the step
797 * @chainable
798 * @return {Object} this.
799 */
800 revealStep(stepConfig) {
801 // Fade the step in.
802 this.currentStepNode.fadeIn('', $.proxy(function() {
803 // Announce via ARIA.
804 this.announceStep(stepConfig);
805
806 // Focus on the current step Node.
807 this.currentStepNode.focus();
808 window.setTimeout($.proxy(function() {
809 // After a brief delay, focus again.
810 // There seems to be an issue with Jaws where it only reads the dialogue title initially.
811 // This second focus helps it to read the full dialogue.
812 if (this.currentStepNode) {
813 this.currentStepNode.focus();
814 }
815 }, this), 100);
001fc061 816
3095c3f6 817 }, this));
001fc061 818
3095c3f6 819 return this;
001fc061
AN
820 }
821
3095c3f6
RW
822 /**
823 * Helper to announce the step on the page.
824 *
825 * @method announceStep
826 * @param {Object} stepConfig The step configuration of the step
827 * @chainable
828 * @return {Object} this.
829 */
830 announceStep(stepConfig) {
831 // Setup the step Dialogue as per:
832 // * https://www.w3.org/TR/wai-aria-practices/#dialog_nonmodal
833 // * https://www.w3.org/TR/wai-aria-practices/#dialog_modal
834
835 // Generate an ID for the current step node.
836 let stepId = 'tour-step-' + this.tourName + '-' + stepConfig.stepNumber;
837 this.currentStepNode.attr('id', stepId);
838
839 let bodyRegion = this.currentStepNode.find('[data-placeholder="body"]').first();
840 bodyRegion.attr('id', stepId + '-body');
841 bodyRegion.attr('role', 'document');
842
843 let headerRegion = this.currentStepNode.find('[data-placeholder="title"]').first();
844 headerRegion.attr('id', stepId + '-title');
845 headerRegion.attr('aria-labelledby', stepId + '-body');
846
847 // Generally, a modal dialog has a role of dialog.
848 this.currentStepNode.attr('role', 'dialog');
849 this.currentStepNode.attr('tabindex', 0);
850 this.currentStepNode.attr('aria-labelledby', stepId + '-title');
851 this.currentStepNode.attr('aria-describedby', stepId + '-body');
852
853 // Configure ARIA attributes on the target.
854 let target = this.getStepTarget(stepConfig);
855 if (target) {
856 if (!target.attr('tabindex')) {
857 target.attr('tabindex', 0);
001fc061 858 }
001fc061 859
3095c3f6
RW
860 target
861 .data('original-describedby', target.attr('aria-describedby'))
862 .attr('aria-describedby', stepId + '-body')
863 ;
001fc061
AN
864 }
865
3095c3f6 866 this.accessibilityShow(stepConfig);
001fc061 867
3095c3f6
RW
868 return this;
869 }
741c2d46 870
3095c3f6
RW
871 /**
872 * Handle key down events.
873 *
874 * @method handleKeyDown
875 * @param {EventFacade} e
876 */
877 handleKeyDown(e) {
878 let tabbableSelector = 'a[href], link[href], [draggable=true], [contenteditable=true], ';
879 tabbableSelector += ':input:enabled, [tabindex], button:enabled';
880 switch (e.keyCode) {
881 case 27:
882 this.endTour();
883 break;
884
885 // 9 == Tab - trap focus for items with a backdrop.
886 case 9:
887 // Tab must be handled on key up only in this instance.
888 (function() {
889 if (!this.currentStepConfig.hasBackdrop) {
890 // Trapping tab focus is only handled for those steps with a backdrop.
891 return;
892 }
001fc061 893
3095c3f6
RW
894 // Find all tabbable locations.
895 let activeElement = $(document.activeElement);
896 let stepTarget = this.getStepTarget(this.currentStepConfig);
897 let tabbableNodes = $(tabbableSelector);
898 let dialogContainer = $('span[data-flexitour="container"]');
899 let currentIndex;
900 // Filter out element which is not belong to target section or dialogue.
901 if (stepTarget) {
902 tabbableNodes = tabbableNodes.filter(function(index, element) {
903 return stepTarget !== null
904 && (stepTarget.has(element).length
905 || dialogContainer.has(element).length
906 || stepTarget.is(element)
907 || dialogContainer.is(element));
908 });
909 }
001fc061 910
3095c3f6
RW
911 // Find index of focusing element.
912 tabbableNodes.each(function(index, element) {
913 if (activeElement.is(element)) {
914 currentIndex = index;
915 return false;
916 }
917 // Keep looping.
918 return true;
48a9fd89 919 });
48a9fd89 920
3095c3f6
RW
921 let nextIndex;
922 let nextNode;
923 let focusRelevant;
924 if (currentIndex != void 0) {
925 let direction = 1;
926 if (e.shiftKey) {
927 direction = -1;
928 }
929 nextIndex = currentIndex;
930 do {
931 nextIndex += direction;
932 nextNode = $(tabbableNodes[nextIndex]);
933 } while (nextNode.length && nextNode.is(':disabled') || nextNode.is(':hidden'));
934 if (nextNode.length) {
935 // A new f
936 focusRelevant = nextNode.closest(stepTarget).length;
937 focusRelevant = focusRelevant || nextNode.closest(this.currentStepNode).length;
938 } else {
939 // Unable to find the target somehow.
940 focusRelevant = false;
941 }
001fc061 942 }
001fc061 943
3095c3f6
RW
944 if (focusRelevant) {
945 nextNode.focus();
001fc061 946 } else {
3095c3f6
RW
947 if (e.shiftKey) {
948 // Focus on the last tabbable node in the step.
949 this.currentStepNode.find(tabbableSelector).last().focus();
001fc061 950 } else {
3095c3f6
RW
951 if (this.currentStepConfig.isOrphan) {
952 // Focus on the step - there is no target.
953 this.currentStepNode.focus();
954 } else {
955 // Focus on the step target.
956 stepTarget.focus();
957 }
001fc061
AN
958 }
959 }
3095c3f6
RW
960 e.preventDefault();
961 }).call(this);
962 break;
963 }
001fc061 964 }
001fc061 965
3095c3f6
RW
966 /**
967 * Start the current tour.
968 *
969 * @method startTour
970 * @param {Integer} startAt Which step number to start at. If not specified, starts at the last point.
971 * @chainable
972 * @return {Object} this.
973 */
974 startTour(startAt) {
975 if (this.storage && typeof startAt === 'undefined') {
976 let storageStartValue = this.storage.getItem(this.storageKey);
977 if (storageStartValue) {
978 let storageStartAt = parseInt(storageStartValue, 10);
979 if (storageStartAt <= this.steps.length) {
980 startAt = storageStartAt;
981 }
aabebc75
AN
982 }
983 }
aabebc75 984
3095c3f6
RW
985 if (typeof startAt === 'undefined') {
986 startAt = this.getCurrentStepNumber();
987 }
001fc061 988
3095c3f6
RW
989 this.fireEventHandlers('beforeStart', startAt);
990 this.gotoStep(startAt);
991 this.fireEventHandlers('afterStart', startAt);
001fc061 992
3095c3f6
RW
993 return this;
994 }
001fc061 995
3095c3f6
RW
996 /**
997 * Restart the tour from the beginning, resetting the completionlag.
998 *
999 * @method restartTour
1000 * @chainable
1001 * @return {Object} this.
1002 */
1003 restartTour() {
1004 return this.startTour(0);
1005 }
001fc061 1006
3095c3f6
RW
1007 /**
1008 * End the current tour.
1009 *
1010 * @method endTour
1011 * @chainable
1012 * @return {Object} this.
1013 */
1014 endTour() {
1015 this.fireEventHandlers('beforeEnd');
1016
1017 if (this.currentStepConfig) {
1018 let previousTarget = this.getStepTarget(this.currentStepConfig);
1019 if (previousTarget) {
1020 if (!previousTarget.attr('tabindex')) {
1021 previousTarget.attr('tabindex', '-1');
1022 }
1023 previousTarget.focus();
001fc061 1024 }
001fc061 1025 }
001fc061 1026
3095c3f6 1027 this.hide(true);
001fc061 1028
3095c3f6 1029 this.fireEventHandlers('afterEnd');
001fc061 1030
3095c3f6 1031 return this;
001fc061
AN
1032 }
1033
3095c3f6
RW
1034 /**
1035 * Hide any currently visible steps.
1036 *
1037 * @method hide
1038 * @param {Bool} transition Animate the visibility change
1039 * @chainable
1040 * @return {Object} this.
1041 */
1042 hide(transition) {
1043 this.fireEventHandlers('beforeHide');
1044
1045 if (this.currentStepNode && this.currentStepNode.length) {
1046 this.currentStepNode.hide();
1047 if (this.currentStepPopper) {
1048 this.currentStepPopper.destroy();
001fc061
AN
1049 }
1050 }
1051
3095c3f6
RW
1052 // Restore original target configuration.
1053 if (this.currentStepConfig) {
1054 let target = this.getStepTarget(this.currentStepConfig);
1055 if (target) {
1056 if (target.data('original-labelledby')) {
1057 target.attr('aria-labelledby', target.data('original-labelledby'));
1058 }
001fc061 1059
3095c3f6
RW
1060 if (target.data('original-describedby')) {
1061 target.attr('aria-describedby', target.data('original-describedby'));
1062 }
1063
1064 if (target.data('original-tabindex')) {
1065 target.attr('tabindex', target.data('tabindex'));
1066 }
1067 }
001fc061 1068
3095c3f6
RW
1069 // Clear the step configuration.
1070 this.currentStepConfig = null;
48a9fd89 1071 }
48a9fd89 1072
3095c3f6
RW
1073 let fadeTime = 0;
1074 if (transition) {
1075 fadeTime = 400;
1076 }
001fc061 1077
3095c3f6
RW
1078 // Remove the backdrop features.
1079 $('[data-flexitour="step-background"]').remove();
1080 $('[data-flexitour="step-backdrop"]').removeAttr('data-flexitour');
1081 $('[data-flexitour="backdrop"]').fadeOut(fadeTime, function() {
1082 $(this).remove();
1083 });
741c2d46 1084
3095c3f6
RW
1085 // Remove aria-describedby and tabindex attributes.
1086 if (this.currentStepNode && this.currentStepNode.length) {
1087 let stepId = this.currentStepNode.attr('id');
1088 if (stepId) {
1089 let currentStepElement = '[aria-describedby="' + stepId + '-body"]';
1090 $(currentStepElement).removeAttr('tabindex');
1091 $(currentStepElement).removeAttr('aria-describedby');
1092 }
1093 }
001fc061 1094
3095c3f6
RW
1095 // Reset the listeners.
1096 this.resetStepListeners();
001fc061 1097
3095c3f6 1098 this.accessibilityHide();
001fc061 1099
3095c3f6 1100 this.fireEventHandlers('afterHide');
001fc061 1101
3095c3f6
RW
1102 this.currentStepNode = null;
1103 this.currentStepPopper = null;
1104 return this;
001fc061
AN
1105 }
1106
3095c3f6
RW
1107 /**
1108 * Show the current steps.
1109 *
1110 * @method show
1111 * @chainable
1112 * @return {Object} this.
1113 */
1114 show() {
1115 // Show the current step.
1116 let startAt = this.getCurrentStepNumber();
1117
1118 return this.gotoStep(startAt);
1119 }
001fc061 1120
3095c3f6
RW
1121 /**
1122 * Return the current step node.
1123 *
1124 * @method getStepContainer
1125 * @return {jQuery}
1126 */
1127 getStepContainer() {
1128 return $(this.currentStepNode);
1129 }
001fc061 1130
3095c3f6
RW
1131 /**
1132 * Calculate scrollTop.
1133 *
1134 * @method calculateScrollTop
1135 * @param {Object} stepConfig The step configuration of the step
1136 * @return {Number}
1137 */
1138 calculateScrollTop(stepConfig) {
1139 let scrollTop = $(window).scrollTop();
1140 let viewportHeight = $(window).height();
1141 let targetNode = this.getStepTarget(stepConfig);
1142
1143 if (stepConfig.placement === 'top') {
1144 // If the placement is top, center scroll at the top of the target.
1145 scrollTop = targetNode.offset().top - (viewportHeight / 2);
1146 } else if (stepConfig.placement === 'bottom') {
1147 // If the placement is bottom, center scroll at the bottom of the target.
1148 scrollTop = targetNode.offset().top + targetNode.height() - (viewportHeight / 2);
1149 } else if (targetNode.height() <= (viewportHeight * 0.8)) {
1150 // If the placement is left/right, and the target fits in the viewport, centre screen on the target
1151 scrollTop = targetNode.offset().top - ((viewportHeight - targetNode.height()) / 2);
1152 } else {
1153 // If the placement is left/right, and the target is bigger than the viewport, set scrollTop to target.top + buffer
1154 // and change step attachmentTarget to top+.
1155 scrollTop = targetNode.offset().top - (viewportHeight * 0.2);
1156 }
001fc061 1157
3095c3f6
RW
1158 // Never scroll over the top.
1159 scrollTop = Math.max(0, scrollTop);
001fc061 1160
3095c3f6
RW
1161 // Never scroll beyond the bottom.
1162 scrollTop = Math.min($(document).height() - viewportHeight, scrollTop);
001fc061 1163
3095c3f6 1164 return Math.ceil(scrollTop);
001fc061
AN
1165 }
1166
3095c3f6
RW
1167 /**
1168 * Calculate dialogue position for page middle.
1169 *
1170 * @method calculateScrollTop
1171 * @return {Number}
1172 */
1173 calculateStepPositionInPage() {
1174 let viewportHeight = $(window).height();
1175 let stepHeight = this.currentStepNode.height();
1176
1177 let viewportWidth = $(window).width();
1178 let stepWidth = this.currentStepNode.width();
1179
1180 return {
1181 top: Math.ceil((viewportHeight - stepHeight) / 2),
1182 left: Math.ceil((viewportWidth - stepWidth) / 2)
1183 };
001fc061
AN
1184 }
1185
3095c3f6
RW
1186 /**
1187 * Position the step on the page.
1188 *
1189 * @method positionStep
1190 * @param {Object} stepConfig The step configuration of the step
1191 * @chainable
1192 * @return {Object} this.
1193 */
1194 positionStep(stepConfig) {
1195 let content = this.currentStepNode;
1196 if (!content || !content.length) {
1197 // Unable to find the step node.
1198 return this;
0c2ecefc 1199 }
3095c3f6
RW
1200
1201 let flipBehavior;
1202 switch (stepConfig.placement) {
1203 case 'left':
1204 flipBehavior = ['left', 'right', 'top', 'bottom'];
1205 break;
1206 case 'right':
1207 flipBehavior = ['right', 'left', 'top', 'bottom'];
1208 break;
1209 case 'top':
1210 flipBehavior = ['top', 'bottom', 'right', 'left'];
1211 break;
1212 case 'bottom':
1213 flipBehavior = ['bottom', 'top', 'right', 'left'];
1214 break;
1215 default:
1216 flipBehavior = 'flip';
1217 break;
1218 }
1219
1220 let target = this.getStepTarget(stepConfig);
1221 var config = {
1222 placement: stepConfig.placement + '-start',
1223 removeOnDestroy: true,
1224 modifiers: {
1225 flip: {
1226 behaviour: flipBehavior,
1227 },
1228 arrow: {
1229 element: '[data-role="arrow"]',
1230 },
1231 },
1232 onCreate: function(data) {
1233 recalculateArrowPosition(data);
1234 },
1235 onUpdate: function(data) {
1236 recalculateArrowPosition(data);
1237 },
1238 };
1239
1240 let recalculateArrowPosition = function(data) {
1241 let placement = data.placement.split('-')[0];
1242 const isVertical = ['left', 'right'].indexOf(placement) !== -1;
1243 const arrowElement = data.instance.popper.querySelector('[data-role="arrow"]');
1244 const stepElement = $(data.instance.popper.querySelector('[data-role="flexitour-step"]'));
1245 if (isVertical) {
1246 let arrowHeight = parseFloat(window.getComputedStyle(arrowElement).height);
1247 let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).top);
1248 let popperHeight = parseFloat(window.getComputedStyle(data.instance.popper).height);
1249 let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).top);
1250 let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));
1251 let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;
1252 let arrowPos = arrowOffset + (arrowHeight / 2);
1253 let maxPos = popperHeight + popperOffset - popperBorderWidth - popperBorderRadiusWidth;
1254 let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;
1255 if (arrowPos >= maxPos || arrowPos <= minPos) {
1256 let newArrowPos = 0;
1257 if (arrowPos > (popperHeight / 2)) {
1258 newArrowPos = maxPos - arrowHeight;
1259 } else {
1260 newArrowPos = minPos + arrowHeight;
1261 }
1262 $(arrowElement).css('top', newArrowPos);
0c2ecefc 1263 }
3095c3f6
RW
1264 } else {
1265 let arrowWidth = parseFloat(window.getComputedStyle(arrowElement).width);
1266 let arrowOffset = parseFloat(window.getComputedStyle(arrowElement).left);
1267 let popperWidth = parseFloat(window.getComputedStyle(data.instance.popper).width);
1268 let popperOffset = parseFloat(window.getComputedStyle(data.instance.popper).left);
1269 let popperBorderWidth = parseFloat(stepElement.css('borderTopWidth'));
1270 let popperBorderRadiusWidth = parseFloat(stepElement.css('borderTopLeftRadius')) * 2;
1271 let arrowPos = arrowOffset + (arrowWidth / 2);
1272 let maxPos = popperWidth + popperOffset - popperBorderWidth - popperBorderRadiusWidth;
1273 let minPos = popperOffset + popperBorderWidth + popperBorderRadiusWidth;
1274 if (arrowPos >= maxPos || arrowPos <= minPos) {
1275 let newArrowPos = 0;
1276 if (arrowPos > (popperWidth / 2)) {
1277 newArrowPos = maxPos - arrowWidth;
1278 } else {
1279 newArrowPos = minPos + arrowWidth;
1280 }
1281 $(arrowElement).css('left', newArrowPos);
0c2ecefc 1282 }
0c2ecefc 1283 }
3095c3f6
RW
1284 };
1285
1286 let background = $('[data-flexitour="step-background"]');
1287 if (background.length) {
1288 target = background;
8385163b 1289 }
3095c3f6 1290 this.currentStepPopper = new Popper(target, content[0], config);
8385163b 1291
3095c3f6 1292 return this;
001fc061 1293 }
001fc061 1294
3095c3f6
RW
1295 /**
1296 * Add the backdrop.
1297 *
1298 * @method positionBackdrop
1299 * @param {Object} stepConfig The step configuration of the step
1300 * @chainable
1301 * @return {Object} this.
1302 */
1303 positionBackdrop(stepConfig) {
1304 if (stepConfig.backdrop) {
1305 this.currentStepConfig.hasBackdrop = true;
1306 let backdrop = $('<div data-flexitour="backdrop"></div>');
001fc061 1307
3095c3f6
RW
1308 if (stepConfig.zIndex) {
1309 if (stepConfig.attachPoint === 'append') {
1310 stepConfig.attachTo.append(backdrop);
1311 } else {
1312 backdrop.insertAfter(stepConfig.attachTo);
1313 }
001fc061 1314 } else {
3095c3f6 1315 $('body').append(backdrop);
001fc061 1316 }
001fc061 1317
3095c3f6
RW
1318 if (this.isStepActuallyVisible(stepConfig)) {
1319 // The step has a visible target.
1320 // Punch a hole through the backdrop.
1321 let background = $('<div data-flexitour="step-background"></div>');
001fc061 1322
3095c3f6 1323 let targetNode = this.getStepTarget(stepConfig);
001fc061 1324
3095c3f6 1325 let buffer = 10;
001fc061 1326
3095c3f6
RW
1327 let colorNode = targetNode;
1328 if (buffer) {
1329 colorNode = $('body');
1330 }
001fc061 1331
001fc061 1332 background.css({
3095c3f6
RW
1333 width: targetNode.outerWidth() + buffer + buffer,
1334 height: targetNode.outerHeight() + buffer + buffer,
1335 left: targetNode.offset().left - buffer,
1336 top: targetNode.offset().top - buffer,
1337 backgroundColor: this.calculateInherittedBackgroundColor(colorNode),
001fc061 1338 });
001fc061 1339
3095c3f6
RW
1340 if (targetNode.offset().left < buffer) {
1341 background.css({
1342 width: targetNode.outerWidth() + targetNode.offset().left + buffer,
1343 left: targetNode.offset().left,
1344 });
1345 }
001fc061 1346
3095c3f6
RW
1347 if (targetNode.offset().top < buffer) {
1348 background.css({
1349 height: targetNode.outerHeight() + targetNode.offset().top + buffer,
1350 top: targetNode.offset().top,
1351 });
1352 }
001fc061 1353
3095c3f6
RW
1354 let targetRadius = targetNode.css('borderRadius');
1355 if (targetRadius && targetRadius !== $('body').css('borderRadius')) {
1356 background.css('borderRadius', targetRadius);
1357 }
001fc061 1358
3095c3f6
RW
1359 let targetPosition = this.calculatePosition(targetNode);
1360 if (targetPosition === 'fixed') {
1361 background.css('top', 0);
1362 } else if (targetPosition === 'absolute') {
1363 background.css('position', 'fixed');
1364 }
001fc061 1365
3095c3f6
RW
1366 let fader = background.clone();
1367 fader.css({
1368 backgroundColor: backdrop.css('backgroundColor'),
1369 opacity: backdrop.css('opacity'),
1370 });
1371 fader.attr('data-flexitour', 'step-background-fader');
1372
1373 if (stepConfig.zIndex) {
1374 if (stepConfig.attachPoint === 'append') {
1375 stepConfig.attachTo.append(background);
1376 } else {
1377 fader.insertAfter(stepConfig.attachTo);
1378 background.insertAfter(stepConfig.attachTo);
1379 }
001fc061 1380 } else {
3095c3f6
RW
1381 $('body').append(fader);
1382 $('body').append(background);
001fc061 1383 }
001fc061 1384
3095c3f6
RW
1385 // Add the backdrop data to the actual target.
1386 // This is the part which actually does the work.
1387 targetNode.attr('data-flexitour', 'step-backdrop');
001fc061 1388
3095c3f6
RW
1389 if (stepConfig.zIndex) {
1390 backdrop.css('zIndex', stepConfig.zIndex);
1391 background.css('zIndex', stepConfig.zIndex + 1);
1392 targetNode.css('zIndex', stepConfig.zIndex + 2);
1393 }
001fc061 1394
3095c3f6
RW
1395 fader.fadeOut('2000', function() {
1396 $(this).remove();
1397 });
1398 }
001fc061 1399 }
3095c3f6 1400 return this;
001fc061 1401 }
001fc061 1402
3095c3f6
RW
1403 /**
1404 * Calculate the inheritted z-index.
1405 *
1406 * @method calculateZIndex
1407 * @param {jQuery} elem The element to calculate z-index for
1408 * @return {Number} Calculated z-index
1409 */
1410 calculateZIndex(elem) {
1411 elem = $(elem);
1412 while (elem.length && elem[0] !== document) {
1413 // Ignore z-index if position is set to a value where z-index is ignored by the browser
1414 // This makes behavior of this function consistent across browsers
1415 // WebKit always returns auto if the element is positioned.
1416 let position = elem.css("position");
1417 if (position === "absolute" || position === "relative" || position === "fixed") {
1418 // IE returns 0 when zIndex is not specified
1419 // other browsers return a string
1420 // we ignore the case of nested elements with an explicit value of 0
1421 // <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
1422 let value = parseInt(elem.css("zIndex"), 10);
1423 if (!isNaN(value) && value !== 0) {
1424 return value;
1425 }
001fc061 1426 }
3095c3f6 1427 elem = elem.parent();
001fc061 1428 }
001fc061 1429
3095c3f6 1430 return 0;
001fc061
AN
1431 }
1432
3095c3f6
RW
1433 /**
1434 * Calculate the inheritted background colour.
1435 *
1436 * @method calculateInherittedBackgroundColor
1437 * @param {jQuery} elem The element to calculate colour for
1438 * @return {String} Calculated background colour
1439 */
1440 calculateInherittedBackgroundColor(elem) {
1441 // Use a fake node to compare each element against.
1442 let fakeNode = $('<div>').hide();
1443 $('body').append(fakeNode);
1444 let fakeElemColor = fakeNode.css('backgroundColor');
1445 fakeNode.remove();
1446
1447 elem = $(elem);
1448 while (elem.length && elem[0] !== document) {
1449 let color = elem.css('backgroundColor');
1450 if (color !== fakeElemColor) {
1451 return color;
1452 }
1453 elem = elem.parent();
001fc061 1454 }
001fc061 1455
3095c3f6
RW
1456 return null;
1457 }
001fc061 1458
3095c3f6
RW
1459 /**
1460 * Calculate the inheritted position.
1461 *
1462 * @method calculatePosition
1463 * @param {jQuery} elem The element to calculate position for
1464 * @return {String} Calculated position
1465 */
1466 calculatePosition(elem) {
1467 elem = $(elem);
1468 while (elem.length && elem[0] !== document) {
1469 let position = elem.css('position');
1470 if (position !== 'static') {
1471 return position;
741c2d46 1472 }
3095c3f6 1473 elem = elem.parent();
741c2d46
AN
1474 }
1475
3095c3f6
RW
1476 return null;
1477 }
741c2d46 1478
3095c3f6
RW
1479 /**
1480 * Perform accessibility changes for step shown.
1481 *
1482 * This will add aria-hidden="true" to all siblings and parent siblings.
1483 *
1484 * @method accessibilityShow
1485 */
1486 accessibilityShow() {
1487 let stateHolder = 'data-has-hidden';
1488 let attrName = 'aria-hidden';
1489 let hideFunction = function(child) {
1490 let flexitourRole = child.data('flexitour');
1491 if (flexitourRole) {
1492 switch (flexitourRole) {
1493 case 'container':
1494 case 'target':
1495 return;
1496 }
1497 }
741c2d46 1498
3095c3f6
RW
1499 let hidden = child.attr(attrName);
1500 if (!hidden) {
1501 child.attr(stateHolder, true);
7c56d558 1502 Aria.hide(child);
3095c3f6
RW
1503 }
1504 };
741c2d46 1505
3095c3f6
RW
1506 this.currentStepNode.siblings().each(function(index, node) {
1507 hideFunction($(node));
1508 });
1509 this.currentStepNode.parentsUntil('body').siblings().each(function(index, node) {
1510 hideFunction($(node));
1511 });
1512 }
001fc061 1513
3095c3f6
RW
1514 /**
1515 * Perform accessibility changes for step hidden.
1516 *
1517 * This will remove any newly added aria-hidden="true".
1518 *
1519 * @method accessibilityHide
1520 */
1521 accessibilityHide() {
1522 let stateHolder = 'data-has-hidden';
3095c3f6
RW
1523 let showFunction = function(child) {
1524 let hidden = child.attr(stateHolder);
1525 if (typeof hidden !== 'undefined') {
1526 child.removeAttr(stateHolder);
7c56d558 1527 Aria.unhide(child);
3095c3f6
RW
1528 }
1529 };
001fc061 1530
3095c3f6
RW
1531 $('[' + stateHolder + ']').each(function(index, node) {
1532 showFunction($(node));
1533 });
1534 }
1535}