intro.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795
  1. /**
  2. * Intro.js v0.5.0
  3. * https://github.com/usablica/intro.js
  4. * MIT licensed
  5. *
  6. * Copyright (C) 2013 usabli.ca - A weekend project by Afshin Mehrabani (@afshinmeh)
  7. */
  8. (function (root, factory) {
  9. if (typeof exports === 'object') {
  10. // CommonJS
  11. factory(exports);
  12. } else if (typeof define === 'function' && define.amd) {
  13. // AMD. Register as an anonymous module.
  14. define(['exports'], factory);
  15. } else {
  16. // Browser globals
  17. factory(root);
  18. }
  19. } (this, function (exports) {
  20. //Default config/variables
  21. var VERSION = '0.5.0';
  22. /**
  23. * IntroJs main class
  24. *
  25. * @class IntroJs
  26. */
  27. function IntroJs(obj) {
  28. this._targetElement = obj;
  29. this._options = {
  30. /* Next button label in tooltip box */
  31. nextLabel: 'Next →',
  32. /* Previous button label in tooltip box */
  33. prevLabel: '← Back',
  34. /* Skip button label in tooltip box */
  35. skipLabel: 'Skip',
  36. /* Done button label in tooltip box */
  37. doneLabel: 'Done',
  38. /* Default tooltip box position */
  39. tooltipPosition: 'bottom',
  40. /* Next CSS class for tooltip boxes */
  41. tooltipClass: '',
  42. /* Close introduction when pressing Escape button? */
  43. exitOnEsc: true,
  44. /* Close introduction when clicking on overlay layer? */
  45. exitOnOverlayClick: true,
  46. /* Show step numbers in introduction? */
  47. showStepNumbers: true
  48. };
  49. }
  50. /**
  51. * Initiate a new introduction/guide from an element in the page
  52. *
  53. * @api private
  54. * @method _introForElement
  55. * @param {Object} targetElm
  56. * @returns {Boolean} Success or not?
  57. */
  58. function _introForElement(targetElm) {
  59. var introItems = [],
  60. self = this;
  61. if (this._options.steps) {
  62. //use steps passed programmatically
  63. var allIntroSteps = [];
  64. for (var i = 0, stepsLength = this._options.steps.length; i < stepsLength; i++) {
  65. var currentItem = this._options.steps[i];
  66. //set the step
  67. currentItem.step = i + 1;
  68. //use querySelector function only when developer used CSS selector
  69. if (typeof(currentItem.element) === 'string') {
  70. //grab the element with given selector from the page
  71. currentItem.element = document.querySelector(currentItem.element);
  72. }
  73. introItems.push(currentItem);
  74. }
  75. } else {
  76. //use steps from data-* annotations
  77. var allIntroSteps = targetElm.querySelectorAll('*[data-intro]');
  78. //if there's no element to intro
  79. if (allIntroSteps.length < 1) {
  80. return false;
  81. }
  82. for (var i = 0, elmsLength = allIntroSteps.length; i < elmsLength; i++) {
  83. var currentElement = allIntroSteps[i];
  84. introItems.push({
  85. element: currentElement,
  86. intro: currentElement.getAttribute('data-intro'),
  87. step: parseInt(currentElement.getAttribute('data-step'), 10),
  88. tooltipClass: currentElement.getAttribute('data-tooltipClass'),
  89. position: currentElement.getAttribute('data-position') || this._options.tooltipPosition
  90. });
  91. }
  92. }
  93. //Ok, sort all items with given steps
  94. introItems.sort(function (a, b) {
  95. return a.step - b.step;
  96. });
  97. //set it to the introJs object
  98. self._introItems = introItems;
  99. //add overlay layer to the page
  100. if(_addOverlayLayer.call(self, targetElm)) {
  101. //then, start the show
  102. _nextStep.call(self);
  103. var skipButton = targetElm.querySelector('.introjs-skipbutton'),
  104. nextStepButton = targetElm.querySelector('.introjs-nextbutton');
  105. self._onKeyDown = function(e) {
  106. if (e.keyCode === 27 && self._options.exitOnEsc == true) {
  107. //escape key pressed, exit the intro
  108. _exitIntro.call(self, targetElm);
  109. //check if any callback is defined
  110. if (self._introExitCallback != undefined) {
  111. self._introExitCallback.call(self);
  112. }
  113. } else if(e.keyCode === 37) {
  114. //left arrow
  115. _previousStep.call(self);
  116. } else if (e.keyCode === 39 || e.keyCode === 13) {
  117. //right arrow or enter
  118. _nextStep.call(self);
  119. //prevent default behaviour on hitting Enter, to prevent steps being skipped in some browsers
  120. if(e.preventDefault) {
  121. e.preventDefault();
  122. } else {
  123. e.returnValue = false;
  124. }
  125. }
  126. };
  127. self._onResize = function(e) {
  128. _setHelperLayerPosition.call(self, document.querySelector('.introjs-helperLayer'));
  129. };
  130. if (window.addEventListener) {
  131. window.addEventListener('keydown', self._onKeyDown, true);
  132. //for window resize
  133. window.addEventListener("resize", self._onResize, true);
  134. } else if (document.attachEvent) { //IE
  135. document.attachEvent('onkeydown', self._onKeyDown);
  136. //for window resize
  137. document.attachEvent("onresize", self._onResize);
  138. }
  139. }
  140. return false;
  141. }
  142. /**
  143. * Go to specific step of introduction
  144. *
  145. * @api private
  146. * @method _goToStep
  147. */
  148. function _goToStep(step) {
  149. //because steps starts with zero
  150. this._currentStep = step - 2;
  151. if(typeof (this._introItems) !== 'undefined') {
  152. _nextStep.call(this);
  153. }
  154. }
  155. /**
  156. * Go to next step on intro
  157. *
  158. * @api private
  159. * @method _nextStep
  160. */
  161. function _nextStep() {
  162. if (typeof (this._currentStep) === 'undefined') {
  163. this._currentStep = 0;
  164. } else {
  165. ++this._currentStep;
  166. }
  167. if((this._introItems.length) <= this._currentStep) {
  168. //end of the intro
  169. //check if any callback is defined
  170. if (typeof (this._introCompleteCallback) === 'function') {
  171. this._introCompleteCallback.call(this);
  172. }
  173. _exitIntro.call(this, this._targetElement);
  174. return;
  175. }
  176. var nextStep = this._introItems[this._currentStep];
  177. if (typeof (this._introBeforeChangeCallback) !== 'undefined') {
  178. this._introBeforeChangeCallback.call(this, nextStep.element);
  179. }
  180. _showElement.call(this, nextStep);
  181. }
  182. /**
  183. * Go to previous step on intro
  184. *
  185. * @api private
  186. * @method _nextStep
  187. */
  188. function _previousStep() {
  189. if (this._currentStep === 0) {
  190. return false;
  191. }
  192. var nextStep = this._introItems[--this._currentStep];
  193. if (typeof (this._introBeforeChangeCallback) !== 'undefined') {
  194. this._introBeforeChangeCallback.call(this, nextStep.element);
  195. }
  196. _showElement.call(this, nextStep);
  197. }
  198. /**
  199. * Exit from intro
  200. *
  201. * @api private
  202. * @method _exitIntro
  203. * @param {Object} targetElement
  204. */
  205. function _exitIntro(targetElement) {
  206. //remove overlay layer from the page
  207. var overlayLayer = targetElement.querySelector('.introjs-overlay');
  208. //for fade-out animation
  209. overlayLayer.style.opacity = 0;
  210. setTimeout(function () {
  211. if (overlayLayer.parentNode) {
  212. overlayLayer.parentNode.removeChild(overlayLayer);
  213. }
  214. }, 500);
  215. //remove all helper layers
  216. var helperLayer = targetElement.querySelector('.introjs-helperLayer');
  217. if (helperLayer) {
  218. helperLayer.parentNode.removeChild(helperLayer);
  219. }
  220. //remove `introjs-showElement` class from the element
  221. var showElement = document.querySelector('.introjs-showElement');
  222. if (showElement) {
  223. showElement.className = showElement.className.replace(/introjs-[a-zA-Z]+/g, '').replace(/^\s+|\s+$/g, ''); // This is a manual trim.
  224. }
  225. //remove `introjs-fixParent` class from the elements
  226. var fixParents = document.querySelectorAll('.introjs-fixParent');
  227. if (fixParents && fixParents.length > 0) {
  228. for (var i = fixParents.length - 1; i >= 0; i--) {
  229. fixParents[i].className = fixParents[i].className.replace(/introjs-fixParent/g, '').replace(/^\s+|\s+$/g, '');
  230. };
  231. }
  232. //clean listeners
  233. if (window.removeEventListener) {
  234. window.removeEventListener('keydown', this._onKeyDown, true);
  235. } else if (document.detachEvent) { //IE
  236. document.detachEvent('onkeydown', this._onKeyDown);
  237. }
  238. //set the step to zero
  239. this._currentStep = undefined;
  240. }
  241. /**
  242. * Render tooltip box in the page
  243. *
  244. * @api private
  245. * @method _placeTooltip
  246. * @param {Object} targetElement
  247. * @param {Object} tooltipLayer
  248. * @param {Object} arrowLayer
  249. */
  250. function _placeTooltip(targetElement, tooltipLayer, arrowLayer) {
  251. //reset the old style
  252. tooltipLayer.style.top = null;
  253. tooltipLayer.style.right = null;
  254. tooltipLayer.style.bottom = null;
  255. tooltipLayer.style.left = null;
  256. //prevent error when `this._currentStep` is undefined
  257. if(!this._introItems[this._currentStep]) return;
  258. var tooltipCssClass = '';
  259. //if we have a custom css class for each step
  260. var currentStepObj = this._introItems[this._currentStep];
  261. if (typeof (currentStepObj.tooltipClass) === 'string') {
  262. tooltipCssClass = currentStepObj.tooltipClass;
  263. } else {
  264. tooltipCssClass = this._options.tooltipClass;
  265. }
  266. tooltipLayer.className = ('introjs-tooltip ' + tooltipCssClass).replace(/^\s+|\s+$/g, '');
  267. //custom css class for tooltip boxes
  268. var tooltipCssClass = this._options.tooltipClass;
  269. var currentTooltipPosition = this._introItems[this._currentStep].position;
  270. switch (currentTooltipPosition) {
  271. case 'top':
  272. tooltipLayer.style.left = '15px';
  273. tooltipLayer.style.top = '-' + (_getOffset(tooltipLayer).height + 10) + 'px';
  274. arrowLayer.className = 'introjs-arrow bottom';
  275. break;
  276. case 'right':
  277. tooltipLayer.style.left = (_getOffset(targetElement).width + 20) + 'px';
  278. arrowLayer.className = 'introjs-arrow left';
  279. break;
  280. case 'left':
  281. tooltipLayer.style.top = '15px';
  282. tooltipLayer.style.right = (_getOffset(targetElement).width + 20) + 'px';
  283. arrowLayer.className = 'introjs-arrow right';
  284. break;
  285. case 'bottom':
  286. // Bottom going to follow the default behavior
  287. default:
  288. tooltipLayer.style.bottom = '-' + (_getOffset(tooltipLayer).height + 10) + 'px';
  289. arrowLayer.className = 'introjs-arrow top';
  290. break;
  291. }
  292. }
  293. /**
  294. * Update the position of the helper layer on the screen
  295. *
  296. * @api private
  297. * @method _setHelperLayerPosition
  298. * @param {Object} helperLayer
  299. */
  300. function _setHelperLayerPosition(helperLayer) {
  301. if(helperLayer) {
  302. //prevent error when `this._currentStep` in undefined
  303. if(!this._introItems[this._currentStep]) return;
  304. var elementPosition = _getOffset(this._introItems[this._currentStep].element);
  305. //set new position to helper layer
  306. helperLayer.setAttribute('style', 'width: ' + (elementPosition.width + 10) + 'px; ' +
  307. 'height:' + (elementPosition.height + 10) + 'px; ' +
  308. 'top:' + (elementPosition.top - 5) + 'px;' +
  309. 'left: ' + (elementPosition.left - 5) + 'px;');
  310. }
  311. }
  312. /**
  313. * Show an element on the page
  314. *
  315. * @api private
  316. * @method _showElement
  317. * @param {Object} targetElement
  318. */
  319. function _showElement(targetElement) {
  320. if (typeof (this._introChangeCallback) !== 'undefined') {
  321. this._introChangeCallback.call(this, targetElement.element);
  322. }
  323. var self = this,
  324. oldHelperLayer = document.querySelector('.introjs-helperLayer'),
  325. elementPosition = _getOffset(targetElement.element);
  326. if(oldHelperLayer != null) {
  327. var oldHelperNumberLayer = oldHelperLayer.querySelector('.introjs-helperNumberLayer'),
  328. oldtooltipLayer = oldHelperLayer.querySelector('.introjs-tooltiptext'),
  329. oldArrowLayer = oldHelperLayer.querySelector('.introjs-arrow'),
  330. oldtooltipContainer = oldHelperLayer.querySelector('.introjs-tooltip'),
  331. skipTooltipButton = oldHelperLayer.querySelector('.introjs-skipbutton'),
  332. prevTooltipButton = oldHelperLayer.querySelector('.introjs-prevbutton'),
  333. nextTooltipButton = oldHelperLayer.querySelector('.introjs-nextbutton');
  334. //hide the tooltip
  335. oldtooltipContainer.style.opacity = 0;
  336. //set new position to helper layer
  337. _setHelperLayerPosition.call(self, oldHelperLayer);
  338. //remove `introjs-fixParent` class from the elements
  339. var fixParents = document.querySelectorAll('.introjs-fixParent');
  340. if (fixParents && fixParents.length > 0) {
  341. for (var i = fixParents.length - 1; i >= 0; i--) {
  342. fixParents[i].className = fixParents[i].className.replace(/introjs-fixParent/g, '').replace(/^\s+|\s+$/g, '');
  343. };
  344. }
  345. //remove old classes
  346. var oldShowElement = document.querySelector('.introjs-showElement');
  347. oldShowElement.className = oldShowElement.className.replace(/introjs-[a-zA-Z]+/g, '').replace(/^\s+|\s+$/g, '');
  348. //we should wait until the CSS3 transition is competed (it's 0.3 sec) to prevent incorrect `height` and `width` calculation
  349. if (self._lastShowElementTimer) {
  350. clearTimeout(self._lastShowElementTimer);
  351. }
  352. self._lastShowElementTimer = setTimeout(function() {
  353. //set current step to the label
  354. if(oldHelperNumberLayer != null) {
  355. oldHelperNumberLayer.innerHTML = targetElement.step;
  356. }
  357. //set current tooltip text
  358. oldtooltipLayer.innerHTML = targetElement.intro;
  359. //set the tooltip position
  360. _placeTooltip.call(self, targetElement.element, oldtooltipContainer, oldArrowLayer);
  361. //show the tooltip
  362. oldtooltipContainer.style.opacity = 1;
  363. }, 350);
  364. } else {
  365. var helperLayer = document.createElement('div'),
  366. arrowLayer = document.createElement('div'),
  367. tooltipLayer = document.createElement('div');
  368. helperLayer.className = 'introjs-helperLayer';
  369. //set new position to helper layer
  370. _setHelperLayerPosition.call(self, helperLayer);
  371. //add helper layer to target element
  372. this._targetElement.appendChild(helperLayer);
  373. arrowLayer.className = 'introjs-arrow';
  374. tooltipLayer.innerHTML = '<div class="introjs-tooltiptext">' +
  375. targetElement.intro +
  376. '</div><div class="introjs-tooltipbuttons"></div>';
  377. //add helper layer number
  378. if (this._options.showStepNumbers) {
  379. var helperNumberLayer = document.createElement('span');
  380. helperNumberLayer.className = 'introjs-helperNumberLayer';
  381. helperNumberLayer.innerHTML = targetElement.step;
  382. helperLayer.appendChild(helperNumberLayer);
  383. }
  384. tooltipLayer.appendChild(arrowLayer);
  385. helperLayer.appendChild(tooltipLayer);
  386. //next button
  387. var nextTooltipButton = document.createElement('a');
  388. nextTooltipButton.onclick = function() {
  389. if(self._introItems.length - 1 != self._currentStep) {
  390. _nextStep.call(self);
  391. }
  392. };
  393. nextTooltipButton.href = 'javascript:void(0);';
  394. nextTooltipButton.innerHTML = this._options.nextLabel;
  395. //previous button
  396. var prevTooltipButton = document.createElement('a');
  397. prevTooltipButton.onclick = function() {
  398. if(self._currentStep != 0) {
  399. _previousStep.call(self);
  400. }
  401. };
  402. prevTooltipButton.href = 'javascript:void(0);';
  403. prevTooltipButton.innerHTML = this._options.prevLabel;
  404. //skip button
  405. var skipTooltipButton = document.createElement('a');
  406. skipTooltipButton.className = 'introjs-button introjs-skipbutton';
  407. skipTooltipButton.href = 'javascript:void(0);';
  408. skipTooltipButton.innerHTML = this._options.skipLabel;
  409. skipTooltipButton.onclick = function() {
  410. if (self._introItems.length - 1 == self._currentStep && typeof (self._introCompleteCallback) === 'function') {
  411. self._introCompleteCallback.call(self);
  412. }
  413. if (self._introItems.length - 1 != self._currentStep && typeof (self._introExitCallback) === 'function') {
  414. self._introExitCallback.call(self);
  415. }
  416. _exitIntro.call(self, self._targetElement);
  417. };
  418. var tooltipButtonsLayer = tooltipLayer.querySelector('.introjs-tooltipbuttons');
  419. tooltipButtonsLayer.appendChild(skipTooltipButton);
  420. //in order to prevent displaying next/previous button always
  421. if (this._introItems.length > 1) {
  422. tooltipButtonsLayer.appendChild(prevTooltipButton);
  423. tooltipButtonsLayer.appendChild(nextTooltipButton);
  424. }
  425. //set proper position
  426. _placeTooltip.call(self, targetElement.element, tooltipLayer, arrowLayer);
  427. }
  428. if (this._currentStep == 0) {
  429. prevTooltipButton.className = 'introjs-button introjs-prevbutton introjs-disabled';
  430. nextTooltipButton.className = 'introjs-button introjs-nextbutton';
  431. skipTooltipButton.innerHTML = this._options.skipLabel;
  432. } else if (this._introItems.length - 1 == this._currentStep) {
  433. skipTooltipButton.innerHTML = this._options.doneLabel;
  434. prevTooltipButton.className = 'introjs-button introjs-prevbutton';
  435. nextTooltipButton.className = 'introjs-button introjs-nextbutton introjs-disabled';
  436. } else {
  437. prevTooltipButton.className = 'introjs-button introjs-prevbutton';
  438. nextTooltipButton.className = 'introjs-button introjs-nextbutton';
  439. skipTooltipButton.innerHTML = this._options.skipLabel;
  440. }
  441. //Set focus on "next" button, so that hitting Enter always moves you onto the next step
  442. nextTooltipButton.focus();
  443. //add target element position style
  444. targetElement.element.className += ' introjs-showElement';
  445. var currentElementPosition = _getPropValue(targetElement.element, 'position');
  446. if (currentElementPosition !== 'absolute' &&
  447. currentElementPosition !== 'relative') {
  448. //change to new intro item
  449. targetElement.element.className += ' introjs-relativePosition';
  450. }
  451. var parentElm = targetElement.element.parentNode;
  452. while(parentElm != null) {
  453. if(parentElm.tagName.toLowerCase() === 'body') break;
  454. var zIndex = _getPropValue(parentElm, 'z-index');
  455. if(/[0-9]+/.test(zIndex)) {
  456. parentElm.className += ' introjs-fixParent';
  457. }
  458. parentElm = parentElm.parentNode;
  459. }
  460. if (!_elementInViewport(targetElement.element)) {
  461. var rect = targetElement.element.getBoundingClientRect(),
  462. top = rect.bottom - (rect.bottom - rect.top),
  463. bottom = rect.bottom - _getWinSize().height;
  464. // Scroll up
  465. if (top < 0) {
  466. window.scrollBy(0, top - 30); // 30px padding from edge to look nice
  467. // Scroll down
  468. } else {
  469. window.scrollBy(0, bottom + 100); // 70px + 30px padding from edge to look nice
  470. }
  471. }
  472. }
  473. /**
  474. * Get an element CSS property on the page
  475. * Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml
  476. *
  477. * @api private
  478. * @method _getPropValue
  479. * @param {Object} element
  480. * @param {String} propName
  481. * @returns Element's property value
  482. */
  483. function _getPropValue (element, propName) {
  484. var propValue = '';
  485. if (element.currentStyle) { //IE
  486. propValue = element.currentStyle[propName];
  487. } else if (document.defaultView && document.defaultView.getComputedStyle) { //Others
  488. propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName);
  489. }
  490. //Prevent exception in IE
  491. if(propValue && propValue.toLowerCase) {
  492. return propValue.toLowerCase();
  493. } else {
  494. return propValue;
  495. }
  496. }
  497. /**
  498. * Provides a cross-browser way to get the screen dimensions
  499. * via: http://stackoverflow.com/questions/5864467/internet-explorer-innerheight
  500. *
  501. * @api private
  502. * @method _getWinSize
  503. * @returns {Object} width and height attributes
  504. */
  505. function _getWinSize() {
  506. if (window.innerWidth != undefined) {
  507. return { width: window.innerWidth, height: window.innerHeight };
  508. } else {
  509. var D = document.documentElement;
  510. return { width: D.clientWidth, height: D.clientHeight };
  511. }
  512. }
  513. /**
  514. * Add overlay layer to the page
  515. * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
  516. *
  517. * @api private
  518. * @method _elementInViewport
  519. * @param {Object} el
  520. */
  521. function _elementInViewport(el) {
  522. var rect = el.getBoundingClientRect();
  523. return (
  524. rect.top >= 0 &&
  525. rect.left >= 0 &&
  526. (rect.bottom+80) <= window.innerHeight && // add 80 to get the text right
  527. rect.right <= window.innerWidth
  528. );
  529. }
  530. /**
  531. * Add overlay layer to the page
  532. *
  533. * @api private
  534. * @method _addOverlayLayer
  535. * @param {Object} targetElm
  536. */
  537. function _addOverlayLayer(targetElm) {
  538. var overlayLayer = document.createElement('div'),
  539. styleText = '',
  540. self = this;
  541. //set css class name
  542. overlayLayer.className = 'introjs-overlay';
  543. //check if the target element is body, we should calculate the size of overlay layer in a better way
  544. if (targetElm.tagName.toLowerCase() === 'body') {
  545. styleText += 'top: 0;bottom: 0; left: 0;right: 0;position: fixed;';
  546. overlayLayer.setAttribute('style', styleText);
  547. } else {
  548. //set overlay layer position
  549. var elementPosition = _getOffset(targetElm);
  550. if(elementPosition) {
  551. styleText += 'width: ' + elementPosition.width + 'px; height:' + elementPosition.height + 'px; top:' + elementPosition.top + 'px;left: ' + elementPosition.left + 'px;';
  552. overlayLayer.setAttribute('style', styleText);
  553. }
  554. }
  555. targetElm.appendChild(overlayLayer);
  556. overlayLayer.onclick = function() {
  557. if(self._options.exitOnOverlayClick == true) {
  558. _exitIntro.call(self, targetElm);
  559. }
  560. //check if any callback is defined
  561. if (self._introExitCallback != undefined) {
  562. self._introExitCallback.call(self);
  563. }
  564. };
  565. setTimeout(function() {
  566. styleText += 'opacity: .8;';
  567. overlayLayer.setAttribute('style', styleText);
  568. }, 10);
  569. return true;
  570. }
  571. /**
  572. * Get an element position on the page
  573. * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966
  574. *
  575. * @api private
  576. * @method _getOffset
  577. * @param {Object} element
  578. * @returns Element's position info
  579. */
  580. function _getOffset(element) {
  581. var elementPosition = {};
  582. //set width
  583. elementPosition.width = element.offsetWidth;
  584. //set height
  585. elementPosition.height = element.offsetHeight;
  586. //calculate element top and left
  587. var _x = 0;
  588. var _y = 0;
  589. while(element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
  590. _x += element.offsetLeft;
  591. _y += element.offsetTop;
  592. element = element.offsetParent;
  593. }
  594. //set top
  595. elementPosition.top = _y;
  596. //set left
  597. elementPosition.left = _x;
  598. return elementPosition;
  599. }
  600. /**
  601. * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
  602. * via: http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
  603. *
  604. * @param obj1
  605. * @param obj2
  606. * @returns obj3 a new object based on obj1 and obj2
  607. */
  608. function _mergeOptions(obj1,obj2) {
  609. var obj3 = {};
  610. for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
  611. for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }
  612. return obj3;
  613. }
  614. var introJs = function (targetElm) {
  615. if (typeof (targetElm) === 'object') {
  616. //Ok, create a new instance
  617. return new IntroJs(targetElm);
  618. } else if (typeof (targetElm) === 'string') {
  619. //select the target element with query selector
  620. var targetElement = document.querySelector(targetElm);
  621. if (targetElement) {
  622. return new IntroJs(targetElement);
  623. } else {
  624. throw new Error('There is no element with given selector.');
  625. }
  626. } else {
  627. return new IntroJs(document.body);
  628. }
  629. };
  630. /**
  631. * Current IntroJs version
  632. *
  633. * @property version
  634. * @type String
  635. */
  636. introJs.version = VERSION;
  637. //Prototype
  638. introJs.fn = IntroJs.prototype = {
  639. clone: function () {
  640. return new IntroJs(this);
  641. },
  642. setOption: function(option, value) {
  643. this._options[option] = value;
  644. return this;
  645. },
  646. setOptions: function(options) {
  647. this._options = _mergeOptions(this._options, options);
  648. return this;
  649. },
  650. start: function () {
  651. _introForElement.call(this, this._targetElement);
  652. return this;
  653. },
  654. goToStep: function(step) {
  655. _goToStep.call(this, step);
  656. return this;
  657. },
  658. exit: function() {
  659. _exitIntro.call(this, this._targetElement);
  660. },
  661. refresh: function() {
  662. _setHelperLayerPosition.call(this, document.querySelector('.introjs-helperLayer'));
  663. return this;
  664. },
  665. onbeforechange: function(providedCallback) {
  666. if (typeof (providedCallback) === 'function') {
  667. this._introBeforeChangeCallback = providedCallback;
  668. } else {
  669. throw new Error('Provided callback for onbeforechange was not a function');
  670. }
  671. return this;
  672. },
  673. onchange: function(providedCallback) {
  674. if (typeof (providedCallback) === 'function') {
  675. this._introChangeCallback = providedCallback;
  676. } else {
  677. throw new Error('Provided callback for onchange was not a function.');
  678. }
  679. return this;
  680. },
  681. oncomplete: function(providedCallback) {
  682. if (typeof (providedCallback) === 'function') {
  683. this._introCompleteCallback = providedCallback;
  684. } else {
  685. throw new Error('Provided callback for oncomplete was not a function.');
  686. }
  687. return this;
  688. },
  689. onexit: function(providedCallback) {
  690. if (typeof (providedCallback) === 'function') {
  691. this._introExitCallback = providedCallback;
  692. } else {
  693. throw new Error('Provided callback for onexit was not a function.');
  694. }
  695. return this;
  696. }
  697. };
  698. exports.introJs = introJs;
  699. return introJs;
  700. }));