Skip to content

Commit

Permalink
Implement SVG-based modal mode
Browse files Browse the repository at this point in the history
  • Loading branch information
BrianSipple committed Sep 20, 2018
1 parent 514a53e commit ff2a1f4
Show file tree
Hide file tree
Showing 10 changed files with 587 additions and 638 deletions.
14 changes: 0 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,6 @@ Thanks to [jquery-disablescroll](https://github.com/ultrapasty/jquery-disablescr

> **default value:** `false`
### modalContainer

`modalContainer` configures where in the DOM the modal overlay element will be placed (only has effect if `modal` is set to `true`)

> **default value:** `body`
### requiredElements

`requiredElements` is an array of objects that indicate DOM elements that are **REQUIRED** by your tour and must
Expand Down Expand Up @@ -161,7 +155,6 @@ this.get('tour').set('steps', [
}
],
classes: 'custom-class-name-1 custom-class-name-2',
copyStyles: false,
highlightClass: 'highlight',
scrollTo: false,
showCancelLink: true,
Expand Down Expand Up @@ -236,13 +229,6 @@ Whether or not the target element being attached to should be "clickable". If se
> **default value:** `true`

##### copyStyles

This is a boolean, and when set to `true` it will fully clone the element and styles, rather than just increasing the element's z-index. This should only be used if the element does not pop out and highlight like it should, when using modal functionality.

> **default value:** `false`

##### highlightClass

This is an extra class to apply to the attachTo element, when it is highlighted. It can be any string. Just style that class name in your css.
Expand Down
228 changes: 122 additions & 106 deletions addon/services/tour.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { get, observer, set } from '@ember/object';
import { isEmpty, isPresent } from '@ember/utils';
import Service from '@ember/service';
import Evented from '@ember/object/evented';
import { run } from '@ember/runloop';
import { run, scheduleOnce } from '@ember/runloop';
import {
elementIsHidden,
getElementForStep,
removeElement,
setPositionForHighlightElement,
toggleShepherdModalClass
} from '../utils';
toggleShepherdModalClass,
} from '../utils/dom';

import {
getModalMaskOpening,
createModalOverlay,
positionModalOpening,
closeModalOpening,
} from '../utils/modal';


