Skip to content

Commit

Permalink
Integrate modal functionality that was originally in Ember Shepherd (#…
Browse files Browse the repository at this point in the history
…301)

* integrate modal functionality that was originally in Ember Shepherd

* Fix: stop ontouchstart events on overlay (#302)

* fix: stop pull on overlap

* fix: selector for overlay

* update listener calls to match previous utils

* Add modal and highlight tests

* Bump some deps
  • Loading branch information
BrianSipple authored and RobbieTheWagner committed Dec 31, 2018
1 parent 445a514 commit f1c4eeb
Show file tree
Hide file tree
Showing 18 changed files with 1,187 additions and 197 deletions.
55 changes: 37 additions & 18 deletions docs/welcome/css/welcome.css

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/welcome/js/welcome.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
const shepherd = new Shepherd.Tour({
defaultStepOptions: {
showCancelLink: true
}
},
useModalOverlay: true
});

shepherd.addStep('welcome', {
Expand Down
16 changes: 0 additions & 16 deletions docs/welcome/scss/welcome.scss
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,6 @@ body {
h1 {
padding-top: 10px;
}

> * {
opacity: 0.3;
pointer-events: none;
transition: opacity 0.4s;

body:not(.shepherd-active) & {
opacity: 1;
pointer-events: auto;
}
}

.shepherd-target.shepherd-enabled {
opacity: 1;
pointer-events: auto;
}
}

.hero-followup {
Expand Down
2 changes: 2 additions & 0 deletions index.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const myTour = new Shepherd.Tour(options);
- `defaultStepOptions`: Default options for Steps created through `addStep`
- `tourName`: An optional "name" for the tour. This will be appended to the the tour's
dynamically generated `id` property -- which is also set on the `body` element as the `data-shepherd-active-tour` attribute whenever the tour becomes active.
- `useModalOverlay`: Whether or not steps should be placed above a darkened modal overlay. If true, the overlay will create an opening around the target element so that it can remain interactive.
- `confirmCancel`: If true, will issue a window.confirm before cancelling
- `confirmCancelMessage`: The message to display in the confirm dialog

Expand Down Expand Up @@ -199,6 +200,7 @@ the step will execute. For example:
- `advanceOn`: An action on the page which should advance shepherd to the next step. It can be of the form `"selector event"`, or an object with those
properties. For example: `".some-element click"`, or `{selector: '.some-element', event: 'click'}`. It doesn't have to be an event inside
the tour, it can be any event fired on any element on the page. You can also always manually advance the Tour by calling `myTour.next()`.
- `highlightClass`: An extra class to apply to the `attachTo` element when it is highlighted (that is, when its step is active). You can then target that selector in your CSS.
- `showCancelLink`: Should a cancel "✕" be shown in the header of the step?
- `showOn`: A function that, when it returns true, will show the step. If it returns false, the step will be skipped.
- `scrollTo`: Should the element be scrolled to when this step is shown?
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@
"chai-as-promised": "^7.1.1",
"codeclimate-test-reporter": "^0.5.1",
"cross-env": "^5.2.0",
"css-loader": "^2.0.2",
"cypress": "^3.1.3",
"css-loader": "^2.1.0",
"cypress": "^3.1.4",
"del": "^3.0.0",
"esdoc": "^1.1.0",
"esdoc-ecmascript-proposal-plugin": "^1.0.0",
"esdoc-standard-plugin": "^1.0.0",
"eslint": "^5.10.0",
"eslint": "^5.11.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-mocha": "^5.1.0",
"eslint-plugin-ship-shape": "^0.6.0",
Expand All @@ -93,17 +93,17 @@
"postcss-loader": "^3.0.0",
"replace": "^1.0.1",
"sass-loader": "^7.1.0",
"sinon": "^7.1.1",
"sinon": "^7.2.2",
"source-map-loader": "^0.2.3",
"start-server-and-test": "^1.7.11",
"style-loader": "^0.23.1",
"stylelint": "^8.4.0",
"stylelint-config-ship-shape": "^0.4.0",
"stylelint-webpack-plugin": "^0.10.5",
"uglifyjs-webpack-plugin": "^2.0.1",
"webpack": "^4.27.0",
"uglifyjs-webpack-plugin": "^2.1.1",
"webpack": "^4.28.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.11"
"webpack-dev-server": "^3.1.14"
},
"engines": {
"node": ">= 6.*"
Expand Down
15 changes: 13 additions & 2 deletions src/js/step.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export class Step extends Evented {
* ```
* @param {string} options.buttons.button.text The HTML text of the button
* @param {string} options.classes A string of extra classes to add to the step's content element.
* @param {string} options.highlightClass An extra class to apply to the `attachTo` element when it is
* highlighted (that is, when its step is active). You can then target that selector in your CSS.
* @param {Object} options.tippyOptions Extra [options to pass to tippy.js]{@link https://atomiks.github.io/tippyjs/#all-options}
* @param {boolean} options.scrollTo Should the element be scrolled to when this step is shown?
* @param {function} options.scrollToHandler A function that lets you override the default scrollTo behavior and
Expand Down Expand Up @@ -258,7 +260,7 @@ export class Step extends Evented {
}

if (this.target) {
this.target.classList.remove('shepherd-enabled', 'shepherd-target');
this._updateStepTargetOnHide();
}

this.trigger('destroy');
Expand All @@ -273,7 +275,7 @@ export class Step extends Evented {
document.body.removeAttribute('data-shepherd-step');

if (this.target) {
this.target.classList.remove('shepherd-enabled', 'shepherd-target');
this._updateStepTargetOnHide();
}

if (this.tooltip) {
Expand Down Expand Up @@ -362,6 +364,7 @@ export class Step extends Evented {
* @private
*/
_show() {
this.tour.beforeShowStep(this);
this.trigger('before-show');

if (!this.el) {
Expand All @@ -381,4 +384,12 @@ export class Step extends Evented {
this.tooltip.show();
this.trigger('show');
}

_updateStepTargetOnHide() {
if (this.options.highlightClass) {
this.target.classList.remove(this.options.highlightClass);
}

this.target.classList.remove('shepherd-enabled', 'shepherd-target');
}
}
136 changes: 134 additions & 2 deletions src/js/tour.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import { isFunction, isNumber, isString, isUndefined, isEmpty } from 'lodash';
import { isFunction, isNumber, isString, isUndefined, isEmpty, debounce } from 'lodash';
import { Evented } from './evented.js';
import { Step } from './step.js';
import { bindMethods } from './bind.js';
import tippy from 'tippy.js';
import { defaults as tooltipDefaults } from './utils/tooltip-defaults';

import {
cleanupModal,
cleanupSteps
} from './utils/cleanup';

import {
addStepEventListeners,
cleanupStepEventListeners,
getElementForStep
} from './utils/dom';

import {
getModalMaskOpening,
createModalOverlay,
positionModalOpening,
closeModalOpening,
classNames as modalClassNames,
toggleShepherdModalClass
} from './utils/modal';

/**
* Creates incremented ID for each newly created tour
*
Expand All @@ -26,13 +46,15 @@ const Shepherd = new Evented();
*/
export class Tour extends Evented {
/**
*
* @param {Object} options The options for the tour
* @param {Object} options.defaultStepOptions Default options for Steps created through `addStep`
* @param {Step[]} options.steps An array of Step instances to initialize the tour with
* @param {string} options.tourName An optional "name" for the tour. This will be appended to the the tour's
* dynamically generated `id` property -- which is also set on the `body` element as the `data-shepherd-active-tour` attribute
* whenever the tour becomes active.
* @param {boolean} options.useModalOverlay Whether or not steps should be placed above a darkened
* modal overlay. If true, the overlay will create an opening around the target element so that it
* can remain interactive
* @returns {Tour}
*/
constructor(options = {}) {
Expand Down Expand Up @@ -133,6 +155,10 @@ export class Tour extends Evented {
this.steps.forEach((step) => step.destroy());
}

cleanupStepEventListeners.call(this);
cleanupSteps(this.tourObject);
cleanupModal.call(this);

this.trigger(event);

Shepherd.activeTour = null;
Expand Down Expand Up @@ -233,6 +259,11 @@ export class Tour extends Evented {
return new Step(this, stepOptions);
}

beforeShowStep(step) {
this._setupModalForStep(step);
this._styleTargetElementForStep(step);
}

/**
* Show a specific step in the tour
* @param {Number|String} key The key to look up the step by
Expand Down Expand Up @@ -269,6 +300,8 @@ export class Tour extends Evented {

this.currentStep = null;
this._setupActiveTour();
this._initModalOverlay();
addStepEventListeners.call(this);
this.next();
}

Expand All @@ -283,6 +316,105 @@ export class Tour extends Evented {
Shepherd.activeTour = this;
}

/**
*
*/
_initModalOverlay() {
if (!this._modalOverlayElem) {
this._modalOverlayElem = createModalOverlay();
this._modalOverlayOpening = getModalMaskOpening(this._modalOverlayElem);

// don't show yet -- each step will control that
this._hideModalOverlay();

document.body.appendChild(this._modalOverlayElem);
}
}

/**
* Modulates the styles of the passed step's target element, based on the step's options and
* the tour's `modal` option, to visually emphasize the element
*
* @param step The step object that attaches to the element
* @private
*/
_styleTargetElementForStep(step) {
const targetElement = getElementForStep(step);

if (!targetElement) {
return;
}

toggleShepherdModalClass(targetElement);

if (step.options.highlightClass) {
targetElement.classList.add(step.options.highlightClass);
}

if (step.options.canClickTarget === false) {
targetElement.style.pointerEvents = 'none';
}
}

/**
* If modal is enabled, setup the svg mask opening and modal overlay for the step
* @param step
* @private
*/
_setupModalForStep(step) {
if (this.options.useModalOverlay) {
this._styleModalOpeningForStep(step);
this._showModalOverlay();

} else {
this._hideModalOverlay();
}
}

_styleModalOpeningForStep(step) {
const modalOverlayOpening = this._modalOverlayOpening;
const targetElement = getElementForStep(step);

if (targetElement) {
positionModalOpening(targetElement, modalOverlayOpening);

this._onScreenChange = debounce(
positionModalOpening.bind(this, targetElement, modalOverlayOpening),
0,
{ leading: false, trailing: true } // see https://lodash.com/docs/#debounce
);

addStepEventListeners.call(this);

} else {
closeModalOpening(this._modalOverlayOpening);
}
}

/**
* Show the modal overlay
* @private
*/
_showModalOverlay() {
document.body.classList.add(modalClassNames.isVisible);

if (this._modalOverlayElem) {
this._modalOverlayElem.style.display = 'block';
}
}

/**
* Hide the modal overlay
* @private
*/
_hideModalOverlay() {
document.body.classList.remove(modalClassNames.isVisible);

if (this._modalOverlayElem) {
this._modalOverlayElem.style.display = 'none';
}
}

/**
* Called when `showOn` evaluates to false, to skip the step
* @param {Step} step The step to skip
Expand Down
52 changes: 52 additions & 0 deletions src/js/utils/cleanup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { defer } from 'lodash';
import { classNames as modalClassNames, preventModalBodyTouch } from './modal';
import { getElementForStep } from './dom';

/**
* Removes svg mask from modal overlay and removes classes for modal being visible
*/
export function cleanupModal() {
defer(() => {
const element = this._modalOverlayElem;

if (element && element instanceof SVGElement) {
element.parentNode.removeChild(element);
}

this._modalOverlayElem = null;
document.body.classList.remove(modalClassNames.isVisible);
});
}

/**
* Cleanup the steps and set pointerEvents back to 'auto'
* @param tour The tour object
*/
export function cleanupSteps(tour) {
if (tour) {
const { steps } = tour;

steps.forEach((step) => {
if (step.options && step.options.canClickTarget === false && step.options.attachTo) {
const stepElement = getElementForStep(step);

if (stepElement instanceof HTMLElement) {
stepElement.style.pointerEvents = 'auto';
}
}
});
}
}

/**
* Remove resize and scroll event listeners
*/
export function cleanupStepEventListeners() {
if (typeof this._onScreenChange === 'function') {
window.removeEventListener('resize', this._onScreenChange, false);
window.removeEventListener('scroll', this._onScreenChange, false);
window.removeEventListener('touchmove', preventModalBodyTouch, false);

this._onScreenChange = null;
}
}
Loading

0 comments on commit f1c4eeb

Please sign in to comment.