From 0d5a54de62e33c828f6060800b387222562b81a0 Mon Sep 17 00:00:00 2001 From: Benjamin Dobell Date: Thu, 2 Jul 2015 21:39:28 -0700 Subject: [PATCH] Added container tour option, instead of always using document.body --- src/js/hopscotch.js | 65 +++++++++++++++++++++++++++------------ test/js/test.hopscotch.js | 58 +++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/src/js/hopscotch.js b/src/js/hopscotch.js index aa7b0ade..0c6a9276 100644 --- a/src/js/hopscotch.js +++ b/src/js/hopscotch.js @@ -346,7 +346,7 @@ }, /** - * Helper function to get a single target DOM element. We will try to + * Helper function to find a DOM element with an identifier. We will try to * locate the DOM element through several ways, in the following order: * * 1) Passing the string into document.querySelector @@ -359,7 +359,7 @@ * * @private */ - getStepTargetHelper: function(target){ + getElementByIdentifier: function(target) { var result = document.getElementById(target); //Backwards compatibility: assume the string is an id @@ -388,6 +388,23 @@ return null; }, + /** + * Returns the container DOM element where bubble elements will be added + * as children. The container element can be specified by tourOpt.container + * as either a string identifier (ID/selector) or directly as a JavaScript + * DOM element. By default, or if the specified string identifier does not + * match an element, the document's body is used. + * + * @private + */ + getContainer: function(tourOpt) { + if (tourOpt.container) { + return typeof tourOpt.container === 'string' ? utils.getElementByIdentifier(tourOpt.container) || document.body : tourOpt.container; + } + + return document.body; + }, + /** * Given a step, returns the target DOM element associated with it. It is * recommended to only assign one target per step. However, there are @@ -407,7 +424,7 @@ if (typeof step.target === 'string') { //Just one target to test. Check and return its results. - return utils.getStepTargetHelper(step.target); + return utils.getElementByIdentifier(step.target); } else if (Array.isArray(step.target)) { // Multiple items to check. Check each and return the first success. @@ -417,7 +434,7 @@ for (i = 0, len = step.target.length; i < len; i++){ if (typeof step.target[i] === 'string') { - queriedTarget = utils.getStepTargetHelper(step.target[i]); + queriedTarget = utils.getElementByIdentifier(step.target[i]); if (queriedTarget) { return queriedTarget; @@ -614,6 +631,8 @@ left, arrowOffset, verticalLeftPosition, + containerBoundingRect, + containerEl = utils.getContainer(this.opt), targetEl = utils.getStepTarget(step), el = this.element, arrowEl = this.arrowEl, @@ -628,6 +647,7 @@ // SET POSITION boundingRect = targetEl.getBoundingClientRect(); + containerBoundingRect = containerEl.getBoundingClientRect(); verticalLeftPosition = step.isRtl ? boundingRect.right - bubbleBoundingWidth : boundingRect.left; @@ -690,6 +710,7 @@ else { left += utils.getPixelValue(step.xOffset); } + // VERTICAL OFFSET if (step.yOffset === 'center') { top = (boundingRect.top + targetEl.offsetHeight/2) - (bubbleBoundingHeight / 2); @@ -704,6 +725,10 @@ left += utils.getScrollLeft(); } + // CONVERT TO CONTAINER COORDINATES + top -= containerBoundingRect.top; + left -= containerBoundingRect.left; + // ACCOUNT FOR FIXED POSITION ELEMENTS el.style.position = (step.fixedElement ? 'fixed' : 'absolute'); @@ -1065,7 +1090,7 @@ self = this, resizeCooldown = false, // for updating after window resize onWinResize, - appendToBody, + appendToContainer, children, numChildren, node, @@ -1134,35 +1159,35 @@ //Hide the bubble by default this.hide(); - //Finally, append our new bubble to body once the DOM is ready. + // Finally, append our new bubble to the container once the DOM is ready. + if (utils.documentIsReady()) { - document.body.appendChild(el); + utils.getContainer(opt).appendChild(el); } else { // Moz, webkit, Opera if (document.addEventListener) { - appendToBody = function() { - document.removeEventListener('DOMContentLoaded', appendToBody); - window.removeEventListener('load', appendToBody); - - document.body.appendChild(el); + appendToContainer = function() { + document.removeEventListener('DOMContentLoaded', appendToContainer); + window.removeEventListener('load', appendToContainer); + utils.getContainer(opt).appendChild(el); }; - document.addEventListener('DOMContentLoaded', appendToBody, false); + document.addEventListener('DOMContentLoaded', appendToContainer, false); } // IE else { - appendToBody = function() { + appendToContainer = function() { if (document.readyState === 'complete') { - document.detachEvent('onreadystatechange', appendToBody); - window.detachEvent('onload', appendToBody); - document.body.appendChild(el); + document.detachEvent('onreadystatechange', appendToContainer); + window.detachEvent('onload', appendToContainer); + utils.getContainer(opt).appendChild(el); } }; - document.attachEvent('onreadystatechange', appendToBody); + document.attachEvent('onreadystatechange', appendToContainer); } - utils.addEvtListener(window, 'load', appendToBody); + utils.addEvtListener(window, 'load', appendToContainer); } } }; @@ -1856,7 +1881,7 @@ // loadTour if we are calling startTour directly. (When we call startTour // from window onLoad handler, we'll use currTour) if (!currTour) { - + // Sanity check! Is there a tour? if(!tour){ throw new Error('Tour data is required for startTour.'); diff --git a/test/js/test.hopscotch.js b/test/js/test.hopscotch.js index 3365febd..655cdd95 100644 --- a/test/js/test.hopscotch.js +++ b/test/js/test.hopscotch.js @@ -138,6 +138,62 @@ describe('Hopscotch', function() { hopscotch.endTour(); }); + it('should create a div for the HopscotchBubble and append to the specified container element', function() { + var containerEl = document.createElement('div'); + document.body.appendChild(containerEl); + hopscotch.startTour({ + id: 'hopscotch-test-tour', + steps: [ + { + target: 'shopping-list', + placement: 'left', + title: 'Shopping List', + content: 'It\'s a shopping list' + } + ], + container: containerEl + }); + expect(document.querySelector('.hopscotch-bubble').parentElement).toEqual(containerEl); + hopscotch.endTour(); + }); + + it('should create a div for the HopscotchBubble and append to the container element specified by ID', function() { + var containerEl = document.createElement('div'); + containerEl.id = 'container-element'; + document.body.appendChild(containerEl); + hopscotch.startTour({ + id: 'hopscotch-test-tour', + steps: [ + { + target: 'shopping-list', + placement: 'left', + title: 'Shopping List', + content: 'It\'s a shopping list' + } + ], + container: 'container-element' + }); + expect(document.querySelector('.hopscotch-bubble').parentElement).toEqual(containerEl); + hopscotch.endTour(); + }); + + it('should create a div for the HopscotchBubble and append to body, as the specified container element does not exist', function() { + hopscotch.startTour({ + id: 'hopscotch-test-tour', + steps: [ + { + target: 'shopping-list', + placement: 'left', + title: 'Shopping List', + content: 'It\'s a shopping list' + } + ], + container: 'no-element' + }); + expect(document.querySelector('.hopscotch-bubble').parentElement).toEqual(document.body); + hopscotch.endTour(); + }); + it('should start the tour at the specified step when a step number is supplied as an argument', function() { hopscotch.startTour({ id: 'hopscotch-test-tour', @@ -211,7 +267,7 @@ describe('Hopscotch', function() { placement: 'left', title: 'Dynamic target', content: 'It comes and goes, talking of Michelangelo' - }, + } ] };