diff --git a/src/js/hopscotch.js b/src/js/hopscotch.js index 63616bfd..b485b091 100644 --- a/src/js/hopscotch.js +++ b/src/js/hopscotch.js @@ -328,7 +328,7 @@ utils = { }, /** - * 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 @@ -341,7 +341,7 @@ utils = { * * @private */ - getStepTargetHelper: function(target){ + getElementByIdentifier: function(target) { var result = document.getElementById(target); //Backwards compatibility: assume the string is an id @@ -370,6 +370,23 @@ utils = { 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 @@ -389,7 +406,7 @@ utils = { 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. @@ -399,7 +416,7 @@ utils = { 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; @@ -596,6 +613,7 @@ HopscotchBubble.prototype = { left, arrowOffset, verticalLeftPosition, + containerElementOffset, targetEl = utils.getStepTarget(step), el = this.element, arrowEl = this.arrowEl, @@ -610,7 +628,6 @@ HopscotchBubble.prototype = { // SET POSITION boundingRect = targetEl.getBoundingClientRect(); - verticalLeftPosition = step.isRtl ? boundingRect.right - bubbleBoundingWidth : boundingRect.left; if (step.placement === 'top') { @@ -672,6 +689,7 @@ HopscotchBubble.prototype = { else { left += utils.getPixelValue(step.xOffset); } + // VERTICAL OFFSET if (step.yOffset === 'center') { top = (boundingRect.top + targetEl.offsetHeight/2) - (bubbleBoundingHeight / 2); @@ -686,6 +704,20 @@ HopscotchBubble.prototype = { left += utils.getScrollLeft(); } + // CONVERT TO CONTAINER COORDINATES + el.style.top = '0'; + el.style.left = '0'; + + containerElementOffset = el.getBoundingClientRect(); + + top -= containerElementOffset.top; + left -= containerElementOffset.left; + + if (!this.opt.fixedContainer) { + top -= utils.getScrollTop(); + left -= utils.getScrollLeft(); + } + // ACCOUNT FOR FIXED POSITION ELEMENTS el.style.position = (step.fixedElement ? 'fixed' : 'absolute'); @@ -1048,7 +1080,7 @@ HopscotchBubble.prototype = { self = this, resizeCooldown = false, // for updating after window resize onWinResize, - appendToBody, + appendToContainer, children, numChildren, node, @@ -1117,18 +1149,18 @@ HopscotchBubble.prototype = { //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); @@ -1386,26 +1418,28 @@ Hopscotch = function(initOptions) { adjustWindowScroll = function(cb) { var bubble = getBubble(), + // Calculate the current viewport top and bottom + windowTop = utils.getScrollTop(), + windowBottom = windowTop + utils.getWindowHeight(), + + containerTop = utils.getContainer(opt).getBoundingClientRect().top + windowTop, + // Calculate the bubble element top and bottom position bubbleEl = bubble.element, - bubbleTop = utils.getPixelValue(bubbleEl.style.top), + bubbleTop = utils.getPixelValue(bubbleEl.style.top) + containerTop, bubbleBottom = bubbleTop + utils.getPixelValue(bubbleEl.offsetHeight), // Calculate the target element top and bottom position targetEl = utils.getStepTarget(getCurrStep()), targetBounds = targetEl.getBoundingClientRect(), - targetElTop = targetBounds.top + utils.getScrollTop(), - targetElBottom = targetBounds.bottom + utils.getScrollTop(), + targetElTop = targetBounds.top + windowTop, + targetElBottom = targetBounds.bottom + windowTop, // The higher of the two: bubble or target targetTop = (bubbleTop < targetElTop) ? bubbleTop : targetElTop, // The lower of the two: bubble or target targetBottom = (bubbleBottom > targetElBottom) ? bubbleBottom : targetElBottom, - // Calculate the current viewport top and bottom - windowTop = utils.getScrollTop(), - windowBottom = windowTop + utils.getWindowHeight(), - // This is our final target scroll value. scrollToVal = targetTop - getOption('scrollTopMargin'), @@ -1414,7 +1448,6 @@ Hopscotch = function(initOptions) { yuiEase, direction, scrollIncr, - scrollTimeout, scrollTimeoutFn; // Target and bubble are both visible in viewport @@ -1763,7 +1796,7 @@ Hopscotch = function(initOptions) { bubble.render(step, stepNum, function(adjustScroll) { // when done adjusting window scroll, call showBubble helper fn - if (adjustScroll) { + if (adjustScroll && !getOption('fixedContainer')) { adjustWindowScroll(showBubble); } else { @@ -2421,4 +2454,4 @@ winHopscotch = new Hopscotch(); // @@include('../../tmp/js/hopscotch_templates.js') // }.call(winHopscotch)); -export default winHopscotch; \ No newline at end of file +export default winHopscotch; diff --git a/test/js/test.hopscotch.js b/test/js/test.hopscotch.js index 3365febd..23371a36 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',