Closed 21degrees closed 7 years ago
I had this fix, can someone take a look at it and apply it if it makes sense? Look at the usage of findAndRemoveIntroCSSOnAllElements.
/**
define(["jquery"], function ($) { //ADDED for AMD support (see end of file)
//Default config/variables
var VERSION = '0.4.0';
/**
* IntroJs main class
*
* @class IntroJs
*/
function IntroJs(obj) {
this._targetElement = obj;
this._options = {
nextLabel: 'Next →',
prevLabel: '← Back',
skipLabel: 'Skip',
doneLabel: 'Done',
tooltipPosition: 'bottom',
exitOnEsc: true,
exitOnOverlayClick: true,
showStepNumbers: true
};
}
/**
* Initiate a new introduction/guide from an element in the page
*
* @api private
* @method _introForElement
* @param {Object} targetElm
* @returns {Boolean} Success or not?
*/
function _introForElement(targetElm) {
var introItems = [],
self = this;
if (this._options.steps) {
//use steps passed programmatically
var allIntroSteps = [];
for (var i = 0, stepsLength = this._options.steps.length; i < stepsLength; i++) {
var currentItem = this._options.steps[i];
//set the step
currentItem.step = i + 1;
//grab the element with given selector from the page
currentItem.element = document.querySelector(currentItem.element);
introItems.push(currentItem);
}
}
else {
//use steps from data-* annotations
var allIntroSteps = targetElm.querySelectorAll('*[data-intro]');
//if there's no element to intro
if (allIntroSteps.length < 1) {
return false;
}
for (var i = 0, elmsLength = allIntroSteps.length; i < elmsLength; i++) {
var currentElement = allIntroSteps[i];
introItems.push({
element: currentElement,
intro: currentElement.getAttribute('data-intro'),
step: parseInt(currentElement.getAttribute('data-step'), 10),
position: currentElement.getAttribute('data-position') || this._options.tooltipPosition
});
}
}
//Ok, sort all items with given steps
introItems.sort(function (a, b) {
return a.step - b.step;
});
//set it to the introJs object
self._introItems = introItems;
//add overlay layer to the page
if (_addOverlayLayer.call(self, targetElm)) {
//then, start the show
_nextStep.call(self);
var skipButton = targetElm.querySelector('.introjs-skipbutton'),
nextStepButton = targetElm.querySelector('.introjs-nextbutton');
self._onKeyDown = function (e) {
if (e.keyCode === 27 && self._options.exitOnEsc == true) {
//escape key pressed, exit the intro
_exitIntro.call(self, targetElm);
//check if any callback is defined
if (self._introExitCallback != undefined) {
self._introExitCallback.call(self);
}
}
else if (e.keyCode === 37) {
//left arrow
_previousStep.call(self);
}
else if (e.keyCode === 39 || e.keyCode === 13) {
//right arrow or enter
_nextStep.call(self);
//prevent default behaviour on hitting Enter, to prevent steps being skipped in some browsers
if (e.preventDefault) {
e.preventDefault();
}
else {
e.returnValue = false;
}
}
};
self._onResize = function (e) {
_setHelperLayerPosition.call(self, document.querySelector('.introjs-helperLayer'));
};
if (window.addEventListener) {
window.addEventListener('keydown', self._onKeyDown, true);
//for window resize
window.addEventListener("resize", self._onResize, true);
}
else if (document.attachEvent) { //IE
document.attachEvent('onkeydown', self._onKeyDown);
//for window resize
document.attachEvent("onresize", self._onResize);
}
}
return false;
}
/**
* Go to specific step of introduction
*
* @api private
* @method _goToStep
*/
function _goToStep(step) {
//because steps starts with zero
this._currentStep = step - 2;
if (typeof (this._introItems) !== 'undefined') {
_nextStep.call(this);
}
}
/**
* Go to next step on intro
*
* @api private
* @method _nextStep
*/
function _nextStep() {
if (typeof (this._introBeforeChangeCallback) !== 'undefined') {
this._introBeforeChangeCallback.call(this, this._targetElement);
}
if (typeof (this._currentStep) === 'undefined') {
this._currentStep = 0;
}
else {
++this._currentStep;
}
if ((this._introItems.length) <= this._currentStep) {
//end of the intro
//check if any callback is defined
if (typeof (this._introCompleteCallback) === 'function') {
this._introCompleteCallback.call(this);
}
_exitIntro.call(this, this._targetElement);
return;
}
_showElement.call(this, this._introItems[this._currentStep]);
}
/**
* Go to previous step on intro
*
* @api private
* @method _nextStep
*/
function _previousStep() {
if (this._currentStep === 0) {
return false;
}
if (typeof (this._introBeforeChangeCallback) !== 'undefined') {
this._introBeforeChangeCallback.call(this, this._targetElement);
}
_showElement.call(this, this._introItems[--this._currentStep]);
}
function findAndRemoveIntroCSSOnAllElements(cssClasses) {
function toClass(cls) {
return "." + cls;
}
var introElements = $(cssClasses.map(toClass).join(","));
function removeIntroClasses() {
$(this).removeClass(cssClasses.join(" "));
}
introElements.map(removeIntroClasses);
}
/**
* Exit from intro
*
* @api private
* @method _exitIntro
* @param {Object} targetElement
*/
function _exitIntro(targetElement) {
//remove overlay layer from the page
var overlayLayer = targetElement.querySelector('.introjs-overlay');
//for fade-out animation
overlayLayer.style.opacity = 0;
setTimeout(function () {
if (overlayLayer.parentNode) {
overlayLayer.parentNode.removeChild(overlayLayer);
}
}, 500);
findAndRemoveIntroCSSOnAllElements(["introjs-showElement" , "introjs-fixParent", "introjs-helperLayer"]);
/* //remove all helper layers
var helperLayer = targetElement.querySelector('.introjs-helperLayer');
if (helperLayer) {
helperLayer.parentNode.removeChild(helperLayer);
}
//remove `introjs-showElement` class from the element
var showElement = document.querySelector('.introjs-showElement');
if (showElement) {
showElement.className = showElement.className.replace(/introjs-[a-zA-Z]+/g, '').replace(/^\s+|\s+$/g, ''); // This is a manual trim.
}
//remove `introjs-fixParent` class from the elements
var fixParents = document.querySelectorAll('.introjs-fixParent');
if (fixParents && fixParents.length > 0) {
for (var i = fixParents.length - 1; i >= 0; i--) {
fixParents[i].className = fixParents[i].className.replace(/introjs-fixParent/g, '').replace(/^\s+|\s+$/g, '');
}
;
}*/
//clean listeners
if (window.removeEventListener) {
window.removeEventListener('keydown', this._onKeyDown, true);
}
else if (document.detachEvent) { //IE
document.detachEvent('onkeydown', this._onKeyDown);
}
//set the step to zero
this._currentStep = undefined;
}
/**
* Render tooltip box in the page
*
* @api private
* @method _placeTooltip
* @param {Object} targetElement
* @param {Object} tooltipLayer
* @param {Object} arrowLayer
*/
function _placeTooltip(targetElement, tooltipLayer, arrowLayer) {
//reset the old style
tooltipLayer.style.top = null;
tooltipLayer.style.right = null;
tooltipLayer.style.bottom = null;
tooltipLayer.style.left = null;
//prevent error when `this._currentStep` is undefined
if (!this._introItems[this._currentStep]) {
return;
}
var currentTooltipPosition = this._introItems[this._currentStep].position;
switch (currentTooltipPosition) {
case 'top':
tooltipLayer.style.left = '15px';
tooltipLayer.style.top = '-' + (_getOffset(tooltipLayer).height + 10) + 'px';
arrowLayer.className = 'introjs-arrow bottom';
break;
case 'right':
tooltipLayer.style.left = (_getOffset(targetElement).width + 20) + 'px';
arrowLayer.className = 'introjs-arrow left';
break;
case 'left':
tooltipLayer.style.top = '15px';
tooltipLayer.style.right = (_getOffset(targetElement).width + 20) + 'px';
arrowLayer.className = 'introjs-arrow right';
break;
case 'bottom':
// Bottom going to follow the default behavior
default:
tooltipLayer.style.bottom = '-' + (_getOffset(tooltipLayer).height + 10) + 'px';
arrowLayer.className = 'introjs-arrow top';
break;
}
}
/**
* Update the position of the helper layer on the screen
*
* @api private
* @method _setHelperLayerPosition
* @param {Object} helperLayer
*/
function _setHelperLayerPosition(helperLayer) {
if (helperLayer) {
//prevent error when `this._currentStep` in undefined
if (!this._introItems[this._currentStep]) {
return;
}
var elementPosition = _getOffset(this._introItems[this._currentStep].element);
//set new position to helper layer
helperLayer.setAttribute('style', 'width: ' + (elementPosition.width + 10) + 'px; ' +
'height:' + (elementPosition.height + 10) + 'px; ' +
'top:' + (elementPosition.top - 5) + 'px;' +
'left: ' + (elementPosition.left - 5) + 'px;');
}
}
/**
* Show an element on the page
*
* @api private
* @method _showElement
* @param {Object} targetElement
*/
function _showElement(targetElement) {
if (typeof (this._introChangeCallback) !== 'undefined') {
this._introChangeCallback.call(this, targetElement.element);
}
var self = this,
oldHelperLayer = document.querySelector('.introjs-helperLayer'),
elementPosition = _getOffset(targetElement.element);
if (oldHelperLayer != null) {
var oldHelperNumberLayer = oldHelperLayer.querySelector('.introjs-helperNumberLayer'),
oldtooltipLayer = oldHelperLayer.querySelector('.introjs-tooltiptext'),
oldArrowLayer = oldHelperLayer.querySelector('.introjs-arrow'),
oldtooltipContainer = oldHelperLayer.querySelector('.introjs-tooltip'),
skipTooltipButton = oldHelperLayer.querySelector('.introjs-skipbutton'),
prevTooltipButton = oldHelperLayer.querySelector('.introjs-prevbutton'),
nextTooltipButton = oldHelperLayer.querySelector('.introjs-nextbutton');
//hide the tooltip
oldtooltipContainer.style.opacity = 0;
//set new position to helper layer
_setHelperLayerPosition.call(self, oldHelperLayer);
/*//remove `introjs-fixParent` class from the elements
var fixParents = document.querySelectorAll('.introjs-fixParent');
if (fixParents && fixParents.length > 0) {
for (var i = fixParents.length - 1; i >= 0; i--) {
fixParents[i].className = fixParents[i].className.replace(/introjs-fixParent/g, '').replace(/^\s+|\s+$/g, '');
}
;
}
//remove old classes
var oldShowElement = document.querySelector('.introjs-showElement');
oldShowElement.className = oldShowElement.className.replace(/introjs-[a-zA-Z]+/g, '').replace(/^\s+|\s+$/g, '');
//we should wait until the CSS3 transition is competed (it's 0.3 sec) to prevent incorrect `height` and `width` calculation
if (self._lastShowElementTimer) {
clearTimeout(self._lastShowElementTimer);
}
*/
findAndRemoveIntroCSSOnAllElements(["introjs-showElement" , "introjs-fixParent"]);
self._lastShowElementTimer = setTimeout(function () {
//set current step to the label
if (oldHelperNumberLayer != null) {
oldHelperNumberLayer.innerHTML = targetElement.step;
}
//set current tooltip text
oldtooltipLayer.innerHTML = targetElement.intro;
//set the tooltip position
_placeTooltip.call(self, targetElement.element, oldtooltipContainer, oldArrowLayer);
//show the tooltip
oldtooltipContainer.style.opacity = 1;
}, 350);
}
else {
var helperLayer = document.createElement('div'),
arrowLayer = document.createElement('div'),
tooltipLayer = document.createElement('div');
helperLayer.className = 'introjs-helperLayer';
//set new position to helper layer
_setHelperLayerPosition.call(self, helperLayer);
//add helper layer to target element
this._targetElement.appendChild(helperLayer);
arrowLayer.className = 'introjs-arrow';
tooltipLayer.className = 'introjs-tooltip';
tooltipLayer.innerHTML = '<div class="introjs-tooltiptext">' +
targetElement.intro +
'</div><div class="introjs-tooltipbuttons"></div>';
//add helper layer number
if (this._options.showStepNumbers) {
var helperNumberLayer = document.createElement('span');
helperNumberLayer.className = 'introjs-helperNumberLayer';
helperNumberLayer.innerHTML = targetElement.step;
helperLayer.appendChild(helperNumberLayer);
}
tooltipLayer.appendChild(arrowLayer);
helperLayer.appendChild(tooltipLayer);
//next button
var nextTooltipButton = document.createElement('a');
nextTooltipButton.onclick = function () {
if (self._introItems.length - 1 != self._currentStep) {
_nextStep.call(self);
}
};
nextTooltipButton.href = 'javascript:void(0);';
nextTooltipButton.innerHTML = this._options.nextLabel;
//previous button
var prevTooltipButton = document.createElement('a');
prevTooltipButton.onclick = function () {
if (self._currentStep != 0) {
_previousStep.call(self);
}
};
prevTooltipButton.href = 'javascript:void(0);';
prevTooltipButton.innerHTML = this._options.prevLabel;
//skip button
var skipTooltipButton = document.createElement('a');
skipTooltipButton.className = 'introjs-button introjs-skipbutton';
skipTooltipButton.href = 'javascript:void(0);';
skipTooltipButton.innerHTML = this._options.skipLabel;
skipTooltipButton.onclick = function () {
if (self._introItems.length - 1 == self._currentStep && typeof (self._introCompleteCallback) === 'function') {
self._introCompleteCallback.call(self);
}
if (self._introItems.length - 1 != self._currentStep && typeof (self._introExitCallback) === 'function') {
self._introExitCallback.call(self);
}
_exitIntro.call(self, self._targetElement);
};
var tooltipButtonsLayer = tooltipLayer.querySelector('.introjs-tooltipbuttons');
tooltipButtonsLayer.appendChild(skipTooltipButton);
tooltipButtonsLayer.appendChild(prevTooltipButton);
tooltipButtonsLayer.appendChild(nextTooltipButton);
//set proper position
_placeTooltip.call(self, targetElement.element, tooltipLayer, arrowLayer);
}
if (this._currentStep == 0) {
prevTooltipButton.className = 'introjs-button introjs-prevbutton introjs-disabled';
nextTooltipButton.className = 'introjs-button introjs-nextbutton';
skipTooltipButton.innerHTML = this._options.skipLabel;
}
else if (this._introItems.length - 1 == this._currentStep) {
skipTooltipButton.innerHTML = this._options.doneLabel;
prevTooltipButton.className = 'introjs-button introjs-prevbutton';
nextTooltipButton.className = 'introjs-button introjs-nextbutton introjs-disabled';
}
else {
prevTooltipButton.className = 'introjs-button introjs-prevbutton';
nextTooltipButton.className = 'introjs-button introjs-nextbutton';
skipTooltipButton.innerHTML = this._options.skipLabel;
}
//Set focus on "next" button, so that hitting Enter always moves you onto the next step
nextTooltipButton.focus();
//add target element position style
targetElement.element.className += ' introjs-showElement';
var currentElementPosition = _getPropValue(targetElement.element, 'position');
if (currentElementPosition !== 'absolute' &&
currentElementPosition !== 'relative')
{
//change to new intro item
targetElement.element.className += ' introjs-relativePosition';
}
var parentElm = targetElement.element.parentNode;
while (parentElm != null) {
if (parentElm.tagName.toLowerCase() === 'body') {
break;
}
var zIndex = _getPropValue(parentElm, 'z-index');
if (/[0-9]+/.test(zIndex)) {
parentElm.className += ' introjs-fixParent';
}
parentElm = parentElm.parentNode;
}
if (!_elementInViewport(targetElement.element)) {
var rect = targetElement.element.getBoundingClientRect(),
top = rect.bottom - (rect.bottom - rect.top),
bottom = rect.bottom - _getWinSize().height;
// Scroll up
if (top < 0) {
window.scrollBy(0, top - 30); // 30px padding from edge to look nice
// Scroll down
}
else {
window.scrollBy(0, bottom + 100); // 70px + 30px padding from edge to look nice
}
}
}
/**
* Get an element CSS property on the page
* Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml
*
* @api private
* @method _getPropValue
* @param {Object} element
* @param {String} propName
* @returns Element's property value
*/
function _getPropValue(element, propName) {
var propValue = '';
if (element.currentStyle) { //IE
propValue = element.currentStyle[propName];
}
else if (document.defaultView && document.defaultView.getComputedStyle) { //Others
propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName);
}
//Prevent exception in IE
if (propValue.toLowerCase) {
return propValue.toLowerCase();
}
else {
return propValue;
}
}
/**
* Provides a cross-browser way to get the screen dimensions
* via: http://stackoverflow.com/questions/5864467/internet-explorer-innerheight
*
* @api private
* @method _getWinSize
* @returns {Object} width and height attributes
*/
function _getWinSize() {
if (window.innerWidth != undefined) {
return { width: window.innerWidth, height: window.innerHeight };
}
else {
var D = document.documentElement;
return { width: D.clientWidth, height: D.clientHeight };
}
}
/**
* Add overlay layer to the page
* http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
*
* @api private
* @method _elementInViewport
* @param {Object} el
*/
function _elementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
(rect.bottom + 80) <= window.innerHeight && // add 80 to get the text right
rect.right <= window.innerWidth
);
}
/**
* Add overlay layer to the page
*
* @api private
* @method _addOverlayLayer
* @param {Object} targetElm
*/
function _addOverlayLayer(targetElm) {
var overlayLayer = document.createElement('div'),
styleText = '',
self = this;
//set css class name
overlayLayer.className = 'introjs-overlay';
//check if the target element is body, we should calculate the size of overlay layer in a better way
if (targetElm.tagName.toLowerCase() === 'body') {
styleText += 'top: 0;bottom: 0; left: 0;right: 0;position: fixed;';
overlayLayer.setAttribute('style', styleText);
}
else {
//set overlay layer position
var elementPosition = _getOffset(targetElm);
if (elementPosition) {
styleText += 'width: ' + elementPosition.width + 'px; height:' + elementPosition.height + 'px; top:' + elementPosition.top + 'px;left: ' + elementPosition.left + 'px;';
overlayLayer.setAttribute('style', styleText);
}
}
targetElm.appendChild(overlayLayer);
overlayLayer.onclick = function () {
if (self._options.exitOnOverlayClick == true) {
_exitIntro.call(self, targetElm);
}
//check if any callback is defined
if (self._introExitCallback != undefined) {
self._introExitCallback.call(self);
}
};
setTimeout(function () {
styleText += 'opacity: .8;';
overlayLayer.setAttribute('style', styleText);
}, 10);
return true;
}
/**
* Get an element position on the page
* Thanks to `meouw`: http://stackoverflow.com/a/442474/375966
*
* @api private
* @method _getOffset
* @param {Object} element
* @returns Element's position info
*/
function _getOffset(element) {
var elementPosition = {};
//set width
elementPosition.width = element.offsetWidth;
//set height
elementPosition.height = element.offsetHeight;
//calculate element top and left
var _x = 0;
var _y = 0;
while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
_x += element.offsetLeft;
_y += element.offsetTop;
element = element.offsetParent;
}
//set top
elementPosition.top = _y;
//set left
elementPosition.left = _x;
return elementPosition;
}
/**
* Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
* via: http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
*
* @param obj1
* @param obj2
* @returns obj3 a new object based on obj1 and obj2
*/
function _mergeOptions(obj1, obj2) {
var obj3 = {};
for (var attrname in obj1) {
obj3[attrname] = obj1[attrname];
}
for (var attrname in obj2) {
obj3[attrname] = obj2[attrname];
}
return obj3;
}
var introJs = function (targetElm) {
if (typeof (targetElm) === 'object') {
//Ok, create a new instance
return new IntroJs(targetElm);
}
else if (typeof (targetElm) === 'string') {
//select the target element with query selector
var targetElement = document.querySelector(targetElm);
if (targetElement) {
return new IntroJs(targetElement);
}
else {
throw new Error('There is no element with given selector.');
}
}
else {
return new IntroJs(document.body);
}
};
/**
* Current IntroJs version
*
* @property version
* @type String
*/
introJs.version = VERSION;
//Prototype
introJs.fn = IntroJs.prototype = {
clone: function () {
return new IntroJs(this);
},
setOption: function (option, value) {
this._options[option] = value;
return this;
},
setOptions: function (options) {
this._options = _mergeOptions(this._options, options);
return this;
},
start: function () {
_introForElement.call(this, this._targetElement);
return this;
},
goToStep: function (step) {
_goToStep.call(this, step);
return this;
},
exit: function () {
_exitIntro.call(this, this._targetElement);
},
onbeforechange: function (providedCallback) {
if (typeof (providedCallback) === 'function') {
this._introBeforeChangeCallback = providedCallback;
}
else {
throw new Error('Provided callback for onbeforechange was not a function');
}
return this;
},
onchange: function (providedCallback) {
if (typeof (providedCallback) === 'function') {
this._introChangeCallback = providedCallback;
}
else {
throw new Error('Provided callback for onchange was not a function.');
}
return this;
},
oncomplete: function (providedCallback) {
if (typeof (providedCallback) === 'function') {
this._introCompleteCallback = providedCallback;
}
else {
throw new Error('Provided callback for oncomplete was not a function.');
}
return this;
},
onexit: function (providedCallback) {
if (typeof (providedCallback) === 'function') {
this._introExitCallback = providedCallback;
}
else {
throw new Error('Provided callback for onexit was not a function.');
}
return this;
}
};
// exports.introJs = introJs;
return introJs;
});
In which browsers? I can't see any problem now.
This is in Chrome! We are using Select2 plugin (http://ivaynberg.github.io/select2/), Select2 does apply styles after the fact. So introjs adds z-index and doesn't remove it, this pushes Select2 component to the background.
So it seems it's a conflict with select2
. Could you please give me an example of even better, an online example of your case? I'll fix it asap.
I believe this issue has already been addressed with new addClass and removeClass methods. Please reopen if it is still an issue.
_exitIntro method doesn't remove .introjs-showElement on all elements, it just removes the current element. This is causing other elements show up on top of introjs layer on subsequent tours and sometimes on the same tour. Suggested implementation to remove a particular class, this must be done for all classes added by introjs.
var showElements = $.find('.introjs-showElement');
showElements.each(function(showElement){ showElement.removeClass('.introjs-showElement'); })