export default Service.extend(Evented, {
// Configuration Options
Expand All @@ -22,10 +28,12 @@ export default Service.extend(Evented, {
isActive: false,
messageForUser: null,
modal: false,
modalContainer: 'body',
requiredElements: [],
steps: [],

_modalOverlayElem: null,
_onScreenChange() {},

willDestroy() {
this.cleanup();
},
Expand Down Expand Up @@ -71,15 +79,12 @@ export default Service.extend(Evented, {
},

onTourStart() {
if (get(this, 'modal')) {
const shepherdOverlay = document.createElement('div');
shepherdOverlay.id = 'shepherdOverlay';
const parent = document.querySelector(get(this, 'modalContainer'));
parent.appendChild(shepherdOverlay);
}
this.addStepEventListeners();

if (get(this, 'disableScroll')) {
disableScroll.on(window);
}

this.trigger('start');
},

Expand All @@ -104,67 +109,9 @@ export default Service.extend(Evented, {
disableScroll.off(window);
}

this._cleanupSteps();
this._cleanupModal();
},

/**
* Creates an overlay element clone of the element you want to highlight and copies all the styles.
* @param step The step object that points to the element to highlight
* @private
*/
createHighlightOverlay(step) {
removeElement('#highlightOverlay');

const currentElement = getElementForStep(step);

if (currentElement) {
const highlightElement = currentElement.cloneNode(true);

highlightElement.setAttribute('id', 'highlightOverlay');
document.body.appendChild(highlightElement);

this.setComputedStylesOnClonedElement(currentElement, highlightElement);

// Style all internal elements as well
const { children } = currentElement;

const clonedChildren = highlightElement.children;

for (let i = 0; i < children.length; i++) {
this.setComputedStylesOnClonedElement(children[i], clonedChildren[i]);
}

setPositionForHighlightElement({
currentElement,
highlightElement
});

window.addEventListener('resize', () => {
run.debounce(this, setPositionForHighlightElement, {
currentElement,
highlightElement
}, 50);
});
}
},

/**
* Set computed styles on the cloned element
*
* @method setComputedStylesOnClonedElement
* @param element element we want to copy
* @param clonedElement cloned element above the overlay
* @private
*/
setComputedStylesOnClonedElement(element, clonedElement) {
const computedStyle = window.getComputedStyle(element, null);

for (let i = 0; i < computedStyle.length; i++) {
const propertyName = computedStyle[i];

clonedElement.style[propertyName] = computedStyle.getPropertyValue(propertyName);
}
this.cleanupStepEventListeners();
this.cleanupSteps();
this.cleanupModal();
},

initialize() {
Expand All @@ -181,7 +128,9 @@ export default Service.extend(Evented, {
tourObject.on('start', run.bind(this, 'onTourStart'));
tourObject.on('complete', run.bind(this, 'onTourFinish', 'complete'));
tourObject.on('cancel', run.bind(this, 'onTourFinish', 'cancel'));
set(this, 'tourObject', tourObject);

this.tourObject = tourObject;
this.initModalOverlay();
},

/**
Expand Down Expand Up @@ -231,34 +180,38 @@ export default Service.extend(Evented, {
}
},

setupModalForStep(step) {
if (!this.modal) {
this.hideModal();

} else {
this.styleModalOpeningForStep(step);
this.showModal();
}
},

/**
* 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
*/
styleTargetElement(step) {
const currentElement = getElementForStep(step);
styleTargetElementForStep(step) {
const targetElement = getElementForStep(step);

if (!currentElement) {
if (!targetElement) {
return;
}

toggleShepherdModalClass(targetElement);

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

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

if (this.modal) {
if (step.options.copyStyles) {
this.createHighlightOverlay(step);
} else {
toggleShepherdModalClass(currentElement);
}
targetElement.style.pointerEvents = 'none';
}
},

Expand Down Expand Up @@ -308,7 +261,6 @@ export default Service.extend(Evented, {
text: 'Exit',
action: tour.cancel
}],
copyStyles: false,
title: get(this, 'errorTitle'),
text: [get(this, 'messageForUser')]
});
Expand All @@ -328,18 +280,20 @@ export default Service.extend(Evented, {
const currentStep = tour.steps[index];

currentStep.on('before-show', () => {
this.styleTargetElement(currentStep);
this.setupModalForStep(currentStep);
this.styleTargetElementForStep(currentStep);
});

currentStep.on('hide', () => {
// Remove element copy, if it was cloned
const currentElement = getElementForStep(currentStep);
const targetElement = getElementForStep(currentStep);

if (currentElement) {
if (targetElement) {
if (currentStep.options.highlightClass) {
currentElement.classList.remove(currentStep.options.highlightClass);
targetElement.classList.remove(currentStep.options.highlightClass);
}

removeElement('#highlightOverlay');
closeModalOpening(this._modalOverlayOpening);
}
});

Expand All @@ -363,7 +317,72 @@ export default Service.extend(Evented, {
});
}),

_cleanupSteps() {
initModalOverlay() {
if (!this._modalOverlayElem) {
this._modalOverlayElem = createModalOverlay();
this._modalOverlayOpening = getModalMaskOpening(this._modalOverlayElem);

this.hideModal();

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

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

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

this._onScreenChange = () => {
run.debounce(
this,
() => { positionModalOpening(targetElement, modalOverlayOpening) },
50
);
};

this.addStepEventListeners();

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

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

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

addStepEventListeners() {
if (typeof this._onScreenChange === 'function') {
window.removeEventListener('resize', this._onScreenChange, false);
window.removeEventListener('scroll', this._onScreenChange, false);
}

window.addEventListener('resize', this._onScreenChange, false);
window.addEventListener('scroll', this._onScreenChange, false);
},


cleanupStepEventListeners() {
if (typeof this._onScreenChange === 'function') {
window.removeEventListener('resize', this._onScreenChange, false);
window.removeEventListener('scroll', this._onScreenChange, false);

this._onScreenChange = null;
}
},

cleanupSteps() {
const tour = this.tourObject;

if (tour) {
Expand All @@ -381,18 +400,15 @@ export default Service.extend(Evented, {
}
},

_cleanupModal() {
if (this.modal) {
run('afterRender', () => {
removeElement('#shepherdOverlay');
removeElement('#highlightOverlay');
cleanupModal() {
scheduleOnce('afterRender', this, () => {
const element = this._modalOverlayElem;

const shepherdModal = document.querySelector('.shepherd-modal');
if (element && element instanceof SVGElement) {
element.parentNode.removeChild(element);
}

if (shepherdModal) {
shepherdModal.classList.remove('shepherd-modal');
}
});
}
}
this._modalOverlayElem = null;
});
},
});
15 changes: 3 additions & 12 deletions addon/styles/addon.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
#highlightOverlay {
z-index: 9998;
}

#shepherdOverlay {
#shepherdModalOverlayContainer {
position: fixed;
top: 0;
left: 0;
Expand All @@ -12,13 +8,7 @@
-khtml-opacity: 0.5;
opacity: 0.5;
z-index: 9997;
background-color: #000;
background: -moz-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
background: -webkit-gradient(radial,center center,0px,center center,100%,color-stop(0%,rgba(0,0,0,0.4)),color-stop(100%,rgba(0,0,0,0.9)));
background: -webkit-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
background: -o-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
background: -ms-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
background: radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
pointer-events: none;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#66000000',endColorstr='#e6000000',GradientType=1);
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
filter: alpha(opacity=50);
Expand All @@ -29,6 +19,7 @@
transition: all 0.3s ease-out;
}


.shepherd-modal.shepherd-enabled {
position: relative;
z-index: 9998;
Expand Down
Loading

0 comments on commit ff2a1f4

Please sign in to comment.