diff --git a/bower.json b/bower.json index be7da60..426752b 100644 --- a/bower.json +++ b/bower.json @@ -6,7 +6,9 @@ "angularjs", "directive", "angular", - "module" + "module", + "tour", + "walkthrough" ], "homepage": "https://github.com/DaftMonk/angular-tour", "bugs": "https://github.com/DaftMonk/angular-tour/issues", @@ -17,10 +19,10 @@ }, "repository": { "type": "git", - "url": "git://github.com/DaftMonk/angular-tour.git" + "url": "git@github.com:DaftMonk/angular-tour.git" }, "main": [ - "./dist/angular-tour-tpls.min.js", + "./dist/angular-tour-tpls.js", "./dist/angular-tour.css" ], "ignore": [ @@ -34,15 +36,11 @@ "package.json" ], "dependencies": { - "angular": "~1.4.6", - "jquery": "~2.0.3" + "angular": "^1.5.0" }, "devDependencies": { - "angular-mocks": "~1.4.6", - "angular-cookie": "~4.0.1" + "angular-mocks": "^1.5.0", + "angular-cookie": "^4.0.1" }, - "license": "MIT", - "resolutions": { - "angular": "~1.4.6" - } + "license": "MIT" } diff --git a/demo/index.html b/demo/index.html index 181bf86..7f58d03 100644 --- a/demo/index.html +++ b/demo/index.html @@ -18,9 +18,6 @@ - - - @@ -51,7 +48,7 @@ }); - + diff --git a/dist/angular-tour-tpls.js b/dist/angular-tour-tpls.js index 97ccc41..f081258 100644 --- a/dist/angular-tour-tpls.js +++ b/dist/angular-tour-tpls.js @@ -1,6 +1,6 @@ /** * An AngularJS directive for showcasing features of your website - * @version v0.2.5 - 2015-12-10 + * @version v0.2.5 - 2016-03-15 * @link https://github.com/DaftMonk/angular-tour * @author Tyler Henkel * @license MIT License, http://www.opensource.org/licenses/MIT @@ -19,490 +19,561 @@ $templateCache.put('tour/tour.tpl.html', '
\n' + ' \n' + '
\n' + '

\n' + '

\n' + ' \n' + ' ×\n' + '
\n' + '
\n' + ''); } ]); - angular.module('angular-tour.tour', []).constant('tourConfig', { - placement: 'top', - animation: true, - nextLabel: 'Next', - scrollSpeed: 500, - margin: 28, - backDrop: false, - useSourceScope: false, - containerElement: 'body' - }).controller('TourController', [ - '$scope', - 'orderedList', - function ($scope, orderedList) { - var self = this, steps = self.steps = orderedList(), firstCurrentStepChange = true; - // we'll pass these in from the directive - self.postTourCallback = angular.noop; - self.postStepCallback = angular.noop; - self.showStepCallback = angular.noop; - self.currentStep = -1; - // if currentStep changes, select the new step - $scope.$watch(function () { - return self.currentStep; - }, function (val) { - if (firstCurrentStepChange) { - firstCurrentStepChange = false; - } else { - self.select(val); - } - }); - self.select = function (nextIndex) { - if (!angular.isNumber(nextIndex)) - return; - self.unselectAllSteps(); - var step = steps.get(nextIndex); - if (step) { - step.ttOpen = true; - } - // update currentStep if we manually selected this index - if (self.currentStep !== nextIndex) { - self.currentStep = nextIndex; - } - if (self.currentStep > -1) - self.showStepCallback(); - if (nextIndex >= steps.getCount()) { - self.postTourCallback(true); - } - self.postStepCallback(); - }; - self.addStep = function (step) { - if (angular.isNumber(step.index) && !isNaN(step.index)) { - steps.set(step.index, step); - } else { - steps.push(step); - } - }; - self.unselectAllSteps = function () { - steps.forEach(function (step) { - step.ttOpen = false; + (function (angular) { + angular.module('angular-tour.tour', []).constant('tourConfig', { + placement: 'top', + animation: true, + nextLabel: 'Next', + scrollSpeed: 500, + margin: 28, + backDrop: false, + useSourceScope: false, + containerElement: 'body' + }).controller('TourController', [ + '$scope', + 'orderedList', + function ($scope, orderedList) { + var self = this, steps = self.steps = orderedList(), firstCurrentStepChange = true; + // we'll pass these in from the directive + self.postTourCallback = angular.noop; + self.postStepCallback = angular.noop; + self.showStepCallback = angular.noop; + self.currentStep = -1; + // if currentStep changes, select the new step + $scope.$watch(function () { + return self.currentStep; + }, function (val) { + if (firstCurrentStepChange) + firstCurrentStepChange = false; + else + self.select(val); }); - }; - self.cancelTour = function () { - self.unselectAllSteps(); - self.postTourCallback(false); - }; - $scope.openTour = function () { - // open at first step if we've already finished tour - var startStep = self.currentStep >= steps.getCount() || self.currentStep < 0 ? 0 : self.currentStep; - self.select(startStep); - }; - $scope.closeTour = function () { - self.cancelTour(); - }; - } - ]).directive('tour', [ - '$parse', - '$timeout', - 'tourConfig', - function ($parse, $timeout, tourConfig) { - return { - controller: 'TourController', - restrict: 'EA', - scope: true, - link: function (scope, element, attrs, ctrl) { - if (!angular.isDefined(attrs.step)) { - throw 'The directive requires a `step` attribute to bind the current step to.'; + self.select = function (nextIndex) { + if (!angular.isNumber(nextIndex)) + return; + self.unselectAllSteps(); + var step = steps.get(nextIndex); + if (step) { + step.ttOpen = true; + } + // update currentStep if we manually selected this index + if (self.currentStep !== nextIndex) { + self.currentStep = nextIndex; } - var model = $parse(attrs.step); - var backDrop = false; - // Watch current step view model and update locally - scope.$watch(attrs.step, function (newVal) { - ctrl.currentStep = newVal; + if (self.currentStep > -1) { + self.showStepCallback(); + } + if (nextIndex >= steps.getCount()) { + self.postTourCallback(true); + } + self.postStepCallback(); + }; + self.addStep = function (step) { + if (angular.isNumber(step.index) && !isNaN(step.index)) + steps.set(step.index, step); + else + steps.push(step); + }; + self.unselectAllSteps = function () { + steps.forEach(function (step) { + step.ttOpen = false; }); - ctrl.postTourCallback = function (completed) { - angular.element('.tour-backdrop').remove(); - backDrop = false; - angular.element('.tour-element-active').removeClass('tour-element-active'); - if (completed && angular.isDefined(attrs.tourComplete)) { - scope.$parent.$eval(attrs.tourComplete); - } - if (angular.isDefined(attrs.postTour)) { - scope.$parent.$eval(attrs.postTour); - } - }; - ctrl.postStepCallback = function () { - if (angular.isDefined(attrs.postStep)) { - scope.$parent.$eval(attrs.postStep); - } - }; - ctrl.showStepCallback = function () { - if (tourConfig.backDrop) { - angular.element(tourConfig.containerElement).append(angular.element('
')); - $timeout(function () { - $('.tour-backdrop').remove(); - angular.element('
').insertBefore('.tour-tip'); - }, 1000); - backDrop = true; + }; + self.cancelTour = function () { + self.unselectAllSteps(); + self.postTourCallback(false); + }; + $scope.openTour = function () { + // open at first step if we've already finished tour + var startStep = self.currentStep >= steps.getCount() || self.currentStep < 0 ? 0 : self.currentStep; + self.select(startStep); + }; + $scope.closeTour = function () { + self.cancelTour(); + }; + } + ]).directive('tour', [ + '$parse', + '$timeout', + 'tourConfig', + function ($parse, $timeout, tourConfig) { + return { + controller: 'TourController', + restrict: 'EA', + scope: true, + link: function (scope, element, attrs, ctrl) { + if (!angular.isDefined(attrs.step)) { + throw 'The directive requires a `step` attribute to bind the current step to.'; } - }; - // update the current step in the view as well as in our controller - scope.setCurrentStep = function (val) { - model.assign(scope.$parent, val); - ctrl.currentStep = val; - }; - scope.getCurrentStep = function () { - return ctrl.currentStep; - }; - } - }; - } - ]).directive('tourtip', [ - '$window', - '$compile', - '$interpolate', - '$timeout', - 'scrollTo', - 'tourConfig', - 'debounce', - '$q', - function ($window, $compile, $interpolate, $timeout, scrollTo, tourConfig, debounce, $q) { - var startSym = $interpolate.startSymbol(), endSym = $interpolate.endSymbol(); - var template = '
'; - return { - require: '^tour', - restrict: 'EA', - scope: true, - link: function (scope, element, attrs, tourCtrl) { - attrs.$observe('tourtip', function (val) { - scope.ttContent = val; - }); - //defaults: tourConfig.placement - attrs.$observe('tourtipPlacement', function (val) { - scope.ttPlacement = (val || tourConfig.placement).toLowerCase().trim(); - scope.centered = scope.ttPlacement.indexOf('center') === 0; - }); - attrs.$observe('tourtipNextLabel', function (val) { - scope.ttNextLabel = val || tourConfig.nextLabel; - }); - attrs.$observe('tourtipContainerElement', function (val) { - scope.ttContainerElement = val || tourConfig.containerElement; - }); - attrs.$observe('tourtipMargin', function (val) { - scope.ttMargin = parseInt(val, 10) || tourConfig.margin; - }); - attrs.$observe('tourtipOffsetVertical', function (val) { - scope.offsetVertical = parseInt(val, 10) || 0; - }); - attrs.$observe('tourtipOffsetHorizontal', function (val) { - scope.offsetHorizontal = parseInt(val, 10) || 0; - }); - //defaults: null - attrs.$observe('onShow', function (val) { - scope.onStepShow = val || null; - }); - //defaults: null - attrs.$observe('onProceed', function (val) { - scope.onStepProceed = val || null; - }); - //defaults: null - attrs.$observe('tourtipElement', function (val) { - scope.ttElement = val || null; - }); - //defaults: null - attrs.$observe('tourtipTitle', function (val) { - scope.ttTitle = val || null; - }); - //defaults: tourConfig.useSourceScope - attrs.$observe('useSourceScope', function (val) { - scope.ttSourceScope = !val ? tourConfig.useSourceScope : val === 'true'; - }); - //Init assignments (fix for Angular 1.3+) - scope.ttNextLabel = tourConfig.nextLabel; - scope.ttContainerElement = tourConfig.containerElement; - scope.ttPlacement = tourConfig.placement.toLowerCase().trim(); - scope.centered = false; - scope.ttMargin = tourConfig.margin; - scope.offsetHorizontal = 0; - scope.offsetVertical = 0; - scope.ttSourceScope = tourConfig.useSourceScope; - scope.ttOpen = false; - scope.ttAnimation = tourConfig.animation; - scope.index = parseInt(attrs.tourtipStep, 10); - var tourtip = $compile(template)(scope); - tourCtrl.addStep(scope); - // wrap this in a time out because the tourtip won't compile right away - $timeout(function () { - scope.$watch('ttOpen', function (val) { - if (val) { - show(); - } else { - hide(); - } + var model = $parse(attrs.step); + var backDrop = false; + // Watch current step view model and update locally + scope.$watch(attrs.step, function (newVal) { + ctrl.currentStep = newVal; }); - }, 500); - //determining target scope. It's used only when using virtual steps and there - //is some action performed like on-show or on-progress. Without virtual steps - //action would performed on element's scope and that would work just fine - //however, when using virtual steps, whose steps can be placed in different - //controller, so it affects scope, which will be used to run this action against. - function getTargetScope() { - var targetElement = scope.ttElement ? angular.element(scope.ttElement) : element; - var targetScope = scope; - if (targetElement !== element && !scope.ttSourceScope) - targetScope = targetElement.scope(); - return targetScope; + ctrl.postTourCallback = function (completed) { + var backdropEle = document.getElementsByClassName('tour-backdrop'); + var active = document.getElementsByClassName('tour-element-active'); + angular.element(backdropEle).remove(); + backDrop = false; + angular.element(active).removeClass('tour-element-active'); + if (completed && angular.isDefined(attrs.tourComplete)) { + scope.$parent.$eval(attrs.tourComplete); + } + if (angular.isDefined(attrs.postTour)) { + scope.$parent.$eval(attrs.postTour); + } + }; + ctrl.postStepCallback = function () { + if (angular.isDefined(attrs.postStep)) { + scope.$parent.$eval(attrs.postStep); + } + }; + ctrl.showStepCallback = function () { + if (tourConfig.backDrop) { + $timeout(function () { + var backdrop = document.getElementsByClassName('tour-backdrop'); + var tooltip = document.getElementsByClassName('tour-tip')[0]; + var div = document.createElement('div'); + div.className = 'tour-backdrop'; + angular.element(backdrop).remove(); + // When the tour ends simply remove the backdrop and return. + if (!angular.isDefined(tooltip)) { + return; + } + tooltip.parentNode.insertBefore(div, tooltip); + }, 501); + backDrop = true; + } + }; + // update the current step in the view as well as in our controller + scope.setCurrentStep = function (val) { + model.assign(scope.$parent, val); + ctrl.currentStep = val; + }; + scope.getCurrentStep = function () { + return ctrl.currentStep; + }; } - function calculatePosition(element, container) { - var minimumLeft = 0; - // minimum left position of tour tip - var restrictRight; - var ttPosition; - // Get the position of the directive element - var position = element[0].getBoundingClientRect(); - //make it relative against page or fixed container, not the window - var top = position.top + window.pageYOffset; - var containerLeft = 0; - if (container && container[0]) { - top = top - container[0].getBoundingClientRect().top + container[0].scrollTop; - // if container is fixed, position tour tip relative to fixed container - if (container.css('position') === 'fixed') { - containerLeft = container[0].getBoundingClientRect().left; + }; + } + ]).directive('tourtip', [ + '$window', + '$compile', + '$interpolate', + '$timeout', + 'scrollTo', + 'tourConfig', + 'debounce', + '$q', + function ($window, $compile, $interpolate, $timeout, scrollTo, tourConfig, debounce, $q) { + var startSym = $interpolate.startSymbol(), endSym = $interpolate.endSymbol(); + var template = '
'; + return { + require: '^tour', + restrict: 'EA', + scope: true, + link: function (scope, element, attrs, tourCtrl) { + attrs.$observe('tourtip', function (val) { + scope.ttContent = val; + }); + //defaults: tourConfig.placement + attrs.$observe('tourtipPlacement', function (val) { + scope.ttPlacement = (val || tourConfig.placement).toLowerCase().trim(); + scope.centered = scope.ttPlacement.indexOf('center') === 0; + }); + attrs.$observe('tourtipNextLabel', function (val) { + scope.ttNextLabel = val || tourConfig.nextLabel; + }); + attrs.$observe('tourtipContainerElement', function (val) { + scope.ttContainerElement = val || tourConfig.containerElement; + }); + attrs.$observe('tourtipMargin', function (val) { + scope.ttMargin = parseInt(val, 10) || tourConfig.margin; + }); + attrs.$observe('tourtipOffsetVertical', function (val) { + scope.offsetVertical = parseInt(val, 10) || 0; + }); + attrs.$observe('tourtipOffsetHorizontal', function (val) { + scope.offsetHorizontal = parseInt(val, 10) || 0; + }); + //defaults: null + attrs.$observe('onShow', function (val) { + scope.onStepShow = val || null; + }); + //defaults: null + attrs.$observe('onProceed', function (val) { + scope.onStepProceed = val || null; + }); + //defaults: null + attrs.$observe('tourtipElement', function (val) { + scope.ttElement = val || null; + }); + //defaults: null + attrs.$observe('tourtipTitle', function (val) { + scope.ttTitle = val || null; + }); + //defaults: tourConfig.useSourceScope + attrs.$observe('useSourceScope', function (val) { + scope.ttSourceScope = !val ? tourConfig.useSourceScope : val === 'true'; + }); + //Init assignments (fix for Angular 1.3+) + scope.ttNextLabel = tourConfig.nextLabel; + scope.ttContainerElement = tourConfig.containerElement; + scope.ttPlacement = tourConfig.placement.toLowerCase().trim(); + scope.centered = false; + scope.ttMargin = tourConfig.margin; + scope.offsetHorizontal = 0; + scope.offsetVertical = 0; + scope.ttSourceScope = tourConfig.useSourceScope; + scope.ttOpen = false; + scope.ttAnimation = tourConfig.animation; + scope.index = parseInt(attrs.tourtipStep, 10); + var tourtip = $compile(template)(scope); + tourCtrl.addStep(scope); + // wrap this in a time out because the tourtip won't compile right away + $timeout(function () { + scope.$watch('ttOpen', function (val) { + if (val) + show(); + else + hide(); + }); + }, 500); + //determining target scope. It's used only when using virtual steps and there + //is some action performed like on-show or on-progress. Without virtual steps + //action would performed on element's scope and that would work just fine + //however, when using virtual steps, whose steps can be placed in different + //controller, so it affects scope, which will be used to run this action against. + function getTargetScope() { + var target = document.querySelectorAll(scope.ttElement); + var targetElement = scope.ttElement ? angular.element(target) : element; + var targetScope = scope; + if (targetElement !== element && !scope.ttSourceScope) { + targetScope = targetElement.scope(); + } + return targetScope; + } + function calculatePosition(element, container) { + var minimumLeft = 0; + // minimum left position of tour tip + var restrictRight; + var ttPosition; + var tourtipWidth = tourtip[0].offsetWidth; + var tourtipHeight = tourtip[0].offsetHeight; + // Get the position of the directive element + var position = element[0].getBoundingClientRect(); + //make it relative against page or fixed container, not the window + var top = position.top + window.pageYOffset; + var containerLeft = 0; + if (container && container[0]) { + top = top - container[0].getBoundingClientRect().top + container[0].scrollTop; + // if container is fixed, position tour tip relative to fixed container + if (container.css('position') === 'fixed') { + containerLeft = container[0].getBoundingClientRect().left; + } + // restrict right position if the tourtip doesn't fit in the container + var containerWidth = container[0].getBoundingClientRect().width; + if (tourtipWidth + position.width > containerWidth) { + restrictRight = containerWidth - position.left + scope.ttMargin; + } } - // restrict right position if the tourtip doesn't fit in the container - var containerWidth = container[0].getBoundingClientRect().width; - if (tourtip.width() + position.width > containerWidth) { - restrictRight = containerWidth - position.left + scope.ttMargin; + var ttWidth = tourtipWidth; + var ttHeight = tourtipHeight; + // Calculate the tourtip's top and left coordinates to center it + var _left; + switch (scope.ttPlacement) { + case 'right': + _left = position.left - containerLeft + position.width + scope.ttMargin + scope.offsetHorizontal; + ttPosition = { + top: top + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'bottom': + _left = position.left - containerLeft + scope.offsetHorizontal; + ttPosition = { + top: top + position.height + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'center': + _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; + ttPosition = { + top: top + 0.5 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'center-top': + _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; + ttPosition = { + top: top + 0.1 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'left': + _left = position.left - containerLeft - ttWidth - scope.ttMargin + scope.offsetHorizontal; + ttPosition = { + top: top + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft, + right: restrictRight + }; + break; + default: + _left = position.left - containerLeft + scope.offsetHorizontal; + ttPosition = { + top: top - ttHeight - scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; } + ttPosition.top += 'px'; + ttPosition.left += 'px'; + return ttPosition; } - var ttWidth = tourtip.width(); - var ttHeight = tourtip.height(); - // Calculate the tourtip's top and left coordinates to center it - switch (scope.ttPlacement) { - case 'right': - var _left = position.left - containerLeft + position.width + scope.ttMargin + scope.offsetHorizontal; - ttPosition = { - top: top + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'bottom': - var _left = position.left - containerLeft + scope.offsetHorizontal; - ttPosition = { - top: top + position.height + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'center': - var _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; - ttPosition = { - top: top + 0.5 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'center-top': - var _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; - ttPosition = { - top: top + 0.1 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'left': - var _left = position.left - containerLeft - ttWidth - scope.ttMargin + scope.offsetHorizontal; - ttPosition = { - top: top + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft, - right: restrictRight - }; - break; - default: - var _left = position.left - containerLeft + scope.offsetHorizontal; - ttPosition = { - top: top - ttHeight - scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft + function show() { + if (!scope.ttContent) { + return; + } + var target = document.querySelectorAll(scope.ttElement); + var targetElement = scope.ttElement ? angular.element(target) : element; + if (targetElement === null || targetElement.length === 0) + throw 'Target element could not be found. Selector: ' + scope.ttElement; + var containerEle = document.querySelectorAll(scope.ttContainerElement); + angular.element(containerEle).append(tourtip); + var updatePosition = function () { + var offsetElement = scope.ttContainerElement === 'body' ? undefined : angular.element(containerEle); + var ttPosition = calculatePosition(targetElement, offsetElement); + // Now set the calculated positioning. + tourtip.css(ttPosition); + // Scroll to the tour tip + scrollTo(tourtip, scope.ttContainerElement, -150, -300, tourConfig.scrollSpeed); }; - break; + if (tourConfig.backDrop) { + focusActiveElement(targetElement); + } + angular.element($window).bind('resize.' + scope.$id, debounce(updatePosition, 50)); + updatePosition(); + // CSS class must be added after the element is already on the DOM otherwise it won't animate (fade in). + tourtip.addClass('show'); + if (scope.onStepShow) { + var targetScope = getTargetScope(); + //fancy! Let's make on show action not instantly, but after a small delay + $timeout(function () { + targetScope.$eval(scope.onStepShow); + }, 300); + } } - ttPosition.top += 'px'; - ttPosition.left += 'px'; - return ttPosition; - } - function show() { - if (!scope.ttContent) { - return; + function hide() { + tourtip.removeClass('show'); + tourtip.detach(); + angular.element($window).unbind('resize.' + scope.$id); } - if (scope.ttAnimation) - tourtip.fadeIn(); - else { - tourtip.css({ display: 'block' }); + function focusActiveElement(el) { + var activeEle = document.getElementsByClassName('tour-element-active'); + angular.element(activeEle).removeClass('tour-element-active'); + if (!scope.centered) { + el.addClass('tour-element-active'); + } } - var targetElement = scope.ttElement ? angular.element(scope.ttElement) : element; - if (targetElement == null || targetElement.length === 0) - throw 'Target element could not be found. Selector: ' + scope.ttElement; - angular.element(scope.ttContainerElement).append(tourtip); - var updatePosition = function () { - var offsetElement = scope.ttContainerElement === 'body' ? undefined : angular.element(scope.ttContainerElement); - var ttPosition = calculatePosition(targetElement, offsetElement); - // Now set the calculated positioning. - tourtip.css(ttPosition); - // Scroll to the tour tip - var ttPositionTop = parseInt(ttPosition.top), ttPositionLeft = parseInt(ttPosition.left); - scrollTo(tourtip, scope.ttContainerElement, -150, -300, tourConfig.scrollSpeed, ttPositionTop, ttPositionLeft); + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTourtip() { + angular.element($window).unbind('resize.' + scope.$id); + tourtip.remove(); + tourtip = null; + }); + scope.proceed = function () { + if (scope.onStepProceed) { + var targetScope = getTargetScope(); + var onProceedResult = targetScope.$eval(scope.onStepProceed); + $q.resolve(onProceedResult).then(function () { + scope.setCurrentStep(scope.getCurrentStep() + 1); + }); + } else { + scope.setCurrentStep(scope.getCurrentStep() + 1); + } }; - if (tourConfig.backDrop) - focusActiveElement(targetElement); - angular.element($window).bind('resize.' + scope.$id, debounce(updatePosition, 50)); - updatePosition(); - if (scope.onStepShow) { - var targetScope = getTargetScope(); - //fancy! Let's make on show action not instantly, but after a small delay - $timeout(function () { - targetScope.$eval(scope.onStepShow); - }, 300); - } - } - function hide() { - tourtip.detach(); - angular.element($window).unbind('resize.' + scope.$id); } - function focusActiveElement(el) { - angular.element('.tour-element-active').removeClass('tour-element-active'); - if (!scope.centered) - el.addClass('tour-element-active'); + }; + } + ]).directive('tourPopup', function () { + return { + replace: true, + templateUrl: 'tour/tour.tpl.html', + scope: true, + restrict: 'EA', + link: function (scope, element, attrs) { + } + }; + }).factory('orderedList', function () { + var OrderedList = function () { + this.map = {}; + this._array = []; + }; + OrderedList.prototype.set = function (key, value) { + if (!angular.isNumber(key)) + return; + if (key in this.map) { + this.map[key] = value; + } else { + if (key < this._array.length) { + var insertIndex = key - 1 > 0 ? key - 1 : 0; + this._array.splice(insertIndex, 0, key); + } else { + this._array.push(key); } - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTourtip() { - angular.element($window).unbind('resize.' + scope.$id); - tourtip.remove(); - tourtip = null; + this.map[key] = value; + this._array.sort(function (a, b) { + return a - b; }); - scope.proceed = function () { - if (scope.onStepProceed) { - var targetScope = getTargetScope(); - var onProceedResult = targetScope.$eval(scope.onStepProceed); - $q.resolve(onProceedResult).then(function () { - scope.setCurrentStep(scope.getCurrentStep() + 1); - }); - } else { - scope.setCurrentStep(scope.getCurrentStep() + 1); - } - }; } }; - } - ]).directive('tourPopup', function () { - return { - replace: true, - templateUrl: 'tour/tour.tpl.html', - scope: true, - restrict: 'EA', - link: function (scope, element, attrs) { - } - }; - }).factory('orderedList', function () { - var OrderedList = function () { - this.map = {}; - this._array = []; - }; - OrderedList.prototype.set = function (key, value) { - if (!angular.isNumber(key)) - return; - if (key in this.map) { - this.map[key] = value; - } else { - if (key < this._array.length) { - var insertIndex = key - 1 > 0 ? key - 1 : 0; - this._array.splice(insertIndex, 0, key); - } else { - this._array.push(key); + OrderedList.prototype.indexOf = function (value) { + for (var prop in this.map) { + if (this.map.hasOwnProperty(prop)) { + if (this.map[prop] === value) + return Number(prop); + } } + }; + OrderedList.prototype.push = function (value) { + var key = this._array[this._array.length - 1] + 1 || 0; + this._array.push(key); this.map[key] = value; this._array.sort(function (a, b) { return a - b; }); - } - }; - OrderedList.prototype.indexOf = function (value) { - for (var prop in this.map) { - if (this.map.hasOwnProperty(prop)) { - if (this.map[prop] === value) - return Number(prop); + }; + OrderedList.prototype.remove = function (key) { + var index = this._array.indexOf(key); + if (index === -1) { + throw new Error('key does not exist'); } - } - }; - OrderedList.prototype.push = function (value) { - var key = this._array[this._array.length - 1] + 1 || 0; - this._array.push(key); - this.map[key] = value; - this._array.sort(function (a, b) { - return a - b; - }); - }; - OrderedList.prototype.remove = function (key) { - var index = this._array.indexOf(key); - if (index === -1) { - throw new Error('key does not exist'); - } - this._array.splice(index, 1); - delete this.map[key]; - }; - OrderedList.prototype.get = function (key) { - return this.map[key]; - }; - OrderedList.prototype.getCount = function () { - return this._array.length; - }; - OrderedList.prototype.forEach = function (f) { - var key, value; - for (var i = 0; i < this._array.length; i++) { - key = this._array[i]; + this._array.splice(index, 1); + delete this.map[key]; + }; + OrderedList.prototype.get = function (key) { + return this.map[key]; + }; + OrderedList.prototype.getCount = function () { + return this._array.length; + }; + OrderedList.prototype.forEach = function (f) { + var key, value; + for (var i = 0; i < this._array.length; i++) { + key = this._array[i]; + value = this.map[key]; + f(value, key); + } + }; + OrderedList.prototype.first = function () { + var key, value; + key = this._array[0]; value = this.map[key]; - f(value, key); - } - }; - OrderedList.prototype.first = function () { - var key, value; - key = this._array[0]; - value = this.map[key]; - return value; - }; - var orderedListFactory = function () { - return new OrderedList(); - }; - return orderedListFactory; - }).factory('scrollTo', function () { - return function (target, containerElement, offsetY, offsetX, speed, ttPositionTop, ttPositionLeft) { - if (target) { - offsetY = offsetY || -100; - offsetX = offsetX || -100; - speed = speed || 500; - $('html,' + containerElement).stop().animate({ - scrollTop: ttPositionTop + offsetY, - scrollLeft: ttPositionLeft + offsetX - }, speed); - } else { - $('html,' + containerElement).stop().animate({ scrollTop: 0 }, speed); + return value; + }; + var orderedListFactory = function () { + return new OrderedList(); + }; + return orderedListFactory; + }).factory('scrollTo', [ + '$interval', + function ($interval) { + var animationInProgress = false; + function getEasingPattern(time) { + return time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // default easeInOutCubic transition + } + function _autoScroll(container, endTop, endLeft, offsetY, offsetX, speed) { + if (animationInProgress) { + return; + } + speed = speed || 500; + offsetY = offsetY || 0; + offsetX = offsetX || 0; + // Set some boundaries in case the offset wants us to scroll to impossible locations + var finalY = endTop + offsetY; + if (finalY < 0) { + finalY = 0; + } else if (finalY > container.scrollHeight) { + finalY = container.scrollHeight; + } + var finalX = endLeft + offsetX; + if (finalX < 0) { + finalX = 0; + } else if (finalX > container.scrollWidth) { + finalX = container.scrollWidth; + } + var startTop = container.scrollTop, startLeft = container.scrollLeft, timeLapsed = 0, distanceY = finalY - startTop, + // If we're going up, this will be a negative number + distanceX = finalX - startLeft, currentPositionY, currentPositionX, timeProgress; + function stopAnimation() { + // If we have reached our destination clear the interval + if (currentPositionY === finalY && currentPositionX === finalX) { + $interval.cancel(runAnimation); + animationInProgress = false; + } + } + function animateScroll() { + console.log('called'); + timeLapsed += 16; + // get percentage of progress to the specified speed (e.g. 16/500). Should always be between 0 and 1 + timeProgress = timeLapsed / speed; + // Make a check and set back to 1 if we went over (e.g. 512/500) + timeProgress = timeProgress > 1 ? 1 : timeProgress; + // Number between 0 and 1 corresponding to the animation pattern + var multiplier = getEasingPattern(timeProgress); + // Calculate the distance to travel in this step. It is the total distance times a percentage of what we will move + var translateY = distanceY * multiplier; + var translateX = distanceX * multiplier; + // Assign to the shorthand variables + currentPositionY = startTop + translateY; + currentPositionX = startLeft + translateX; + // Move slightly following the easing pattern + container.scrollTop = currentPositionY; + container.scrollLeft = currentPositionX; + // Check if we have reached our destination + stopAnimation(); + } + animationInProgress = true; + // Kicks off the function + var runAnimation = $interval(animateScroll, 16); + } + return function (target, containerSelector, offsetY, offsetX, speed) { + var container = document.querySelectorAll(containerSelector); + offsetY = offsetY || -100; + offsetX = offsetX || -100; + _autoScroll(container[0], target[0].offsetTop, target[0].offsetLeft, offsetY, offsetX, speed); + }; } - }; - }).factory('debounce', [ - '$timeout', - '$q', - function ($timeout, $q) { - return function (func, wait, immediate) { - var timeout; - var deferred = $q.defer(); - return function () { - var context = this, args = arguments; - var later = function () { - timeout = null; - if (!immediate) { + ]).factory('debounce', [ + '$timeout', + '$q', + function ($timeout, $q) { + return function (func, wait, immediate) { + var timeout; + var deferred = $q.defer(); + return function () { + var context = this, args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + deferred.resolve(func.apply(context, args)); + deferred = $q.defer(); + } + }; + var callNow = immediate && !timeout; + if (timeout) { + $timeout.cancel(timeout); + } + timeout = $timeout(later, wait); + if (callNow) { deferred.resolve(func.apply(context, args)); deferred = $q.defer(); } + return deferred.promise; }; - var callNow = immediate && !timeout; - if (timeout) { - $timeout.cancel(timeout); - } - timeout = $timeout(later, wait); - if (callNow) { - deferred.resolve(func.apply(context, args)); - deferred = $q.defer(); - } - return deferred.promise; }; - }; - } - ]); + } + ]); + }(angular)); }(window, document)); \ No newline at end of file diff --git a/dist/angular-tour-tpls.min.js b/dist/angular-tour-tpls.min.js index aa1728c..99beecf 100644 --- a/dist/angular-tour-tpls.min.js +++ b/dist/angular-tour-tpls.min.js @@ -1 +1 @@ -!function(a,b,c){"use strict";angular.module("angular-tour",["angular-tour.tpls","angular-tour.tour"]),angular.module("angular-tour.tpls",["tour/tour.tpl.html"]),angular.module("tour/tour.tpl.html",[]).run(["$templateCache",function(a){a.put("tour/tour.tpl.html",'
\n \n
\n

\n

\n \n ×\n
\n
\n')}]),angular.module("angular-tour.tour",[]).constant("tourConfig",{placement:"top",animation:!0,nextLabel:"Next",scrollSpeed:500,margin:28,backDrop:!1,useSourceScope:!1,containerElement:"body"}).controller("TourController",["$scope","orderedList",function(a,b){var c=this,d=c.steps=b(),e=!0;c.postTourCallback=angular.noop,c.postStepCallback=angular.noop,c.showStepCallback=angular.noop,c.currentStep=-1,a.$watch(function(){return c.currentStep},function(a){e?e=!1:c.select(a)}),c.select=function(a){if(angular.isNumber(a)){c.unselectAllSteps();var b=d.get(a);b&&(b.ttOpen=!0),c.currentStep!==a&&(c.currentStep=a),c.currentStep>-1&&c.showStepCallback(),a>=d.getCount()&&c.postTourCallback(!0),c.postStepCallback()}},c.addStep=function(a){angular.isNumber(a.index)&&!isNaN(a.index)?d.set(a.index,a):d.push(a)},c.unselectAllSteps=function(){d.forEach(function(a){a.ttOpen=!1})},c.cancelTour=function(){c.unselectAllSteps(),c.postTourCallback(!1)},a.openTour=function(){var a=c.currentStep>=d.getCount()||c.currentStep<0?0:c.currentStep;c.select(a)},a.closeTour=function(){c.cancelTour()}}]).directive("tour",["$parse","$timeout","tourConfig",function(a,b,c){return{controller:"TourController",restrict:"EA",scope:!0,link:function(d,e,f,g){if(!angular.isDefined(f.step))throw"The directive requires a `step` attribute to bind the current step to.";var h=a(f.step),i=!1;d.$watch(f.step,function(a){g.currentStep=a}),g.postTourCallback=function(a){angular.element(".tour-backdrop").remove(),i=!1,angular.element(".tour-element-active").removeClass("tour-element-active"),a&&angular.isDefined(f.tourComplete)&&d.$parent.$eval(f.tourComplete),angular.isDefined(f.postTour)&&d.$parent.$eval(f.postTour)},g.postStepCallback=function(){angular.isDefined(f.postStep)&&d.$parent.$eval(f.postStep)},g.showStepCallback=function(){c.backDrop&&(angular.element(c.containerElement).append(angular.element('
')),b(function(){$(".tour-backdrop").remove(),angular.element('
').insertBefore(".tour-tip")},1e3),i=!0)},d.setCurrentStep=function(a){h.assign(d.$parent,a),g.currentStep=a},d.getCurrentStep=function(){return g.currentStep}}}}]).directive("tourtip",["$window","$compile","$interpolate","$timeout","scrollTo","tourConfig","debounce","$q",function(b,d,e,f,g,h,i,j){var k=(e.startSymbol(),e.endSymbol(),"
");return{require:"^tour",restrict:"EA",scope:!0,link:function(e,l,m,n){function o(){var a=e.ttElement?angular.element(e.ttElement):l,b=e;return a===l||e.ttSourceScope||(b=a.scope()),b}function p(b,c){var d,f,g=0,h=b[0].getBoundingClientRect(),i=h.top+a.pageYOffset,j=0;if(c&&c[0]){i=i-c[0].getBoundingClientRect().top+c[0].scrollTop,"fixed"===c.css("position")&&(j=c[0].getBoundingClientRect().left);var k=c[0].getBoundingClientRect().width;t.width()+h.width>k&&(d=k-h.left+e.ttMargin)}var l=t.width(),m=t.height();switch(e.ttPlacement){case"right":var n=h.left-j+h.width+e.ttMargin+e.offsetHorizontal;f={top:i+e.offsetVertical,left:n>0?n:g};break;case"bottom":var n=h.left-j+e.offsetHorizontal;f={top:i+h.height+e.ttMargin+e.offsetVertical,left:n>0?n:g};break;case"center":var n=h.left-j+.5*(h.width-l)+e.offsetHorizontal;f={top:i+.5*(h.height-m)+e.ttMargin+e.offsetVertical,left:n>0?n:g};break;case"center-top":var n=h.left-j+.5*(h.width-l)+e.offsetHorizontal;f={top:i+.1*(h.height-m)+e.ttMargin+e.offsetVertical,left:n>0?n:g};break;case"left":var n=h.left-j-l-e.ttMargin+e.offsetHorizontal;f={top:i+e.offsetVertical,left:n>0?n:g,right:d};break;default:var n=h.left-j+e.offsetHorizontal;f={top:i-m-e.ttMargin+e.offsetVertical,left:n>0?n:g}}return f.top+="px",f.left+="px",f}function q(){if(e.ttContent){e.ttAnimation?t.fadeIn():t.css({display:"block"});var a=e.ttElement?angular.element(e.ttElement):l;if(null==a||0===a.length)throw"Target element could not be found. Selector: "+e.ttElement;angular.element(e.ttContainerElement).append(t);var d=function(){var b="body"===e.ttContainerElement?c:angular.element(e.ttContainerElement),d=p(a,b);t.css(d);var f=parseInt(d.top),i=parseInt(d.left);g(t,e.ttContainerElement,-150,-300,h.scrollSpeed,f,i)};if(h.backDrop&&s(a),angular.element(b).bind("resize."+e.$id,i(d,50)),d(),e.onStepShow){var j=o();f(function(){j.$eval(e.onStepShow)},300)}}}function r(){t.detach(),angular.element(b).unbind("resize."+e.$id)}function s(a){angular.element(".tour-element-active").removeClass("tour-element-active"),e.centered||a.addClass("tour-element-active")}m.$observe("tourtip",function(a){e.ttContent=a}),m.$observe("tourtipPlacement",function(a){e.ttPlacement=(a||h.placement).toLowerCase().trim(),e.centered=0===e.ttPlacement.indexOf("center")}),m.$observe("tourtipNextLabel",function(a){e.ttNextLabel=a||h.nextLabel}),m.$observe("tourtipContainerElement",function(a){e.ttContainerElement=a||h.containerElement}),m.$observe("tourtipMargin",function(a){e.ttMargin=parseInt(a,10)||h.margin}),m.$observe("tourtipOffsetVertical",function(a){e.offsetVertical=parseInt(a,10)||0}),m.$observe("tourtipOffsetHorizontal",function(a){e.offsetHorizontal=parseInt(a,10)||0}),m.$observe("onShow",function(a){e.onStepShow=a||null}),m.$observe("onProceed",function(a){e.onStepProceed=a||null}),m.$observe("tourtipElement",function(a){e.ttElement=a||null}),m.$observe("tourtipTitle",function(a){e.ttTitle=a||null}),m.$observe("useSourceScope",function(a){e.ttSourceScope=a?"true"===a:h.useSourceScope}),e.ttNextLabel=h.nextLabel,e.ttContainerElement=h.containerElement,e.ttPlacement=h.placement.toLowerCase().trim(),e.centered=!1,e.ttMargin=h.margin,e.offsetHorizontal=0,e.offsetVertical=0,e.ttSourceScope=h.useSourceScope,e.ttOpen=!1,e.ttAnimation=h.animation,e.index=parseInt(m.tourtipStep,10);var t=d(k)(e);n.addStep(e),f(function(){e.$watch("ttOpen",function(a){a?q():r()})},500),e.$on("$destroy",function(){angular.element(b).unbind("resize."+e.$id),t.remove(),t=null}),e.proceed=function(){if(e.onStepProceed){var a=o(),b=a.$eval(e.onStepProceed);j.resolve(b).then(function(){e.setCurrentStep(e.getCurrentStep()+1)})}else e.setCurrentStep(e.getCurrentStep()+1)}}}}]).directive("tourPopup",function(){return{replace:!0,templateUrl:"tour/tour.tpl.html",scope:!0,restrict:"EA",link:function(a,b,c){}}}).factory("orderedList",function(){var a=function(){this.map={},this._array=[]};a.prototype.set=function(a,b){if(angular.isNumber(a))if(a in this.map)this.map[a]=b;else{if(a0?a-1:0;this._array.splice(c,0,a)}else this._array.push(a);this.map[a]=b,this._array.sort(function(a,b){return a-b})}},a.prototype.indexOf=function(a){for(var b in this.map)if(this.map.hasOwnProperty(b)&&this.map[b]===a)return Number(b)},a.prototype.push=function(a){var b=this._array[this._array.length-1]+1||0;this._array.push(b),this.map[b]=a,this._array.sort(function(a,b){return a-b})},a.prototype.remove=function(a){var b=this._array.indexOf(a);if(-1===b)throw new Error("key does not exist");this._array.splice(b,1),delete this.map[a]},a.prototype.get=function(a){return this.map[a]},a.prototype.getCount=function(){return this._array.length},a.prototype.forEach=function(a){for(var b,c,d=0;d\n \n
\n

\n

\n \n ×\n
\n\n')}]),function(d){d.module("angular-tour.tour",[]).constant("tourConfig",{placement:"top",animation:!0,nextLabel:"Next",scrollSpeed:500,margin:28,backDrop:!1,useSourceScope:!1,containerElement:"body"}).controller("TourController",["$scope","orderedList",function(a,b){var c=this,e=c.steps=b(),f=!0;c.postTourCallback=d.noop,c.postStepCallback=d.noop,c.showStepCallback=d.noop,c.currentStep=-1,a.$watch(function(){return c.currentStep},function(a){f?f=!1:c.select(a)}),c.select=function(a){if(d.isNumber(a)){c.unselectAllSteps();var b=e.get(a);b&&(b.ttOpen=!0),c.currentStep!==a&&(c.currentStep=a),c.currentStep>-1&&c.showStepCallback(),a>=e.getCount()&&c.postTourCallback(!0),c.postStepCallback()}},c.addStep=function(a){d.isNumber(a.index)&&!isNaN(a.index)?e.set(a.index,a):e.push(a)},c.unselectAllSteps=function(){e.forEach(function(a){a.ttOpen=!1})},c.cancelTour=function(){c.unselectAllSteps(),c.postTourCallback(!1)},a.openTour=function(){var a=c.currentStep>=e.getCount()||c.currentStep<0?0:c.currentStep;c.select(a)},a.closeTour=function(){c.cancelTour()}}]).directive("tour",["$parse","$timeout","tourConfig",function(a,c,e){return{controller:"TourController",restrict:"EA",scope:!0,link:function(f,g,h,i){if(!d.isDefined(h.step))throw"The directive requires a `step` attribute to bind the current step to.";var j=a(h.step),k=!1;f.$watch(h.step,function(a){i.currentStep=a}),i.postTourCallback=function(a){var c=b.getElementsByClassName("tour-backdrop"),e=b.getElementsByClassName("tour-element-active");d.element(c).remove(),k=!1,d.element(e).removeClass("tour-element-active"),a&&d.isDefined(h.tourComplete)&&f.$parent.$eval(h.tourComplete),d.isDefined(h.postTour)&&f.$parent.$eval(h.postTour)},i.postStepCallback=function(){d.isDefined(h.postStep)&&f.$parent.$eval(h.postStep)},i.showStepCallback=function(){e.backDrop&&(c(function(){var a=b.getElementsByClassName("tour-backdrop"),c=b.getElementsByClassName("tour-tip")[0],e=b.createElement("div");e.className="tour-backdrop",d.element(a).remove(),d.isDefined(c)&&c.parentNode.insertBefore(e,c)},501),k=!0)},f.setCurrentStep=function(a){j.assign(f.$parent,a),i.currentStep=a},f.getCurrentStep=function(){return i.currentStep}}}}]).directive("tourtip",["$window","$compile","$interpolate","$timeout","scrollTo","tourConfig","debounce","$q",function(e,f,g,h,i,j,k,l){var m=(g.startSymbol(),g.endSymbol(),"
");return{require:"^tour",restrict:"EA",scope:!0,link:function(g,n,o,p){function q(){var a=b.querySelectorAll(g.ttElement),c=g.ttElement?d.element(a):n,e=g;return c===n||g.ttSourceScope||(e=c.scope()),e}function r(b,c){var d,e,f=0,h=v[0].offsetWidth,i=v[0].offsetHeight,j=b[0].getBoundingClientRect(),k=j.top+a.pageYOffset,l=0;if(c&&c[0]){k=k-c[0].getBoundingClientRect().top+c[0].scrollTop,"fixed"===c.css("position")&&(l=c[0].getBoundingClientRect().left);var m=c[0].getBoundingClientRect().width;h+j.width>m&&(d=m-j.left+g.ttMargin)}var n,o=h,p=i;switch(g.ttPlacement){case"right":n=j.left-l+j.width+g.ttMargin+g.offsetHorizontal,e={top:k+g.offsetVertical,left:n>0?n:f};break;case"bottom":n=j.left-l+g.offsetHorizontal,e={top:k+j.height+g.ttMargin+g.offsetVertical,left:n>0?n:f};break;case"center":n=j.left-l+.5*(j.width-o)+g.offsetHorizontal,e={top:k+.5*(j.height-p)+g.ttMargin+g.offsetVertical,left:n>0?n:f};break;case"center-top":n=j.left-l+.5*(j.width-o)+g.offsetHorizontal,e={top:k+.1*(j.height-p)+g.ttMargin+g.offsetVertical,left:n>0?n:f};break;case"left":n=j.left-l-o-g.ttMargin+g.offsetHorizontal,e={top:k+g.offsetVertical,left:n>0?n:f,right:d};break;default:n=j.left-l+g.offsetHorizontal,e={top:k-p-g.ttMargin+g.offsetVertical,left:n>0?n:f}}return e.top+="px",e.left+="px",e}function s(){if(g.ttContent){var a=b.querySelectorAll(g.ttElement),f=g.ttElement?d.element(a):n;if(null===f||0===f.length)throw"Target element could not be found. Selector: "+g.ttElement;var l=b.querySelectorAll(g.ttContainerElement);d.element(l).append(v);var m=function(){var a="body"===g.ttContainerElement?c:d.element(l),b=r(f,a);v.css(b),i(v,g.ttContainerElement,-150,-300,j.scrollSpeed)};if(j.backDrop&&u(f),d.element(e).bind("resize."+g.$id,k(m,50)),m(),v.addClass("show"),g.onStepShow){var o=q();h(function(){o.$eval(g.onStepShow)},300)}}}function t(){v.removeClass("show"),v.detach(),d.element(e).unbind("resize."+g.$id)}function u(a){var c=b.getElementsByClassName("tour-element-active");d.element(c).removeClass("tour-element-active"),g.centered||a.addClass("tour-element-active")}o.$observe("tourtip",function(a){g.ttContent=a}),o.$observe("tourtipPlacement",function(a){g.ttPlacement=(a||j.placement).toLowerCase().trim(),g.centered=0===g.ttPlacement.indexOf("center")}),o.$observe("tourtipNextLabel",function(a){g.ttNextLabel=a||j.nextLabel}),o.$observe("tourtipContainerElement",function(a){g.ttContainerElement=a||j.containerElement}),o.$observe("tourtipMargin",function(a){g.ttMargin=parseInt(a,10)||j.margin}),o.$observe("tourtipOffsetVertical",function(a){g.offsetVertical=parseInt(a,10)||0}),o.$observe("tourtipOffsetHorizontal",function(a){g.offsetHorizontal=parseInt(a,10)||0}),o.$observe("onShow",function(a){g.onStepShow=a||null}),o.$observe("onProceed",function(a){g.onStepProceed=a||null}),o.$observe("tourtipElement",function(a){g.ttElement=a||null}),o.$observe("tourtipTitle",function(a){g.ttTitle=a||null}),o.$observe("useSourceScope",function(a){g.ttSourceScope=a?"true"===a:j.useSourceScope}),g.ttNextLabel=j.nextLabel,g.ttContainerElement=j.containerElement,g.ttPlacement=j.placement.toLowerCase().trim(),g.centered=!1,g.ttMargin=j.margin,g.offsetHorizontal=0,g.offsetVertical=0,g.ttSourceScope=j.useSourceScope,g.ttOpen=!1,g.ttAnimation=j.animation,g.index=parseInt(o.tourtipStep,10);var v=f(m)(g);p.addStep(g),h(function(){g.$watch("ttOpen",function(a){a?s():t()})},500),g.$on("$destroy",function(){d.element(e).unbind("resize."+g.$id),v.remove(),v=null}),g.proceed=function(){if(g.onStepProceed){var a=q(),b=a.$eval(g.onStepProceed);l.resolve(b).then(function(){g.setCurrentStep(g.getCurrentStep()+1)})}else g.setCurrentStep(g.getCurrentStep()+1)}}}}]).directive("tourPopup",function(){return{replace:!0,templateUrl:"tour/tour.tpl.html",scope:!0,restrict:"EA",link:function(a,b,c){}}}).factory("orderedList",function(){var a=function(){this.map={},this._array=[]};a.prototype.set=function(a,b){if(d.isNumber(a))if(a in this.map)this.map[a]=b;else{if(a0?a-1:0;this._array.splice(c,0,a)}else this._array.push(a);this.map[a]=b,this._array.sort(function(a,b){return a-b})}},a.prototype.indexOf=function(a){for(var b in this.map)if(this.map.hasOwnProperty(b)&&this.map[b]===a)return Number(b)},a.prototype.push=function(a){var b=this._array[this._array.length-1]+1||0;this._array.push(b),this.map[b]=a,this._array.sort(function(a,b){return a-b})},a.prototype.remove=function(a){var b=this._array.indexOf(a);if(-1===b)throw new Error("key does not exist");this._array.splice(b,1),delete this.map[a]},a.prototype.get=function(a){return this.map[a]},a.prototype.getCount=function(){return this._array.length},a.prototype.forEach=function(a){for(var b,c,d=0;da?4*a*a*a:(a-1)*(2*a-2)*(2*a-2)+1}function d(b,d,f,g,h,i){function j(){n===l&&o===m&&(a.cancel(v),e=!1)}function k(){console.log("called"),s+=16,p=s/i,p=p>1?1:p;var a=c(p),d=t*a,e=u*a;n=q+d,o=r+e,b.scrollTop=n,b.scrollLeft=o,j()}if(!e){i=i||500,g=g||0,h=h||0;var l=d+g;0>l?l=0:l>b.scrollHeight&&(l=b.scrollHeight);var m=f+h;0>m?m=0:m>b.scrollWidth&&(m=b.scrollWidth);var n,o,p,q=b.scrollTop,r=b.scrollLeft,s=0,t=l-q,u=m-r;e=!0;var v=a(k,16)}}var e=!1;return function(a,c,e,f,g){var h=b.querySelectorAll(c);e=e||-100,f=f||-100,d(h[0],a[0].offsetTop,a[0].offsetLeft,e,f,g)}}]).factory("debounce",["$timeout","$q",function(a,b){return function(c,d,e){var f,g=b.defer();return function(){var h=this,i=arguments,j=function(){f=null,e||(g.resolve(c.apply(h,i)),g=b.defer())},k=e&&!f;return f&&a.cancel(f),f=a(j,d),k&&(g.resolve(c.apply(h,i)),g=b.defer()),g.promise}}}])}(angular)}(window,document); \ No newline at end of file diff --git a/dist/angular-tour.css b/dist/angular-tour.css index 3725810..b723980 100644 --- a/dist/angular-tour.css +++ b/dist/angular-tour.css @@ -1,9 +1,12 @@ .tour-tip { - display: none; + opacity: 0; + visibility: hidden; + display: block; + transition: visibility 0s, opacity 0.6s cubic-bezier(0.345, 0, 0.25, 1); position: absolute; background: #1C252E; color: #FFF; - z-index: 101; + z-index: 121 !important; top: 0; left: 2.5%; font-family: inherit; @@ -12,6 +15,11 @@ border-radius: 10px; } +.tour-tip.show { + opacity: 1; + visibility: visible; +} + .tour-tip p { color: #CBD0D4; font-size: .9em; diff --git a/dist/angular-tour.js b/dist/angular-tour.js index 0ff1e06..a70ee03 100644 --- a/dist/angular-tour.js +++ b/dist/angular-tour.js @@ -1,6 +1,6 @@ /** * An AngularJS directive for showcasing features of your website - * @version v0.2.5 - 2015-12-10 + * @version v0.2.5 - 2016-03-15 * @link https://github.com/DaftMonk/angular-tour * @author Tyler Henkel * @license MIT License, http://www.opensource.org/licenses/MIT @@ -9,490 +9,561 @@ (function (window, document, undefined) { 'use strict'; angular.module('angular-tour', ['angular-tour.tour']); - angular.module('angular-tour.tour', []).constant('tourConfig', { - placement: 'top', - animation: true, - nextLabel: 'Next', - scrollSpeed: 500, - margin: 28, - backDrop: false, - useSourceScope: false, - containerElement: 'body' - }).controller('TourController', [ - '$scope', - 'orderedList', - function ($scope, orderedList) { - var self = this, steps = self.steps = orderedList(), firstCurrentStepChange = true; - // we'll pass these in from the directive - self.postTourCallback = angular.noop; - self.postStepCallback = angular.noop; - self.showStepCallback = angular.noop; - self.currentStep = -1; - // if currentStep changes, select the new step - $scope.$watch(function () { - return self.currentStep; - }, function (val) { - if (firstCurrentStepChange) { - firstCurrentStepChange = false; - } else { - self.select(val); - } - }); - self.select = function (nextIndex) { - if (!angular.isNumber(nextIndex)) - return; - self.unselectAllSteps(); - var step = steps.get(nextIndex); - if (step) { - step.ttOpen = true; - } - // update currentStep if we manually selected this index - if (self.currentStep !== nextIndex) { - self.currentStep = nextIndex; - } - if (self.currentStep > -1) - self.showStepCallback(); - if (nextIndex >= steps.getCount()) { - self.postTourCallback(true); - } - self.postStepCallback(); - }; - self.addStep = function (step) { - if (angular.isNumber(step.index) && !isNaN(step.index)) { - steps.set(step.index, step); - } else { - steps.push(step); - } - }; - self.unselectAllSteps = function () { - steps.forEach(function (step) { - step.ttOpen = false; + (function (angular) { + angular.module('angular-tour.tour', []).constant('tourConfig', { + placement: 'top', + animation: true, + nextLabel: 'Next', + scrollSpeed: 500, + margin: 28, + backDrop: false, + useSourceScope: false, + containerElement: 'body' + }).controller('TourController', [ + '$scope', + 'orderedList', + function ($scope, orderedList) { + var self = this, steps = self.steps = orderedList(), firstCurrentStepChange = true; + // we'll pass these in from the directive + self.postTourCallback = angular.noop; + self.postStepCallback = angular.noop; + self.showStepCallback = angular.noop; + self.currentStep = -1; + // if currentStep changes, select the new step + $scope.$watch(function () { + return self.currentStep; + }, function (val) { + if (firstCurrentStepChange) + firstCurrentStepChange = false; + else + self.select(val); }); - }; - self.cancelTour = function () { - self.unselectAllSteps(); - self.postTourCallback(false); - }; - $scope.openTour = function () { - // open at first step if we've already finished tour - var startStep = self.currentStep >= steps.getCount() || self.currentStep < 0 ? 0 : self.currentStep; - self.select(startStep); - }; - $scope.closeTour = function () { - self.cancelTour(); - }; - } - ]).directive('tour', [ - '$parse', - '$timeout', - 'tourConfig', - function ($parse, $timeout, tourConfig) { - return { - controller: 'TourController', - restrict: 'EA', - scope: true, - link: function (scope, element, attrs, ctrl) { - if (!angular.isDefined(attrs.step)) { - throw 'The directive requires a `step` attribute to bind the current step to.'; + self.select = function (nextIndex) { + if (!angular.isNumber(nextIndex)) + return; + self.unselectAllSteps(); + var step = steps.get(nextIndex); + if (step) { + step.ttOpen = true; + } + // update currentStep if we manually selected this index + if (self.currentStep !== nextIndex) { + self.currentStep = nextIndex; } - var model = $parse(attrs.step); - var backDrop = false; - // Watch current step view model and update locally - scope.$watch(attrs.step, function (newVal) { - ctrl.currentStep = newVal; + if (self.currentStep > -1) { + self.showStepCallback(); + } + if (nextIndex >= steps.getCount()) { + self.postTourCallback(true); + } + self.postStepCallback(); + }; + self.addStep = function (step) { + if (angular.isNumber(step.index) && !isNaN(step.index)) + steps.set(step.index, step); + else + steps.push(step); + }; + self.unselectAllSteps = function () { + steps.forEach(function (step) { + step.ttOpen = false; }); - ctrl.postTourCallback = function (completed) { - angular.element('.tour-backdrop').remove(); - backDrop = false; - angular.element('.tour-element-active').removeClass('tour-element-active'); - if (completed && angular.isDefined(attrs.tourComplete)) { - scope.$parent.$eval(attrs.tourComplete); - } - if (angular.isDefined(attrs.postTour)) { - scope.$parent.$eval(attrs.postTour); - } - }; - ctrl.postStepCallback = function () { - if (angular.isDefined(attrs.postStep)) { - scope.$parent.$eval(attrs.postStep); - } - }; - ctrl.showStepCallback = function () { - if (tourConfig.backDrop) { - angular.element(tourConfig.containerElement).append(angular.element('
')); - $timeout(function () { - $('.tour-backdrop').remove(); - angular.element('
').insertBefore('.tour-tip'); - }, 1000); - backDrop = true; + }; + self.cancelTour = function () { + self.unselectAllSteps(); + self.postTourCallback(false); + }; + $scope.openTour = function () { + // open at first step if we've already finished tour + var startStep = self.currentStep >= steps.getCount() || self.currentStep < 0 ? 0 : self.currentStep; + self.select(startStep); + }; + $scope.closeTour = function () { + self.cancelTour(); + }; + } + ]).directive('tour', [ + '$parse', + '$timeout', + 'tourConfig', + function ($parse, $timeout, tourConfig) { + return { + controller: 'TourController', + restrict: 'EA', + scope: true, + link: function (scope, element, attrs, ctrl) { + if (!angular.isDefined(attrs.step)) { + throw 'The directive requires a `step` attribute to bind the current step to.'; } - }; - // update the current step in the view as well as in our controller - scope.setCurrentStep = function (val) { - model.assign(scope.$parent, val); - ctrl.currentStep = val; - }; - scope.getCurrentStep = function () { - return ctrl.currentStep; - }; - } - }; - } - ]).directive('tourtip', [ - '$window', - '$compile', - '$interpolate', - '$timeout', - 'scrollTo', - 'tourConfig', - 'debounce', - '$q', - function ($window, $compile, $interpolate, $timeout, scrollTo, tourConfig, debounce, $q) { - var startSym = $interpolate.startSymbol(), endSym = $interpolate.endSymbol(); - var template = '
'; - return { - require: '^tour', - restrict: 'EA', - scope: true, - link: function (scope, element, attrs, tourCtrl) { - attrs.$observe('tourtip', function (val) { - scope.ttContent = val; - }); - //defaults: tourConfig.placement - attrs.$observe('tourtipPlacement', function (val) { - scope.ttPlacement = (val || tourConfig.placement).toLowerCase().trim(); - scope.centered = scope.ttPlacement.indexOf('center') === 0; - }); - attrs.$observe('tourtipNextLabel', function (val) { - scope.ttNextLabel = val || tourConfig.nextLabel; - }); - attrs.$observe('tourtipContainerElement', function (val) { - scope.ttContainerElement = val || tourConfig.containerElement; - }); - attrs.$observe('tourtipMargin', function (val) { - scope.ttMargin = parseInt(val, 10) || tourConfig.margin; - }); - attrs.$observe('tourtipOffsetVertical', function (val) { - scope.offsetVertical = parseInt(val, 10) || 0; - }); - attrs.$observe('tourtipOffsetHorizontal', function (val) { - scope.offsetHorizontal = parseInt(val, 10) || 0; - }); - //defaults: null - attrs.$observe('onShow', function (val) { - scope.onStepShow = val || null; - }); - //defaults: null - attrs.$observe('onProceed', function (val) { - scope.onStepProceed = val || null; - }); - //defaults: null - attrs.$observe('tourtipElement', function (val) { - scope.ttElement = val || null; - }); - //defaults: null - attrs.$observe('tourtipTitle', function (val) { - scope.ttTitle = val || null; - }); - //defaults: tourConfig.useSourceScope - attrs.$observe('useSourceScope', function (val) { - scope.ttSourceScope = !val ? tourConfig.useSourceScope : val === 'true'; - }); - //Init assignments (fix for Angular 1.3+) - scope.ttNextLabel = tourConfig.nextLabel; - scope.ttContainerElement = tourConfig.containerElement; - scope.ttPlacement = tourConfig.placement.toLowerCase().trim(); - scope.centered = false; - scope.ttMargin = tourConfig.margin; - scope.offsetHorizontal = 0; - scope.offsetVertical = 0; - scope.ttSourceScope = tourConfig.useSourceScope; - scope.ttOpen = false; - scope.ttAnimation = tourConfig.animation; - scope.index = parseInt(attrs.tourtipStep, 10); - var tourtip = $compile(template)(scope); - tourCtrl.addStep(scope); - // wrap this in a time out because the tourtip won't compile right away - $timeout(function () { - scope.$watch('ttOpen', function (val) { - if (val) { - show(); - } else { - hide(); - } + var model = $parse(attrs.step); + var backDrop = false; + // Watch current step view model and update locally + scope.$watch(attrs.step, function (newVal) { + ctrl.currentStep = newVal; }); - }, 500); - //determining target scope. It's used only when using virtual steps and there - //is some action performed like on-show or on-progress. Without virtual steps - //action would performed on element's scope and that would work just fine - //however, when using virtual steps, whose steps can be placed in different - //controller, so it affects scope, which will be used to run this action against. - function getTargetScope() { - var targetElement = scope.ttElement ? angular.element(scope.ttElement) : element; - var targetScope = scope; - if (targetElement !== element && !scope.ttSourceScope) - targetScope = targetElement.scope(); - return targetScope; + ctrl.postTourCallback = function (completed) { + var backdropEle = document.getElementsByClassName('tour-backdrop'); + var active = document.getElementsByClassName('tour-element-active'); + angular.element(backdropEle).remove(); + backDrop = false; + angular.element(active).removeClass('tour-element-active'); + if (completed && angular.isDefined(attrs.tourComplete)) { + scope.$parent.$eval(attrs.tourComplete); + } + if (angular.isDefined(attrs.postTour)) { + scope.$parent.$eval(attrs.postTour); + } + }; + ctrl.postStepCallback = function () { + if (angular.isDefined(attrs.postStep)) { + scope.$parent.$eval(attrs.postStep); + } + }; + ctrl.showStepCallback = function () { + if (tourConfig.backDrop) { + $timeout(function () { + var backdrop = document.getElementsByClassName('tour-backdrop'); + var tooltip = document.getElementsByClassName('tour-tip')[0]; + var div = document.createElement('div'); + div.className = 'tour-backdrop'; + angular.element(backdrop).remove(); + // When the tour ends simply remove the backdrop and return. + if (!angular.isDefined(tooltip)) { + return; + } + tooltip.parentNode.insertBefore(div, tooltip); + }, 501); + backDrop = true; + } + }; + // update the current step in the view as well as in our controller + scope.setCurrentStep = function (val) { + model.assign(scope.$parent, val); + ctrl.currentStep = val; + }; + scope.getCurrentStep = function () { + return ctrl.currentStep; + }; } - function calculatePosition(element, container) { - var minimumLeft = 0; - // minimum left position of tour tip - var restrictRight; - var ttPosition; - // Get the position of the directive element - var position = element[0].getBoundingClientRect(); - //make it relative against page or fixed container, not the window - var top = position.top + window.pageYOffset; - var containerLeft = 0; - if (container && container[0]) { - top = top - container[0].getBoundingClientRect().top + container[0].scrollTop; - // if container is fixed, position tour tip relative to fixed container - if (container.css('position') === 'fixed') { - containerLeft = container[0].getBoundingClientRect().left; + }; + } + ]).directive('tourtip', [ + '$window', + '$compile', + '$interpolate', + '$timeout', + 'scrollTo', + 'tourConfig', + 'debounce', + '$q', + function ($window, $compile, $interpolate, $timeout, scrollTo, tourConfig, debounce, $q) { + var startSym = $interpolate.startSymbol(), endSym = $interpolate.endSymbol(); + var template = '
'; + return { + require: '^tour', + restrict: 'EA', + scope: true, + link: function (scope, element, attrs, tourCtrl) { + attrs.$observe('tourtip', function (val) { + scope.ttContent = val; + }); + //defaults: tourConfig.placement + attrs.$observe('tourtipPlacement', function (val) { + scope.ttPlacement = (val || tourConfig.placement).toLowerCase().trim(); + scope.centered = scope.ttPlacement.indexOf('center') === 0; + }); + attrs.$observe('tourtipNextLabel', function (val) { + scope.ttNextLabel = val || tourConfig.nextLabel; + }); + attrs.$observe('tourtipContainerElement', function (val) { + scope.ttContainerElement = val || tourConfig.containerElement; + }); + attrs.$observe('tourtipMargin', function (val) { + scope.ttMargin = parseInt(val, 10) || tourConfig.margin; + }); + attrs.$observe('tourtipOffsetVertical', function (val) { + scope.offsetVertical = parseInt(val, 10) || 0; + }); + attrs.$observe('tourtipOffsetHorizontal', function (val) { + scope.offsetHorizontal = parseInt(val, 10) || 0; + }); + //defaults: null + attrs.$observe('onShow', function (val) { + scope.onStepShow = val || null; + }); + //defaults: null + attrs.$observe('onProceed', function (val) { + scope.onStepProceed = val || null; + }); + //defaults: null + attrs.$observe('tourtipElement', function (val) { + scope.ttElement = val || null; + }); + //defaults: null + attrs.$observe('tourtipTitle', function (val) { + scope.ttTitle = val || null; + }); + //defaults: tourConfig.useSourceScope + attrs.$observe('useSourceScope', function (val) { + scope.ttSourceScope = !val ? tourConfig.useSourceScope : val === 'true'; + }); + //Init assignments (fix for Angular 1.3+) + scope.ttNextLabel = tourConfig.nextLabel; + scope.ttContainerElement = tourConfig.containerElement; + scope.ttPlacement = tourConfig.placement.toLowerCase().trim(); + scope.centered = false; + scope.ttMargin = tourConfig.margin; + scope.offsetHorizontal = 0; + scope.offsetVertical = 0; + scope.ttSourceScope = tourConfig.useSourceScope; + scope.ttOpen = false; + scope.ttAnimation = tourConfig.animation; + scope.index = parseInt(attrs.tourtipStep, 10); + var tourtip = $compile(template)(scope); + tourCtrl.addStep(scope); + // wrap this in a time out because the tourtip won't compile right away + $timeout(function () { + scope.$watch('ttOpen', function (val) { + if (val) + show(); + else + hide(); + }); + }, 500); + //determining target scope. It's used only when using virtual steps and there + //is some action performed like on-show or on-progress. Without virtual steps + //action would performed on element's scope and that would work just fine + //however, when using virtual steps, whose steps can be placed in different + //controller, so it affects scope, which will be used to run this action against. + function getTargetScope() { + var target = document.querySelectorAll(scope.ttElement); + var targetElement = scope.ttElement ? angular.element(target) : element; + var targetScope = scope; + if (targetElement !== element && !scope.ttSourceScope) { + targetScope = targetElement.scope(); + } + return targetScope; + } + function calculatePosition(element, container) { + var minimumLeft = 0; + // minimum left position of tour tip + var restrictRight; + var ttPosition; + var tourtipWidth = tourtip[0].offsetWidth; + var tourtipHeight = tourtip[0].offsetHeight; + // Get the position of the directive element + var position = element[0].getBoundingClientRect(); + //make it relative against page or fixed container, not the window + var top = position.top + window.pageYOffset; + var containerLeft = 0; + if (container && container[0]) { + top = top - container[0].getBoundingClientRect().top + container[0].scrollTop; + // if container is fixed, position tour tip relative to fixed container + if (container.css('position') === 'fixed') { + containerLeft = container[0].getBoundingClientRect().left; + } + // restrict right position if the tourtip doesn't fit in the container + var containerWidth = container[0].getBoundingClientRect().width; + if (tourtipWidth + position.width > containerWidth) { + restrictRight = containerWidth - position.left + scope.ttMargin; + } } - // restrict right position if the tourtip doesn't fit in the container - var containerWidth = container[0].getBoundingClientRect().width; - if (tourtip.width() + position.width > containerWidth) { - restrictRight = containerWidth - position.left + scope.ttMargin; + var ttWidth = tourtipWidth; + var ttHeight = tourtipHeight; + // Calculate the tourtip's top and left coordinates to center it + var _left; + switch (scope.ttPlacement) { + case 'right': + _left = position.left - containerLeft + position.width + scope.ttMargin + scope.offsetHorizontal; + ttPosition = { + top: top + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'bottom': + _left = position.left - containerLeft + scope.offsetHorizontal; + ttPosition = { + top: top + position.height + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'center': + _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; + ttPosition = { + top: top + 0.5 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'center-top': + _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; + ttPosition = { + top: top + 0.1 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'left': + _left = position.left - containerLeft - ttWidth - scope.ttMargin + scope.offsetHorizontal; + ttPosition = { + top: top + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft, + right: restrictRight + }; + break; + default: + _left = position.left - containerLeft + scope.offsetHorizontal; + ttPosition = { + top: top - ttHeight - scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; } + ttPosition.top += 'px'; + ttPosition.left += 'px'; + return ttPosition; } - var ttWidth = tourtip.width(); - var ttHeight = tourtip.height(); - // Calculate the tourtip's top and left coordinates to center it - switch (scope.ttPlacement) { - case 'right': - var _left = position.left - containerLeft + position.width + scope.ttMargin + scope.offsetHorizontal; - ttPosition = { - top: top + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'bottom': - var _left = position.left - containerLeft + scope.offsetHorizontal; - ttPosition = { - top: top + position.height + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'center': - var _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; - ttPosition = { - top: top + 0.5 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'center-top': - var _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; - ttPosition = { - top: top + 0.1 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'left': - var _left = position.left - containerLeft - ttWidth - scope.ttMargin + scope.offsetHorizontal; - ttPosition = { - top: top + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft, - right: restrictRight - }; - break; - default: - var _left = position.left - containerLeft + scope.offsetHorizontal; - ttPosition = { - top: top - ttHeight - scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft + function show() { + if (!scope.ttContent) { + return; + } + var target = document.querySelectorAll(scope.ttElement); + var targetElement = scope.ttElement ? angular.element(target) : element; + if (targetElement === null || targetElement.length === 0) + throw 'Target element could not be found. Selector: ' + scope.ttElement; + var containerEle = document.querySelectorAll(scope.ttContainerElement); + angular.element(containerEle).append(tourtip); + var updatePosition = function () { + var offsetElement = scope.ttContainerElement === 'body' ? undefined : angular.element(containerEle); + var ttPosition = calculatePosition(targetElement, offsetElement); + // Now set the calculated positioning. + tourtip.css(ttPosition); + // Scroll to the tour tip + scrollTo(tourtip, scope.ttContainerElement, -150, -300, tourConfig.scrollSpeed); }; - break; + if (tourConfig.backDrop) { + focusActiveElement(targetElement); + } + angular.element($window).bind('resize.' + scope.$id, debounce(updatePosition, 50)); + updatePosition(); + // CSS class must be added after the element is already on the DOM otherwise it won't animate (fade in). + tourtip.addClass('show'); + if (scope.onStepShow) { + var targetScope = getTargetScope(); + //fancy! Let's make on show action not instantly, but after a small delay + $timeout(function () { + targetScope.$eval(scope.onStepShow); + }, 300); + } } - ttPosition.top += 'px'; - ttPosition.left += 'px'; - return ttPosition; - } - function show() { - if (!scope.ttContent) { - return; + function hide() { + tourtip.removeClass('show'); + tourtip.detach(); + angular.element($window).unbind('resize.' + scope.$id); } - if (scope.ttAnimation) - tourtip.fadeIn(); - else { - tourtip.css({ display: 'block' }); + function focusActiveElement(el) { + var activeEle = document.getElementsByClassName('tour-element-active'); + angular.element(activeEle).removeClass('tour-element-active'); + if (!scope.centered) { + el.addClass('tour-element-active'); + } } - var targetElement = scope.ttElement ? angular.element(scope.ttElement) : element; - if (targetElement == null || targetElement.length === 0) - throw 'Target element could not be found. Selector: ' + scope.ttElement; - angular.element(scope.ttContainerElement).append(tourtip); - var updatePosition = function () { - var offsetElement = scope.ttContainerElement === 'body' ? undefined : angular.element(scope.ttContainerElement); - var ttPosition = calculatePosition(targetElement, offsetElement); - // Now set the calculated positioning. - tourtip.css(ttPosition); - // Scroll to the tour tip - var ttPositionTop = parseInt(ttPosition.top), ttPositionLeft = parseInt(ttPosition.left); - scrollTo(tourtip, scope.ttContainerElement, -150, -300, tourConfig.scrollSpeed, ttPositionTop, ttPositionLeft); + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTourtip() { + angular.element($window).unbind('resize.' + scope.$id); + tourtip.remove(); + tourtip = null; + }); + scope.proceed = function () { + if (scope.onStepProceed) { + var targetScope = getTargetScope(); + var onProceedResult = targetScope.$eval(scope.onStepProceed); + $q.resolve(onProceedResult).then(function () { + scope.setCurrentStep(scope.getCurrentStep() + 1); + }); + } else { + scope.setCurrentStep(scope.getCurrentStep() + 1); + } }; - if (tourConfig.backDrop) - focusActiveElement(targetElement); - angular.element($window).bind('resize.' + scope.$id, debounce(updatePosition, 50)); - updatePosition(); - if (scope.onStepShow) { - var targetScope = getTargetScope(); - //fancy! Let's make on show action not instantly, but after a small delay - $timeout(function () { - targetScope.$eval(scope.onStepShow); - }, 300); - } } - function hide() { - tourtip.detach(); - angular.element($window).unbind('resize.' + scope.$id); - } - function focusActiveElement(el) { - angular.element('.tour-element-active').removeClass('tour-element-active'); - if (!scope.centered) - el.addClass('tour-element-active'); + }; + } + ]).directive('tourPopup', function () { + return { + replace: true, + templateUrl: 'tour/tour.tpl.html', + scope: true, + restrict: 'EA', + link: function (scope, element, attrs) { + } + }; + }).factory('orderedList', function () { + var OrderedList = function () { + this.map = {}; + this._array = []; + }; + OrderedList.prototype.set = function (key, value) { + if (!angular.isNumber(key)) + return; + if (key in this.map) { + this.map[key] = value; + } else { + if (key < this._array.length) { + var insertIndex = key - 1 > 0 ? key - 1 : 0; + this._array.splice(insertIndex, 0, key); + } else { + this._array.push(key); } - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTourtip() { - angular.element($window).unbind('resize.' + scope.$id); - tourtip.remove(); - tourtip = null; + this.map[key] = value; + this._array.sort(function (a, b) { + return a - b; }); - scope.proceed = function () { - if (scope.onStepProceed) { - var targetScope = getTargetScope(); - var onProceedResult = targetScope.$eval(scope.onStepProceed); - $q.resolve(onProceedResult).then(function () { - scope.setCurrentStep(scope.getCurrentStep() + 1); - }); - } else { - scope.setCurrentStep(scope.getCurrentStep() + 1); - } - }; } }; - } - ]).directive('tourPopup', function () { - return { - replace: true, - templateUrl: 'tour/tour.tpl.html', - scope: true, - restrict: 'EA', - link: function (scope, element, attrs) { - } - }; - }).factory('orderedList', function () { - var OrderedList = function () { - this.map = {}; - this._array = []; - }; - OrderedList.prototype.set = function (key, value) { - if (!angular.isNumber(key)) - return; - if (key in this.map) { - this.map[key] = value; - } else { - if (key < this._array.length) { - var insertIndex = key - 1 > 0 ? key - 1 : 0; - this._array.splice(insertIndex, 0, key); - } else { - this._array.push(key); + OrderedList.prototype.indexOf = function (value) { + for (var prop in this.map) { + if (this.map.hasOwnProperty(prop)) { + if (this.map[prop] === value) + return Number(prop); + } } + }; + OrderedList.prototype.push = function (value) { + var key = this._array[this._array.length - 1] + 1 || 0; + this._array.push(key); this.map[key] = value; this._array.sort(function (a, b) { return a - b; }); - } - }; - OrderedList.prototype.indexOf = function (value) { - for (var prop in this.map) { - if (this.map.hasOwnProperty(prop)) { - if (this.map[prop] === value) - return Number(prop); + }; + OrderedList.prototype.remove = function (key) { + var index = this._array.indexOf(key); + if (index === -1) { + throw new Error('key does not exist'); } - } - }; - OrderedList.prototype.push = function (value) { - var key = this._array[this._array.length - 1] + 1 || 0; - this._array.push(key); - this.map[key] = value; - this._array.sort(function (a, b) { - return a - b; - }); - }; - OrderedList.prototype.remove = function (key) { - var index = this._array.indexOf(key); - if (index === -1) { - throw new Error('key does not exist'); - } - this._array.splice(index, 1); - delete this.map[key]; - }; - OrderedList.prototype.get = function (key) { - return this.map[key]; - }; - OrderedList.prototype.getCount = function () { - return this._array.length; - }; - OrderedList.prototype.forEach = function (f) { - var key, value; - for (var i = 0; i < this._array.length; i++) { - key = this._array[i]; + this._array.splice(index, 1); + delete this.map[key]; + }; + OrderedList.prototype.get = function (key) { + return this.map[key]; + }; + OrderedList.prototype.getCount = function () { + return this._array.length; + }; + OrderedList.prototype.forEach = function (f) { + var key, value; + for (var i = 0; i < this._array.length; i++) { + key = this._array[i]; + value = this.map[key]; + f(value, key); + } + }; + OrderedList.prototype.first = function () { + var key, value; + key = this._array[0]; value = this.map[key]; - f(value, key); - } - }; - OrderedList.prototype.first = function () { - var key, value; - key = this._array[0]; - value = this.map[key]; - return value; - }; - var orderedListFactory = function () { - return new OrderedList(); - }; - return orderedListFactory; - }).factory('scrollTo', function () { - return function (target, containerElement, offsetY, offsetX, speed, ttPositionTop, ttPositionLeft) { - if (target) { - offsetY = offsetY || -100; - offsetX = offsetX || -100; - speed = speed || 500; - $('html,' + containerElement).stop().animate({ - scrollTop: ttPositionTop + offsetY, - scrollLeft: ttPositionLeft + offsetX - }, speed); - } else { - $('html,' + containerElement).stop().animate({ scrollTop: 0 }, speed); + return value; + }; + var orderedListFactory = function () { + return new OrderedList(); + }; + return orderedListFactory; + }).factory('scrollTo', [ + '$interval', + function ($interval) { + var animationInProgress = false; + function getEasingPattern(time) { + return time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // default easeInOutCubic transition + } + function _autoScroll(container, endTop, endLeft, offsetY, offsetX, speed) { + if (animationInProgress) { + return; + } + speed = speed || 500; + offsetY = offsetY || 0; + offsetX = offsetX || 0; + // Set some boundaries in case the offset wants us to scroll to impossible locations + var finalY = endTop + offsetY; + if (finalY < 0) { + finalY = 0; + } else if (finalY > container.scrollHeight) { + finalY = container.scrollHeight; + } + var finalX = endLeft + offsetX; + if (finalX < 0) { + finalX = 0; + } else if (finalX > container.scrollWidth) { + finalX = container.scrollWidth; + } + var startTop = container.scrollTop, startLeft = container.scrollLeft, timeLapsed = 0, distanceY = finalY - startTop, + // If we're going up, this will be a negative number + distanceX = finalX - startLeft, currentPositionY, currentPositionX, timeProgress; + function stopAnimation() { + // If we have reached our destination clear the interval + if (currentPositionY === finalY && currentPositionX === finalX) { + $interval.cancel(runAnimation); + animationInProgress = false; + } + } + function animateScroll() { + console.log('called'); + timeLapsed += 16; + // get percentage of progress to the specified speed (e.g. 16/500). Should always be between 0 and 1 + timeProgress = timeLapsed / speed; + // Make a check and set back to 1 if we went over (e.g. 512/500) + timeProgress = timeProgress > 1 ? 1 : timeProgress; + // Number between 0 and 1 corresponding to the animation pattern + var multiplier = getEasingPattern(timeProgress); + // Calculate the distance to travel in this step. It is the total distance times a percentage of what we will move + var translateY = distanceY * multiplier; + var translateX = distanceX * multiplier; + // Assign to the shorthand variables + currentPositionY = startTop + translateY; + currentPositionX = startLeft + translateX; + // Move slightly following the easing pattern + container.scrollTop = currentPositionY; + container.scrollLeft = currentPositionX; + // Check if we have reached our destination + stopAnimation(); + } + animationInProgress = true; + // Kicks off the function + var runAnimation = $interval(animateScroll, 16); + } + return function (target, containerSelector, offsetY, offsetX, speed) { + var container = document.querySelectorAll(containerSelector); + offsetY = offsetY || -100; + offsetX = offsetX || -100; + _autoScroll(container[0], target[0].offsetTop, target[0].offsetLeft, offsetY, offsetX, speed); + }; } - }; - }).factory('debounce', [ - '$timeout', - '$q', - function ($timeout, $q) { - return function (func, wait, immediate) { - var timeout; - var deferred = $q.defer(); - return function () { - var context = this, args = arguments; - var later = function () { - timeout = null; - if (!immediate) { + ]).factory('debounce', [ + '$timeout', + '$q', + function ($timeout, $q) { + return function (func, wait, immediate) { + var timeout; + var deferred = $q.defer(); + return function () { + var context = this, args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + deferred.resolve(func.apply(context, args)); + deferred = $q.defer(); + } + }; + var callNow = immediate && !timeout; + if (timeout) { + $timeout.cancel(timeout); + } + timeout = $timeout(later, wait); + if (callNow) { deferred.resolve(func.apply(context, args)); deferred = $q.defer(); } + return deferred.promise; }; - var callNow = immediate && !timeout; - if (timeout) { - $timeout.cancel(timeout); - } - timeout = $timeout(later, wait); - if (callNow) { - deferred.resolve(func.apply(context, args)); - deferred = $q.defer(); - } - return deferred.promise; }; - }; - } - ]); + } + ]); + }(angular)); }(window, document)); \ No newline at end of file diff --git a/dist/angular-tour.min.js b/dist/angular-tour.min.js index 2bd71d5..3b6fcf4 100644 --- a/dist/angular-tour.min.js +++ b/dist/angular-tour.min.js @@ -1 +1 @@ -!function(a,b,c){"use strict";angular.module("angular-tour",["angular-tour.tour"]),angular.module("angular-tour.tour",[]).constant("tourConfig",{placement:"top",animation:!0,nextLabel:"Next",scrollSpeed:500,margin:28,backDrop:!1,useSourceScope:!1,containerElement:"body"}).controller("TourController",["$scope","orderedList",function(a,b){var c=this,d=c.steps=b(),e=!0;c.postTourCallback=angular.noop,c.postStepCallback=angular.noop,c.showStepCallback=angular.noop,c.currentStep=-1,a.$watch(function(){return c.currentStep},function(a){e?e=!1:c.select(a)}),c.select=function(a){if(angular.isNumber(a)){c.unselectAllSteps();var b=d.get(a);b&&(b.ttOpen=!0),c.currentStep!==a&&(c.currentStep=a),c.currentStep>-1&&c.showStepCallback(),a>=d.getCount()&&c.postTourCallback(!0),c.postStepCallback()}},c.addStep=function(a){angular.isNumber(a.index)&&!isNaN(a.index)?d.set(a.index,a):d.push(a)},c.unselectAllSteps=function(){d.forEach(function(a){a.ttOpen=!1})},c.cancelTour=function(){c.unselectAllSteps(),c.postTourCallback(!1)},a.openTour=function(){var a=c.currentStep>=d.getCount()||c.currentStep<0?0:c.currentStep;c.select(a)},a.closeTour=function(){c.cancelTour()}}]).directive("tour",["$parse","$timeout","tourConfig",function(a,b,c){return{controller:"TourController",restrict:"EA",scope:!0,link:function(d,e,f,g){if(!angular.isDefined(f.step))throw"The directive requires a `step` attribute to bind the current step to.";var h=a(f.step),i=!1;d.$watch(f.step,function(a){g.currentStep=a}),g.postTourCallback=function(a){angular.element(".tour-backdrop").remove(),i=!1,angular.element(".tour-element-active").removeClass("tour-element-active"),a&&angular.isDefined(f.tourComplete)&&d.$parent.$eval(f.tourComplete),angular.isDefined(f.postTour)&&d.$parent.$eval(f.postTour)},g.postStepCallback=function(){angular.isDefined(f.postStep)&&d.$parent.$eval(f.postStep)},g.showStepCallback=function(){c.backDrop&&(angular.element(c.containerElement).append(angular.element('
')),b(function(){$(".tour-backdrop").remove(),angular.element('
').insertBefore(".tour-tip")},1e3),i=!0)},d.setCurrentStep=function(a){h.assign(d.$parent,a),g.currentStep=a},d.getCurrentStep=function(){return g.currentStep}}}}]).directive("tourtip",["$window","$compile","$interpolate","$timeout","scrollTo","tourConfig","debounce","$q",function(b,d,e,f,g,h,i,j){var k=(e.startSymbol(),e.endSymbol(),"
");return{require:"^tour",restrict:"EA",scope:!0,link:function(e,l,m,n){function o(){var a=e.ttElement?angular.element(e.ttElement):l,b=e;return a===l||e.ttSourceScope||(b=a.scope()),b}function p(b,c){var d,f,g=0,h=b[0].getBoundingClientRect(),i=h.top+a.pageYOffset,j=0;if(c&&c[0]){i=i-c[0].getBoundingClientRect().top+c[0].scrollTop,"fixed"===c.css("position")&&(j=c[0].getBoundingClientRect().left);var k=c[0].getBoundingClientRect().width;t.width()+h.width>k&&(d=k-h.left+e.ttMargin)}var l=t.width(),m=t.height();switch(e.ttPlacement){case"right":var n=h.left-j+h.width+e.ttMargin+e.offsetHorizontal;f={top:i+e.offsetVertical,left:n>0?n:g};break;case"bottom":var n=h.left-j+e.offsetHorizontal;f={top:i+h.height+e.ttMargin+e.offsetVertical,left:n>0?n:g};break;case"center":var n=h.left-j+.5*(h.width-l)+e.offsetHorizontal;f={top:i+.5*(h.height-m)+e.ttMargin+e.offsetVertical,left:n>0?n:g};break;case"center-top":var n=h.left-j+.5*(h.width-l)+e.offsetHorizontal;f={top:i+.1*(h.height-m)+e.ttMargin+e.offsetVertical,left:n>0?n:g};break;case"left":var n=h.left-j-l-e.ttMargin+e.offsetHorizontal;f={top:i+e.offsetVertical,left:n>0?n:g,right:d};break;default:var n=h.left-j+e.offsetHorizontal;f={top:i-m-e.ttMargin+e.offsetVertical,left:n>0?n:g}}return f.top+="px",f.left+="px",f}function q(){if(e.ttContent){e.ttAnimation?t.fadeIn():t.css({display:"block"});var a=e.ttElement?angular.element(e.ttElement):l;if(null==a||0===a.length)throw"Target element could not be found. Selector: "+e.ttElement;angular.element(e.ttContainerElement).append(t);var d=function(){var b="body"===e.ttContainerElement?c:angular.element(e.ttContainerElement),d=p(a,b);t.css(d);var f=parseInt(d.top),i=parseInt(d.left);g(t,e.ttContainerElement,-150,-300,h.scrollSpeed,f,i)};if(h.backDrop&&s(a),angular.element(b).bind("resize."+e.$id,i(d,50)),d(),e.onStepShow){var j=o();f(function(){j.$eval(e.onStepShow)},300)}}}function r(){t.detach(),angular.element(b).unbind("resize."+e.$id)}function s(a){angular.element(".tour-element-active").removeClass("tour-element-active"),e.centered||a.addClass("tour-element-active")}m.$observe("tourtip",function(a){e.ttContent=a}),m.$observe("tourtipPlacement",function(a){e.ttPlacement=(a||h.placement).toLowerCase().trim(),e.centered=0===e.ttPlacement.indexOf("center")}),m.$observe("tourtipNextLabel",function(a){e.ttNextLabel=a||h.nextLabel}),m.$observe("tourtipContainerElement",function(a){e.ttContainerElement=a||h.containerElement}),m.$observe("tourtipMargin",function(a){e.ttMargin=parseInt(a,10)||h.margin}),m.$observe("tourtipOffsetVertical",function(a){e.offsetVertical=parseInt(a,10)||0}),m.$observe("tourtipOffsetHorizontal",function(a){e.offsetHorizontal=parseInt(a,10)||0}),m.$observe("onShow",function(a){e.onStepShow=a||null}),m.$observe("onProceed",function(a){e.onStepProceed=a||null}),m.$observe("tourtipElement",function(a){e.ttElement=a||null}),m.$observe("tourtipTitle",function(a){e.ttTitle=a||null}),m.$observe("useSourceScope",function(a){e.ttSourceScope=a?"true"===a:h.useSourceScope}),e.ttNextLabel=h.nextLabel,e.ttContainerElement=h.containerElement,e.ttPlacement=h.placement.toLowerCase().trim(),e.centered=!1,e.ttMargin=h.margin,e.offsetHorizontal=0,e.offsetVertical=0,e.ttSourceScope=h.useSourceScope,e.ttOpen=!1,e.ttAnimation=h.animation,e.index=parseInt(m.tourtipStep,10);var t=d(k)(e);n.addStep(e),f(function(){e.$watch("ttOpen",function(a){a?q():r()})},500),e.$on("$destroy",function(){angular.element(b).unbind("resize."+e.$id),t.remove(),t=null}),e.proceed=function(){if(e.onStepProceed){var a=o(),b=a.$eval(e.onStepProceed);j.resolve(b).then(function(){e.setCurrentStep(e.getCurrentStep()+1)})}else e.setCurrentStep(e.getCurrentStep()+1)}}}}]).directive("tourPopup",function(){return{replace:!0,templateUrl:"tour/tour.tpl.html",scope:!0,restrict:"EA",link:function(a,b,c){}}}).factory("orderedList",function(){var a=function(){this.map={},this._array=[]};a.prototype.set=function(a,b){if(angular.isNumber(a))if(a in this.map)this.map[a]=b;else{if(a0?a-1:0;this._array.splice(c,0,a)}else this._array.push(a);this.map[a]=b,this._array.sort(function(a,b){return a-b})}},a.prototype.indexOf=function(a){for(var b in this.map)if(this.map.hasOwnProperty(b)&&this.map[b]===a)return Number(b)},a.prototype.push=function(a){var b=this._array[this._array.length-1]+1||0;this._array.push(b),this.map[b]=a,this._array.sort(function(a,b){return a-b})},a.prototype.remove=function(a){var b=this._array.indexOf(a);if(-1===b)throw new Error("key does not exist");this._array.splice(b,1),delete this.map[a]},a.prototype.get=function(a){return this.map[a]},a.prototype.getCount=function(){return this._array.length},a.prototype.forEach=function(a){for(var b,c,d=0;d-1&&c.showStepCallback(),a>=e.getCount()&&c.postTourCallback(!0),c.postStepCallback()}},c.addStep=function(a){d.isNumber(a.index)&&!isNaN(a.index)?e.set(a.index,a):e.push(a)},c.unselectAllSteps=function(){e.forEach(function(a){a.ttOpen=!1})},c.cancelTour=function(){c.unselectAllSteps(),c.postTourCallback(!1)},a.openTour=function(){var a=c.currentStep>=e.getCount()||c.currentStep<0?0:c.currentStep;c.select(a)},a.closeTour=function(){c.cancelTour()}}]).directive("tour",["$parse","$timeout","tourConfig",function(a,c,e){return{controller:"TourController",restrict:"EA",scope:!0,link:function(f,g,h,i){if(!d.isDefined(h.step))throw"The directive requires a `step` attribute to bind the current step to.";var j=a(h.step),k=!1;f.$watch(h.step,function(a){i.currentStep=a}),i.postTourCallback=function(a){var c=b.getElementsByClassName("tour-backdrop"),e=b.getElementsByClassName("tour-element-active");d.element(c).remove(),k=!1,d.element(e).removeClass("tour-element-active"),a&&d.isDefined(h.tourComplete)&&f.$parent.$eval(h.tourComplete),d.isDefined(h.postTour)&&f.$parent.$eval(h.postTour)},i.postStepCallback=function(){d.isDefined(h.postStep)&&f.$parent.$eval(h.postStep)},i.showStepCallback=function(){e.backDrop&&(c(function(){var a=b.getElementsByClassName("tour-backdrop"),c=b.getElementsByClassName("tour-tip")[0],e=b.createElement("div");e.className="tour-backdrop",d.element(a).remove(),d.isDefined(c)&&c.parentNode.insertBefore(e,c)},501),k=!0)},f.setCurrentStep=function(a){j.assign(f.$parent,a),i.currentStep=a},f.getCurrentStep=function(){return i.currentStep}}}}]).directive("tourtip",["$window","$compile","$interpolate","$timeout","scrollTo","tourConfig","debounce","$q",function(e,f,g,h,i,j,k,l){var m=(g.startSymbol(),g.endSymbol(),"
");return{require:"^tour",restrict:"EA",scope:!0,link:function(g,n,o,p){function q(){var a=b.querySelectorAll(g.ttElement),c=g.ttElement?d.element(a):n,e=g;return c===n||g.ttSourceScope||(e=c.scope()),e}function r(b,c){var d,e,f=0,h=v[0].offsetWidth,i=v[0].offsetHeight,j=b[0].getBoundingClientRect(),k=j.top+a.pageYOffset,l=0;if(c&&c[0]){k=k-c[0].getBoundingClientRect().top+c[0].scrollTop,"fixed"===c.css("position")&&(l=c[0].getBoundingClientRect().left);var m=c[0].getBoundingClientRect().width;h+j.width>m&&(d=m-j.left+g.ttMargin)}var n,o=h,p=i;switch(g.ttPlacement){case"right":n=j.left-l+j.width+g.ttMargin+g.offsetHorizontal,e={top:k+g.offsetVertical,left:n>0?n:f};break;case"bottom":n=j.left-l+g.offsetHorizontal,e={top:k+j.height+g.ttMargin+g.offsetVertical,left:n>0?n:f};break;case"center":n=j.left-l+.5*(j.width-o)+g.offsetHorizontal,e={top:k+.5*(j.height-p)+g.ttMargin+g.offsetVertical,left:n>0?n:f};break;case"center-top":n=j.left-l+.5*(j.width-o)+g.offsetHorizontal,e={top:k+.1*(j.height-p)+g.ttMargin+g.offsetVertical,left:n>0?n:f};break;case"left":n=j.left-l-o-g.ttMargin+g.offsetHorizontal,e={top:k+g.offsetVertical,left:n>0?n:f,right:d};break;default:n=j.left-l+g.offsetHorizontal,e={top:k-p-g.ttMargin+g.offsetVertical,left:n>0?n:f}}return e.top+="px",e.left+="px",e}function s(){if(g.ttContent){var a=b.querySelectorAll(g.ttElement),f=g.ttElement?d.element(a):n;if(null===f||0===f.length)throw"Target element could not be found. Selector: "+g.ttElement;var l=b.querySelectorAll(g.ttContainerElement);d.element(l).append(v);var m=function(){var a="body"===g.ttContainerElement?c:d.element(l),b=r(f,a);v.css(b),i(v,g.ttContainerElement,-150,-300,j.scrollSpeed)};if(j.backDrop&&u(f),d.element(e).bind("resize."+g.$id,k(m,50)),m(),v.addClass("show"),g.onStepShow){var o=q();h(function(){o.$eval(g.onStepShow)},300)}}}function t(){v.removeClass("show"),v.detach(),d.element(e).unbind("resize."+g.$id)}function u(a){var c=b.getElementsByClassName("tour-element-active");d.element(c).removeClass("tour-element-active"),g.centered||a.addClass("tour-element-active")}o.$observe("tourtip",function(a){g.ttContent=a}),o.$observe("tourtipPlacement",function(a){g.ttPlacement=(a||j.placement).toLowerCase().trim(),g.centered=0===g.ttPlacement.indexOf("center")}),o.$observe("tourtipNextLabel",function(a){g.ttNextLabel=a||j.nextLabel}),o.$observe("tourtipContainerElement",function(a){g.ttContainerElement=a||j.containerElement}),o.$observe("tourtipMargin",function(a){g.ttMargin=parseInt(a,10)||j.margin}),o.$observe("tourtipOffsetVertical",function(a){g.offsetVertical=parseInt(a,10)||0}),o.$observe("tourtipOffsetHorizontal",function(a){g.offsetHorizontal=parseInt(a,10)||0}),o.$observe("onShow",function(a){g.onStepShow=a||null}),o.$observe("onProceed",function(a){g.onStepProceed=a||null}),o.$observe("tourtipElement",function(a){g.ttElement=a||null}),o.$observe("tourtipTitle",function(a){g.ttTitle=a||null}),o.$observe("useSourceScope",function(a){g.ttSourceScope=a?"true"===a:j.useSourceScope}),g.ttNextLabel=j.nextLabel,g.ttContainerElement=j.containerElement,g.ttPlacement=j.placement.toLowerCase().trim(),g.centered=!1,g.ttMargin=j.margin,g.offsetHorizontal=0,g.offsetVertical=0,g.ttSourceScope=j.useSourceScope,g.ttOpen=!1,g.ttAnimation=j.animation,g.index=parseInt(o.tourtipStep,10);var v=f(m)(g);p.addStep(g),h(function(){g.$watch("ttOpen",function(a){a?s():t()})},500),g.$on("$destroy",function(){d.element(e).unbind("resize."+g.$id),v.remove(),v=null}),g.proceed=function(){if(g.onStepProceed){var a=q(),b=a.$eval(g.onStepProceed);l.resolve(b).then(function(){g.setCurrentStep(g.getCurrentStep()+1)})}else g.setCurrentStep(g.getCurrentStep()+1)}}}}]).directive("tourPopup",function(){return{replace:!0,templateUrl:"tour/tour.tpl.html",scope:!0,restrict:"EA",link:function(a,b,c){}}}).factory("orderedList",function(){var a=function(){this.map={},this._array=[]};a.prototype.set=function(a,b){if(d.isNumber(a))if(a in this.map)this.map[a]=b;else{if(a0?a-1:0;this._array.splice(c,0,a)}else this._array.push(a);this.map[a]=b,this._array.sort(function(a,b){return a-b})}},a.prototype.indexOf=function(a){for(var b in this.map)if(this.map.hasOwnProperty(b)&&this.map[b]===a)return Number(b)},a.prototype.push=function(a){var b=this._array[this._array.length-1]+1||0;this._array.push(b),this.map[b]=a,this._array.sort(function(a,b){return a-b})},a.prototype.remove=function(a){var b=this._array.indexOf(a);if(-1===b)throw new Error("key does not exist");this._array.splice(b,1),delete this.map[a]},a.prototype.get=function(a){return this.map[a]},a.prototype.getCount=function(){return this._array.length},a.prototype.forEach=function(a){for(var b,c,d=0;da?4*a*a*a:(a-1)*(2*a-2)*(2*a-2)+1}function d(b,d,f,g,h,i){function j(){n===l&&o===m&&(a.cancel(v),e=!1)}function k(){console.log("called"),s+=16,p=s/i,p=p>1?1:p;var a=c(p),d=t*a,e=u*a;n=q+d,o=r+e,b.scrollTop=n,b.scrollLeft=o,j()}if(!e){i=i||500,g=g||0,h=h||0;var l=d+g;0>l?l=0:l>b.scrollHeight&&(l=b.scrollHeight);var m=f+h;0>m?m=0:m>b.scrollWidth&&(m=b.scrollWidth);var n,o,p,q=b.scrollTop,r=b.scrollLeft,s=0,t=l-q,u=m-r;e=!0;var v=a(k,16)}}var e=!1;return function(a,c,e,f,g){var h=b.querySelectorAll(c);e=e||-100,f=f||-100,d(h[0],a[0].offsetTop,a[0].offsetLeft,e,f,g)}}]).factory("debounce",["$timeout","$q",function(a,b){return function(c,d,e){var f,g=b.defer();return function(){var h=this,i=arguments,j=function(){f=null,e||(g.resolve(c.apply(h,i)),g=b.defer())},k=e&&!f;return f&&a.cancel(f),f=a(j,d),k&&(g.resolve(c.apply(h,i)),g=b.defer()),g.promise}}}])}(angular)}(window,document); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index 7e40d48..7b5ea8f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,7 +9,6 @@ module.exports = function (config) { frameworks: ['jasmine'], files : [ - 'bower_components/jquery/jquery.js', 'bower_components/angular/angular.js', 'bower_components/angular-cookie/angular-cookie.js', 'bower_components/angular-mocks/angular-mocks.js', diff --git a/package.json b/package.json index bf38be7..4b3cce8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "angularjs", "directive", "angular", - "module" + "module", + "tour", + "walkthrough" ], "homepage": "https://github.com/DaftMonk/angular-tour", "bugs": "https://github.com/DaftMonk/angular-tour/issues", diff --git a/src/tour/tour.js b/src/tour/tour.js index 85581e3..b7de041 100644 --- a/src/tour/tour.js +++ b/src/tour/tour.js @@ -1,586 +1,655 @@ -'use strict'; - -angular.module('angular-tour.tour', []) - -/** - * tourConfig - * Default configuration, can be customized by injecting tourConfig into your app and modifying it - */ -.constant('tourConfig', { - placement: 'top', // default placement relative to target. 'top', 'right', 'left', 'bottom' - animation: true, // if tips fade in - nextLabel: 'Next', // default text in the next tip button - scrollSpeed: 500, // page scrolling speed in milliseconds - margin: 28, // how many pixels margin the tip is from the target - backDrop: false, // if there is a backdrop (gray overlay) when tour starts - useSourceScope: false, // only target scope should be used (only when using virtual steps) - containerElement: 'body' // default container element to parent tourtips to -}) - -/** - * TourController - * the logic for the tour, which manages all the steps - */ -.controller('TourController', function($scope, orderedList) { - var self = this, - steps = self.steps = orderedList(), - firstCurrentStepChange = true; - - // we'll pass these in from the directive - self.postTourCallback = angular.noop; - self.postStepCallback = angular.noop; - self.showStepCallback = angular.noop; - self.currentStep = -1; - - // if currentStep changes, select the new step - $scope.$watch(function() { - return self.currentStep; - }, - function(val) { - if (firstCurrentStepChange) { - firstCurrentStepChange = false; - } else { - self.select(val); - } +(function(angular){ + + 'use strict'; + + angular.module('angular-tour.tour', []) + + /** + * tourConfig + * Default configuration, can be customized by injecting tourConfig into your app and modifying it + */ + .constant('tourConfig', { + placement: 'top', // default placement relative to target. 'top', 'right', 'left', 'bottom' + animation: true, // if tips fade in + nextLabel: 'Next', // default text in the next tip button + scrollSpeed: 500, // page scrolling speed in milliseconds + margin: 28, // how many pixels margin the tip is from the target + backDrop: false, // if there is a backdrop (gray overlay) when tour starts + useSourceScope: false, // only target scope should be used (only when using virtual steps) + containerElement: 'body' // default container element to parent tourtips to + }) + + /** + * TourController + * the logic for the tour, which manages all the steps + */ + .controller('TourController', ['$scope', 'orderedList', + function($scope, orderedList) { + + var self = this, + steps = self.steps = orderedList(), + firstCurrentStepChange = true; + + // we'll pass these in from the directive + self.postTourCallback = angular.noop; + self.postStepCallback = angular.noop; + self.showStepCallback = angular.noop; + self.currentStep = -1; + + // if currentStep changes, select the new step + $scope.$watch(function () { + return self.currentStep; + }, function (val) { + if (firstCurrentStepChange) + firstCurrentStepChange = false; + else + self.select(val); } - ); + ); - self.select = function(nextIndex) { + self.select = function(nextIndex) { if (!angular.isNumber(nextIndex)) return; self.unselectAllSteps(); var step = steps.get(nextIndex); - if (step) { - step.ttOpen = true; - } + if (step) { step.ttOpen = true; } // update currentStep if we manually selected this index - if (self.currentStep !== nextIndex) { - self.currentStep = nextIndex; - } + if (self.currentStep !== nextIndex) { self.currentStep = nextIndex; } - if (self.currentStep > -1) - self.showStepCallback(); + if (self.currentStep > -1) { self.showStepCallback(); } + + if (nextIndex >= steps.getCount()) { self.postTourCallback(true); } - if (nextIndex >= steps.getCount()) { - self.postTourCallback(true); - } self.postStepCallback(); - }; + }; - self.addStep = function(step) { - if (angular.isNumber(step.index) && !isNaN(step.index)) { - steps.set(step.index, step); - } else { - steps.push(step); - } - }; + self.addStep = function (step) { + if (angular.isNumber(step.index) && !isNaN(step.index)) + steps.set(step.index, step); + else + steps.push(step); + }; - self.unselectAllSteps = function() { - steps.forEach(function(step) { - step.ttOpen = false; + self.unselectAllSteps = function() { + steps.forEach(function (step) { + step.ttOpen = false; }); - }; + }; - self.cancelTour = function() { + self.cancelTour = function() { self.unselectAllSteps(); self.postTourCallback(false); - }; + }; - $scope.openTour = function() { + $scope.openTour = function() { // open at first step if we've already finished tour var startStep = self.currentStep >= steps.getCount() || self.currentStep < 0 ? 0 : self.currentStep; self.select(startStep); - }; + }; - $scope.closeTour = function() { + $scope.closeTour = function() { self.cancelTour(); - }; -}) - -/** - * Tour - * directive that allows you to control the tour - */ -.directive('tour', function($parse, $timeout, tourConfig) { - return { + }; + }]) + + /** + * Tour + * directive that allows you to control the tour + */ + .directive('tour', ['$parse', '$timeout', 'tourConfig', + function($parse, $timeout, tourConfig) { + + return { controller: 'TourController', restrict: 'EA', scope: true, link: function(scope, element, attrs, ctrl) { - if (!angular.isDefined(attrs.step)) { - throw ('The directive requires a `step` attribute to bind the current step to.'); + if (!angular.isDefined(attrs.step)) { + throw ('The directive requires a `step` attribute to bind the current step to.'); + } + var model = $parse(attrs.step); + var backDrop = false; + + // Watch current step view model and update locally + scope.$watch(attrs.step, function(newVal) { + ctrl.currentStep = newVal; + }); + + ctrl.postTourCallback = function(completed) { + var backdropEle = document.getElementsByClassName('tour-backdrop'); + var active = document.getElementsByClassName('tour-element-active'); + angular.element(backdropEle).remove(); + backDrop = false; + angular.element(active).removeClass('tour-element-active'); + + if (completed && angular.isDefined(attrs.tourComplete)) { + scope.$parent.$eval(attrs.tourComplete); } - var model = $parse(attrs.step); - var backDrop = false; - - // Watch current step view model and update locally - scope.$watch(attrs.step, function(newVal) { - ctrl.currentStep = newVal; - }); - - ctrl.postTourCallback = function(completed) { - angular.element('.tour-backdrop').remove(); - backDrop = false; - angular.element('.tour-element-active').removeClass('tour-element-active'); - - if (completed && angular.isDefined(attrs.tourComplete)) { - scope.$parent.$eval(attrs.tourComplete); - } - if (angular.isDefined(attrs.postTour)) { - scope.$parent.$eval(attrs.postTour); - } - }; - ctrl.postStepCallback = function() { - if (angular.isDefined(attrs.postStep)) { - scope.$parent.$eval(attrs.postStep); - } - }; - - ctrl.showStepCallback = function() { - if (tourConfig.backDrop) { - angular.element(tourConfig.containerElement).append(angular.element('
')); - - $timeout(function() { - $('.tour-backdrop').remove(); - angular.element('
').insertBefore('.tour-tip'); - }, 1000) + if (angular.isDefined(attrs.postTour)) { + scope.$parent.$eval(attrs.postTour); + } + }; - backDrop = true; - } - }; + ctrl.postStepCallback = function() { + if (angular.isDefined(attrs.postStep)) { + scope.$parent.$eval(attrs.postStep); + } + }; + + ctrl.showStepCallback = function() { + if (tourConfig.backDrop) { + + $timeout(function() { + var backdrop = document.getElementsByClassName('tour-backdrop'); + var tooltip = document.getElementsByClassName('tour-tip')[0]; + var div = document.createElement('div'); + div.className = 'tour-backdrop'; + angular.element(backdrop).remove(); + // When the tour ends simply remove the backdrop and return. + if (!angular.isDefined(tooltip)) { return; } + tooltip.parentNode.insertBefore(div, tooltip); + }, 501); + + backDrop = true; + } + }; - // update the current step in the view as well as in our controller - scope.setCurrentStep = function(val) { - model.assign(scope.$parent, val); - ctrl.currentStep = val; - }; + // update the current step in the view as well as in our controller + scope.setCurrentStep = function(val) { + model.assign(scope.$parent, val); + ctrl.currentStep = val; + }; - scope.getCurrentStep = function() { - return ctrl.currentStep; - }; + scope.getCurrentStep = function() { + return ctrl.currentStep; + }; } - }; -}) + }; + }]) -/** - * Tourtip - * tourtip manages the state of the tour-popup directive - */ -.directive('tourtip', function($window, $compile, $interpolate, $timeout, scrollTo, tourConfig, debounce, $q) { - var startSym = $interpolate.startSymbol(), - endSym = $interpolate.endSymbol(); + /** + * Tourtip + * tourtip manages the state of the tour-popup directive + */ + .directive('tourtip', ['$window', '$compile', '$interpolate', '$timeout', 'scrollTo', 'tourConfig', 'debounce', '$q', + function($window, $compile, $interpolate, $timeout, scrollTo, tourConfig, debounce, $q) { - var template = '
'; + var startSym = $interpolate.startSymbol(), + endSym = $interpolate.endSymbol(); - return { + var template = '
'; + + return { require: '^tour', restrict: 'EA', scope: true, link: function(scope, element, attrs, tourCtrl) { - attrs.$observe('tourtip', function(val) { - scope.ttContent = val; - }); - - //defaults: tourConfig.placement - attrs.$observe('tourtipPlacement', function(val) { - scope.ttPlacement = (val || tourConfig.placement).toLowerCase().trim(); - scope.centered = (scope.ttPlacement.indexOf('center') === 0); - }); - - attrs.$observe('tourtipNextLabel', function(val) { - scope.ttNextLabel = val || tourConfig.nextLabel; - }); - - attrs.$observe('tourtipContainerElement', function(val) { - scope.ttContainerElement = val || tourConfig.containerElement; - }); - - attrs.$observe('tourtipMargin', function(val) { - scope.ttMargin = parseInt(val, 10) || tourConfig.margin; - }); - - attrs.$observe('tourtipOffsetVertical', function(val) { - scope.offsetVertical = parseInt(val, 10) || 0; - }); - - attrs.$observe('tourtipOffsetHorizontal', function(val) { - scope.offsetHorizontal = parseInt(val, 10) || 0; - }); - - //defaults: null - attrs.$observe('onShow', function(val) { - scope.onStepShow = val || null; - }); - - //defaults: null - attrs.$observe('onProceed', function(val) { - scope.onStepProceed = val || null; - }); - - //defaults: null - attrs.$observe('tourtipElement', function(val) { - scope.ttElement = val || null; - }); - - //defaults: null - attrs.$observe('tourtipTitle', function (val) { - scope.ttTitle = val || null; - }); - //defaults: tourConfig.useSourceScope - attrs.$observe('useSourceScope', function(val) { - scope.ttSourceScope = !val ? tourConfig.useSourceScope : val === 'true'; + attrs.$observe('tourtip', function(val) { + scope.ttContent = val; + }); + + //defaults: tourConfig.placement + attrs.$observe('tourtipPlacement', function(val) { + scope.ttPlacement = (val || tourConfig.placement).toLowerCase().trim(); + scope.centered = (scope.ttPlacement.indexOf('center') === 0); + }); + + attrs.$observe('tourtipNextLabel', function(val) { + scope.ttNextLabel = val || tourConfig.nextLabel; + }); + + attrs.$observe('tourtipContainerElement', function(val) { + scope.ttContainerElement = val || tourConfig.containerElement; + }); + + attrs.$observe('tourtipMargin', function(val) { + scope.ttMargin = parseInt(val, 10) || tourConfig.margin; + }); + + attrs.$observe('tourtipOffsetVertical', function(val) { + scope.offsetVertical = parseInt(val, 10) || 0; + }); + + attrs.$observe('tourtipOffsetHorizontal', function(val) { + scope.offsetHorizontal = parseInt(val, 10) || 0; + }); + + //defaults: null + attrs.$observe('onShow', function(val) { + scope.onStepShow = val || null; + }); + + //defaults: null + attrs.$observe('onProceed', function(val) { + scope.onStepProceed = val || null; + }); + + //defaults: null + attrs.$observe('tourtipElement', function(val) { + scope.ttElement = val || null; + }); + + //defaults: null + attrs.$observe('tourtipTitle', function (val) { + scope.ttTitle = val || null; + }); + + //defaults: tourConfig.useSourceScope + attrs.$observe('useSourceScope', function(val) { + scope.ttSourceScope = !val ? tourConfig.useSourceScope : val === 'true'; + }); + + //Init assignments (fix for Angular 1.3+) + scope.ttNextLabel = tourConfig.nextLabel; + scope.ttContainerElement = tourConfig.containerElement; + scope.ttPlacement = tourConfig.placement.toLowerCase().trim(); + scope.centered = false; + scope.ttMargin = tourConfig.margin; + scope.offsetHorizontal = 0; + scope.offsetVertical = 0; + scope.ttSourceScope = tourConfig.useSourceScope; + scope.ttOpen = false; + scope.ttAnimation = tourConfig.animation; + scope.index = parseInt(attrs.tourtipStep, 10); + + var tourtip = $compile(template)(scope); + tourCtrl.addStep(scope); + + // wrap this in a time out because the tourtip won't compile right away + $timeout(function() { + scope.$watch('ttOpen', function(val) { + if (val) + show(); + else + hide(); }); - - //Init assignments (fix for Angular 1.3+) - scope.ttNextLabel = tourConfig.nextLabel; - scope.ttContainerElement = tourConfig.containerElement; - scope.ttPlacement = tourConfig.placement.toLowerCase().trim(); - scope.centered = false; - scope.ttMargin = tourConfig.margin; - scope.offsetHorizontal = 0; - scope.offsetVertical = 0; - scope.ttSourceScope = tourConfig.useSourceScope; - scope.ttOpen = false; - scope.ttAnimation = tourConfig.animation; - scope.index = parseInt(attrs.tourtipStep, 10); - - var tourtip = $compile(template)(scope); - tourCtrl.addStep(scope); - - // wrap this in a time out because the tourtip won't compile right away - $timeout(function() { - scope.$watch('ttOpen', function(val) { - if (val) { - show(); - } else { - hide(); - } - }); - }, 500); - - - //determining target scope. It's used only when using virtual steps and there - //is some action performed like on-show or on-progress. Without virtual steps - //action would performed on element's scope and that would work just fine - //however, when using virtual steps, whose steps can be placed in different - //controller, so it affects scope, which will be used to run this action against. - function getTargetScope() { - var targetElement = scope.ttElement ? angular.element(scope.ttElement) : element; - - var targetScope = scope; - if (targetElement !== element && !scope.ttSourceScope) - targetScope = targetElement.scope(); - - return targetScope; + }, 500); + + + //determining target scope. It's used only when using virtual steps and there + //is some action performed like on-show or on-progress. Without virtual steps + //action would performed on element's scope and that would work just fine + //however, when using virtual steps, whose steps can be placed in different + //controller, so it affects scope, which will be used to run this action against. + function getTargetScope() { + var target = document.querySelectorAll(scope.ttElement); + var targetElement = scope.ttElement ? angular.element(target) : element; + + var targetScope = scope; + if (targetElement !== element && !scope.ttSourceScope) { targetScope = targetElement.scope(); } + + return targetScope; + } + + function calculatePosition(element, container) { + var minimumLeft = 0; // minimum left position of tour tip + var restrictRight; + var ttPosition; + var tourtipWidth = tourtip[0].offsetWidth; + var tourtipHeight = tourtip[0].offsetHeight; + + // Get the position of the directive element + var position = element[0].getBoundingClientRect(); + + //make it relative against page or fixed container, not the window + var top = position.top + window.pageYOffset; + var containerLeft = 0; + if (container && container[0]) { + top = top - container[0].getBoundingClientRect().top + container[0].scrollTop; + // if container is fixed, position tour tip relative to fixed container + if (container.css('position') === 'fixed') { + containerLeft = container[0].getBoundingClientRect().left; + } + // restrict right position if the tourtip doesn't fit in the container + var containerWidth = container[0].getBoundingClientRect().width; + if (tourtipWidth + position.width > containerWidth) { + restrictRight = containerWidth - position.left + scope.ttMargin; + } } - function calculatePosition(element, container) { - var minimumLeft = 0; // minimum left position of tour tip - var restrictRight; - var ttPosition; - - // Get the position of the directive element - var position = element[0].getBoundingClientRect(); - - //make it relative against page or fixed container, not the window - var top = position.top + window.pageYOffset; - var containerLeft = 0; - if (container && container[0]) { - top = top - container[0].getBoundingClientRect().top + container[0].scrollTop; - // if container is fixed, position tour tip relative to fixed container - if (container.css('position') === 'fixed') { - containerLeft = container[0].getBoundingClientRect().left; - } - // restrict right position if the tourtip doesn't fit in the container - var containerWidth = container[0].getBoundingClientRect().width; - if (tourtip.width() + position.width > containerWidth) { - restrictRight = containerWidth - position.left + scope.ttMargin; - } - } - - var ttWidth = tourtip.width(); - var ttHeight = tourtip.height(); - - // Calculate the tourtip's top and left coordinates to center it - switch (scope.ttPlacement) { - case 'right': - var _left = position.left - containerLeft + position.width + scope.ttMargin + scope.offsetHorizontal; - ttPosition = { - top: top + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'bottom': - var _left = position.left - containerLeft + scope.offsetHorizontal; - ttPosition = { - top: top + position.height + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'center': - var _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; - ttPosition = { - top: top + 0.5 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'center-top': - var _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; - ttPosition = { - top: top + 0.1 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - case 'left': - var _left = position.left - containerLeft - ttWidth - scope.ttMargin + scope.offsetHorizontal; - ttPosition = { - top: top + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft, - right: restrictRight - }; - break; - default: - var _left = position.left - containerLeft + scope.offsetHorizontal; - ttPosition = { - top: top - ttHeight - scope.ttMargin + scope.offsetVertical, - left: _left > 0 ? _left : minimumLeft - }; - break; - } - - ttPosition.top += 'px'; - ttPosition.left += 'px'; - - return ttPosition; + var ttWidth = tourtipWidth; + var ttHeight = tourtipHeight; + + // Calculate the tourtip's top and left coordinates to center it + var _left; + switch(scope.ttPlacement) { + case 'right': + _left = position.left - containerLeft + position.width + scope.ttMargin + scope.offsetHorizontal; + ttPosition = { + top: top + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'bottom': + _left = position.left - containerLeft + scope.offsetHorizontal; + ttPosition = { + top: top + position.height + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'center': + _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; + ttPosition = { + top: top + 0.5 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'center-top': + _left = position.left - containerLeft + 0.5 * (position.width - ttWidth) + scope.offsetHorizontal; + ttPosition = { + top: top + 0.1 * (position.height - ttHeight) + scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; + case 'left': + _left = position.left - containerLeft - ttWidth - scope.ttMargin + scope.offsetHorizontal; + ttPosition = { + top: top + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft, + right: restrictRight + }; + break; + default: + _left = position.left - containerLeft + scope.offsetHorizontal; + ttPosition = { + top: top - ttHeight - scope.ttMargin + scope.offsetVertical, + left: _left > 0 ? _left : minimumLeft + }; + break; } - function show() { - if (!scope.ttContent) { - return; - } + ttPosition.top += 'px'; + ttPosition.left += 'px'; - if (scope.ttAnimation) - tourtip.fadeIn(); - else { - tourtip.css({ - display: 'block' - }); - } + return ttPosition; + } - var targetElement = scope.ttElement ? angular.element(scope.ttElement) : element; + function show() { + if (!scope.ttContent) { return; } - if (targetElement == null || targetElement.length === 0) - throw 'Target element could not be found. Selector: ' + scope.ttElement; + var target = document.querySelectorAll(scope.ttElement); + var targetElement = scope.ttElement ? angular.element(target) : element; - angular.element(scope.ttContainerElement).append(tourtip); + if (targetElement === null || targetElement.length === 0) + throw 'Target element could not be found. Selector: ' + scope.ttElement; - var updatePosition = function() { + var containerEle = document.querySelectorAll(scope.ttContainerElement); + angular.element(containerEle).append(tourtip); - var offsetElement = scope.ttContainerElement === 'body' ? undefined : angular.element(scope.ttContainerElement); - var ttPosition = calculatePosition(targetElement, offsetElement); + var updatePosition = function() { - // Now set the calculated positioning. - tourtip.css(ttPosition); + var offsetElement = scope.ttContainerElement === 'body' ? undefined : angular.element(containerEle); + var ttPosition = calculatePosition(targetElement, offsetElement); - // Scroll to the tour tip - var ttPositionTop = parseInt(ttPosition.top), - ttPositionLeft = parseInt(ttPosition.left); - scrollTo(tourtip, scope.ttContainerElement, -150, -300, tourConfig.scrollSpeed, ttPositionTop, ttPositionLeft); - }; + // Now set the calculated positioning. + tourtip.css(ttPosition); - if (tourConfig.backDrop) - focusActiveElement(targetElement); + // Scroll to the tour tip + scrollTo(tourtip, scope.ttContainerElement, -150, -300, tourConfig.scrollSpeed); + }; - angular.element($window).bind('resize.' + scope.$id, debounce(updatePosition, 50)); + if (tourConfig.backDrop) { focusActiveElement(targetElement); } - updatePosition(); + angular.element($window).bind('resize.' + scope.$id, debounce(updatePosition, 50)); - if (scope.onStepShow) { - var targetScope = getTargetScope(); + updatePosition(); - //fancy! Let's make on show action not instantly, but after a small delay - $timeout(function() { - targetScope.$eval(scope.onStepShow); - }, 300); - } - } + // CSS class must be added after the element is already on the DOM otherwise it won't animate (fade in). + tourtip.addClass('show'); - function hide() { - tourtip.detach(); - angular.element($window).unbind('resize.' + scope.$id); - } + if (scope.onStepShow) { + var targetScope = getTargetScope(); - function focusActiveElement(el) { - angular.element('.tour-element-active').removeClass('tour-element-active'); - - if (!scope.centered) - el.addClass('tour-element-active'); + //fancy! Let's make on show action not instantly, but after a small delay + $timeout(function() { + targetScope.$eval(scope.onStepShow); + }, 300); } - - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTourtip() { - angular.element($window).unbind('resize.' + scope.$id); - tourtip.remove(); - tourtip = null; - }); - - scope.proceed = function() { - if (scope.onStepProceed) { - var targetScope = getTargetScope(); - - var onProceedResult = targetScope.$eval(scope.onStepProceed); - $q.resolve(onProceedResult).then(function () { - scope.setCurrentStep(scope.getCurrentStep() + 1); - }); - } else { - scope.setCurrentStep(scope.getCurrentStep() + 1); - } - }; + } + + function hide() { + tourtip.removeClass('show'); + tourtip.detach(); + angular.element($window).unbind('resize.' + scope.$id); + } + + function focusActiveElement(el) { + var activeEle = document.getElementsByClassName('tour-element-active'); + angular.element(activeEle).removeClass('tour-element-active'); + if (!scope.centered) { el.addClass('tour-element-active'); } + } + + // Make sure tooltip is destroyed and removed. + scope.$on('$destroy', function onDestroyTourtip() { + angular.element($window).unbind('resize.' + scope.$id); + tourtip.remove(); + tourtip = null; + }); + + scope.proceed = function() { + if (scope.onStepProceed) { + var targetScope = getTargetScope(); + + var onProceedResult = targetScope.$eval(scope.onStepProceed); + $q.resolve(onProceedResult).then(function () { + scope.setCurrentStep(scope.getCurrentStep() + 1); + }); + } else { + scope.setCurrentStep(scope.getCurrentStep() + 1); + } + }; } - }; -}) - -/** - * TourPopup - * the directive that actually has the template for the tip - */ -.directive('tourPopup', function() { - return { + }; + }]) + + /** + * TourPopup + * the directive that actually has the template for the tip + */ + .directive('tourPopup', function() { + return { replace: true, templateUrl: 'tour/tour.tpl.html', scope: true, restrict: 'EA', link: function(scope, element, attrs) {} - }; -}) - -/** - * OrderedList - * Used for keeping steps in order - */ -.factory('orderedList', function() { - var OrderedList = function() { + }; + }) + + /** + * OrderedList + * Used for keeping steps in order + */ + .factory('orderedList', function() { + var OrderedList = function() { this.map = {}; this._array = []; - }; + }; - OrderedList.prototype.set = function(key, value) { + OrderedList.prototype.set = function(key, value) { if (!angular.isNumber(key)) - return; + return; if (key in this.map) { - this.map[key] = value; + this.map[key] = value; } else { - if (key < this._array.length) { - var insertIndex = key - 1 > 0 ? key - 1 : 0; - this._array.splice(insertIndex, 0, key); - } else { - this._array.push(key); - } - this.map[key] = value; - this._array.sort(function(a, b) { - return a - b; - }); + if (key < this._array.length) { + var insertIndex = key - 1 > 0 ? key - 1 : 0; + this._array.splice(insertIndex, 0, key); + } else { + this._array.push(key); + } + this.map[key] = value; + this._array.sort(function(a, b) { + return a - b; + }); } - }; - OrderedList.prototype.indexOf = function(value) { + }; + + OrderedList.prototype.indexOf = function(value) { for (var prop in this.map) { - if (this.map.hasOwnProperty(prop)) { - if (this.map[prop] === value) - return Number(prop); - } + if (this.map.hasOwnProperty(prop)) { + if (this.map[prop] === value) + return Number(prop); + } } - }; - OrderedList.prototype.push = function(value) { + }; + + OrderedList.prototype.push = function(value) { var key = this._array[this._array.length - 1] + 1 || 0; this._array.push(key); this.map[key] = value; this._array.sort(function(a, b) { - return a - b; + return a - b; }); - }; - OrderedList.prototype.remove = function(key) { + }; + + OrderedList.prototype.remove = function(key) { var index = this._array.indexOf(key); if (index === -1) { - throw new Error('key does not exist'); + throw new Error('key does not exist'); } this._array.splice(index, 1); delete this.map[key]; - }; - OrderedList.prototype.get = function(key) { + }; + + OrderedList.prototype.get = function(key) { return this.map[key]; - }; - OrderedList.prototype.getCount = function() { + }; + + OrderedList.prototype.getCount = function() { return this._array.length; - }; - OrderedList.prototype.forEach = function(f) { + }; + + OrderedList.prototype.forEach = function(f) { var key, value; for (var i = 0; i < this._array.length; i++) { - key = this._array[i]; - value = this.map[key]; - f(value, key); + key = this._array[i]; + value = this.map[key]; + f(value, key); } - }; - OrderedList.prototype.first = function() { + }; + + OrderedList.prototype.first = function() { var key, value; key = this._array[0]; value = this.map[key]; return value; - }; + }; - var orderedListFactory = function() { + var orderedListFactory = function() { return new OrderedList(); - }; - - return orderedListFactory; -}) - -/** - * ScrollTo - * Smoothly scroll to a dom element - */ -.factory('scrollTo', function() { - return function(target, containerElement, offsetY, offsetX, speed, ttPositionTop, ttPositionLeft) { - if (target) { - offsetY = offsetY || -100; - offsetX = offsetX || -100; - speed = speed || 500; - $('html,' + containerElement).stop().animate({ - scrollTop: ttPositionTop + offsetY, - scrollLeft: ttPositionLeft + offsetX - }, speed); - } else { - $('html,' + containerElement).stop().animate({ - scrollTop: 0 - }, speed); - } - }; -}) -.factory('debounce', function($timeout, $q) { - return function(func, wait, immediate) { - var timeout; - var deferred = $q.defer(); - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if(!immediate) { - deferred.resolve(func.apply(context, args)); - deferred = $q.defer(); - } }; - var callNow = immediate && !timeout; - if ( timeout ) { - $timeout.cancel(timeout); + + return orderedListFactory; + }) + + /** + * ScrollTo + * Smoothly scroll to a dom element + */ + .factory('scrollTo', ['$interval', function($interval) { + + var animationInProgress = false; + + function getEasingPattern (time) { + return time < 0.5 ? (4 * time * time * time) : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // default easeInOutCubic transition } - timeout = $timeout(later, wait); - if (callNow) { - deferred.resolve(func.apply(context,args)); - deferred = $q.defer(); + + function _autoScroll (container, endTop, endLeft, offsetY, offsetX, speed) { + + if (animationInProgress) { return; } + + speed = speed || 500; + offsetY = offsetY || 0; + offsetX = offsetX || 0; + // Set some boundaries in case the offset wants us to scroll to impossible locations + var finalY = endTop + offsetY; + if (finalY < 0) { finalY = 0; } else if (finalY > container.scrollHeight) { finalY = container.scrollHeight; } + var finalX = endLeft + offsetX; + if (finalX < 0) { finalX = 0; } else if (finalX > container.scrollWidth) { finalX = container.scrollWidth; } + + var startTop = container.scrollTop, + startLeft = container.scrollLeft, + timeLapsed = 0, + distanceY = finalY - startTop, // If we're going up, this will be a negative number + distanceX = finalX - startLeft, + currentPositionY, + currentPositionX, + timeProgress; + + function stopAnimation() { + // If we have reached our destination clear the interval + if (currentPositionY === finalY && currentPositionX === finalX) { + $interval.cancel(runAnimation); + animationInProgress = false; + } + } + + function animateScroll() { + console.log('called'); + timeLapsed += 16; + // get percentage of progress to the specified speed (e.g. 16/500). Should always be between 0 and 1 + timeProgress = ( timeLapsed / speed ); + // Make a check and set back to 1 if we went over (e.g. 512/500) + timeProgress = ( timeProgress > 1 ) ? 1 : timeProgress; + // Number between 0 and 1 corresponding to the animation pattern + var multiplier = getEasingPattern(timeProgress); + // Calculate the distance to travel in this step. It is the total distance times a percentage of what we will move + var translateY = distanceY * multiplier; + var translateX = distanceX * multiplier; + // Assign to the shorthand variables + currentPositionY = startTop + translateY; + currentPositionX = startLeft + translateX; + // Move slightly following the easing pattern + container.scrollTop = currentPositionY; + container.scrollLeft = currentPositionX; + // Check if we have reached our destination + stopAnimation(); + } + + animationInProgress = true; + // Kicks off the function + var runAnimation = $interval(animateScroll, 16); } - return deferred.promise; - }; - }; -}); + + return function(target, containerSelector, offsetY, offsetX, speed) { + var container = document.querySelectorAll(containerSelector); + offsetY = offsetY || -100; + offsetX = offsetX || -100; + _autoScroll(container[0], target[0].offsetTop, target[0].offsetLeft, offsetY, offsetX, speed); + }; + }]) + + .factory('debounce', ['$timeout', '$q', + function($timeout, $q) { + + return function(func, wait, immediate) { + var timeout; + var deferred = $q.defer(); + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if(!immediate) { + deferred.resolve(func.apply(context, args)); + deferred = $q.defer(); + } + }; + var callNow = immediate && !timeout; + if ( timeout ) { + $timeout.cancel(timeout); + } + timeout = $timeout(later, wait); + if (callNow) { + deferred.resolve(func.apply(context,args)); + deferred = $q.defer(); + } + return deferred.promise; + }; + }; + }]); + +})(angular); diff --git a/src/tour/tour.scss b/src/tour/tour.scss index 7a875db..924ae98 100644 --- a/src/tour/tour.scss +++ b/src/tour/tour.scss @@ -1,15 +1,30 @@ .tour-tip { - display: none; + opacity: 0; + visibility: hidden; + display: block; + + -webkit-transition: visibility 0s, opacity 0.6s cubic-bezier(0.345, 0, 0.25, 1); + -moz-transition: visibility 0s, opacity 0.6s cubic-bezier(0.345, 0, 0.25, 1); + -ms-transition: visibility 0s, opacity 0.6s cubic-bezier(0.345, 0, 0.25, 1); + -o-transition: visibility 0s, opacity 0.6s cubic-bezier(0.345, 0, 0.25, 1); + transition: visibility 0s, opacity 0.6s cubic-bezier(0.345, 0, 0.25, 1); + position: absolute; background: #1C252E; color: #FFF; - z-index: 101; + z-index: 121 !important; top: 0; left: 2.5%; font-family: inherit; font-weight: 400; max-width: 400px; border-radius: 10px; + + &.show { + opacity: 1; + visibility: visible; + } + p { color: #CBD0D4; font-size: .9em; diff --git a/src/tour/tour.spec.js b/src/tour/tour.spec.js index da3f40b..c94f5a0 100644 --- a/src/tour/tour.spec.js +++ b/src/tour/tour.spec.js @@ -7,7 +7,14 @@ describe('Directive: tour', function () { beforeEach(module('angular-tour.tour')); function getTourStep() { - return angular.element('body').find('div.tour-tip'); + var tour = document.querySelector('div.tour-tip'); + return angular.element(tour); + } + + function htmlToElement(html) { + var template = document.createElement('template'); + template.innerHTML = html; + return template.content.childNodes; } var $rootScope, $compile, $controller, $timeout; @@ -118,7 +125,7 @@ describe('Directive: tour', function () { }); describe('basics', function() { - var elm, scope, tour, tip1, tip2, tourScope; + var elm, scope, tourTpl, tour, tip1, tip2, tourScope; beforeEach(function() { @@ -126,14 +133,15 @@ describe('Directive: tour', function () { scope.stepIndex = 0; - tour = angular.element('' + - 'Important website feature' + - 'Another website feature' + - ''); + tourTpl = htmlToElement('' + + 'Important website feature' + + 'Another website feature' + + '')[0]; + tour = angular.element(tourTpl); elm = $compile(tour)(scope); - tip1 = tour.find('#tt1'); - tip2 = tour.find('#tt2'); + tip1 = angular.element( document.getElementById('#tt1') ); + tip2 = angular.element( document.getElementById('#tt2') ); scope.$apply(); $timeout.flush(); @@ -161,7 +169,7 @@ describe('Directive: tour', function () { }); describe('tourtip', function() { - var elm, scope, tour, tip1, tip2, tourScope; + var elm, scope, tourTpl, tour, tip1, tip2, tourScope; beforeEach(function() { @@ -169,14 +177,17 @@ describe('Directive: tour', function () { scope.stepIndex = 0; - tour = angular.element('' + - 'Important website feature' + - 'Another website feature' + - ''); + tourTpl = htmlToElement('')[0]; + var feature1 = htmlToElement('Important website feature')[0]; + var feature2 = htmlToElement('Another website feature')[0]; + + tour = angular.element(tourTpl); + tour.append(feature1); + tour.append(feature2); elm = $compile(tour)(scope); - tip1 = tour.find('#tt1'); - tip2 = tour.find('#tt2'); + tip1 = tour.children().eq(0); + tip2 = tour.children().eq(1); scope.$apply(); $timeout.flush(); @@ -194,12 +205,12 @@ describe('Directive: tour', function () { expect(elm).toHaveOpenTourtips(1); var stepText = tip1.attr('tourtip'); var tourStep = getTourStep(); - expect(tourStep.html()).toContain('feature 1'); + expect(tourStep.html()).toContain(stepText); expect(tip1.scope().ttOpen).toBe(true); }); it('should open tip2 popup and close tip1 on next', function () { - var elmNext = getTourStep().find('.tour-next-tip').eq(0); + var elmNext = getTourStep()[0].querySelector('.tour-next-tip'); elmNext.click(); @@ -218,7 +229,7 @@ describe('Directive: tour', function () { }); it('should close tips when you click close', function () { - var elmNext = getTourStep().find('.tour-close-tip').eq(0); + var elmNext = getTourStep()[0].querySelector('.tour-close-tip'); elmNext.click(); expect(tip1.scope().ttOpen).toBe(false); @@ -229,7 +240,7 @@ describe('Directive: tour', function () { scope.onProceedFunction = function() {}; scope.ttSourceScope = true; spyOn(scope, 'onProceedFunction'); - var tour1Next = getTourStep().find('.tour-next-tip').eq(0); + var tour1Next = getTourStep()[0].querySelector('.tour-next-tip'); tour1Next.click(); $timeout.flush(); @@ -268,13 +279,9 @@ describe('Directive: tour', function () { scope.otherStepIndex = -1; // set up first tour - tour = angular.element(''); - tip1 = angular.element('' + - 'Important website feature' + - ''); - tip2 = angular.element('' + - 'Another website feature' + - ''); + tour = angular.element(htmlToElement('')[0]); + tip1 = angular.element(htmlToElement('Important website feature')[0]); + tip2 = angular.element(htmlToElement('Another website feature')[0]); tour.append(tip1); tour.append(tip2); @@ -291,7 +298,7 @@ describe('Directive: tour', function () { it('should call post-tour method', function() { scope.tourEnd = function() {}; spyOn(scope, 'tourEnd'); - var tour1Next = getTourStep().find('.tour-next-tip').eq(0); + var tour1Next = getTourStep()[0].querySelector('.tour-next-tip'); tour1Next.click(); tour1Next.click(); expect(scope.tourEnd).toHaveBeenCalled(); @@ -307,7 +314,7 @@ describe('Directive: tour', function () { it('should call tour-complete method', function() { scope.tourComplete = function() {}; spyOn(scope, 'tourComplete'); - var tour1Next = getTourStep().find('.tour-next-tip').eq(0); + var tour1Next = getTourStep()[0].querySelector('.tour-next-tip'); tour1Next.click(); tour1Next.click(); expect(scope.tourComplete).toHaveBeenCalled(); @@ -323,7 +330,7 @@ describe('Directive: tour', function () { it('should call post-step method', function() { scope.tourStep = function() {}; spyOn(scope, 'tourStep'); - var tour1Next = getTourStep().find('.tour-next-tip').eq(0); + var tour1Next = getTourStep()[0].querySelector('.tour-next-tip'); tour1Next.click(); expect(scope.tourStep).toHaveBeenCalled(); }); @@ -334,12 +341,14 @@ describe('Directive: tour', function () { scope.otherStepIndex = 0; }; + var tourWrapper = htmlToElement('')[0]; + var feature1 = htmlToElement('Important website feature')[0]; + var feature2 = htmlToElement('Another website feature')[0]; + // set up second tour - var otherTour = angular.element(''); - var otherTip1 = angular.element('' + - 'Important website feature' + ''); - var otherTip2 = angular.element('' + - 'Another website feature' + ''); + var otherTour = angular.element(tourWrapper); + var otherTip1 = angular.element(feature1); + var otherTip2 = angular.element(feature2); otherTour.append(otherTip1); otherTour.append(otherTip2); @@ -349,7 +358,7 @@ describe('Directive: tour', function () { scope.$apply(); $timeout.flush(); - var tour1Next = getTourStep().find('.tour-next-tip').eq(0); + var tour1Next = getTourStep()[0].querySelector('.tour-next-tip'); // expect second tour to be closed expect(otherTip1.scope().ttOpen).toBe(false); @@ -367,7 +376,7 @@ describe('Directive: tour', function () { otherElm = $compile(otherTour)(scope); scope.$apply(); $timeout.flush(); - var tour2Next = getTourStep().find('.tour-next-tip').eq(0); + var tour2Next = getTourStep()[0].querySelector('.tour-next-tip'); // expect second tour to open after first tour finished expect(otherTip1.scope().ttOpen).toBe(true); @@ -441,28 +450,42 @@ describe('Directive: tour', function () { }); describe('scroll service', function() { - var target, scope, scrollTo; + var div, target, bodyEle, body, scope, scrollTo, arrived; beforeEach(inject(function (_scrollTo_) { scope = $rootScope.$new(); scrollTo = _scrollTo_; - - target = angular.element('
'); - $('body').height(window.innerHeight*2).append(target); - window.scrollTo(0, 0); + + div = htmlToElement('
Some element absolutely positioned
')[0]; + target = angular.element(div); + + bodyEle = document.querySelector('body'); + bodyEle.style.height = window.innerHeight * 2 + 'px'; + body = angular.element(bodyEle); + + body.append(target); })); it('should scroll to position', function () { - expect($(window).scrollTop()).toEqual(0); + expect( bodyEle.scrollTop ).toEqual(0); + scrollTo(target, 'body', -100, -100, 500); + + runs(function() { + arrived = false; + + setTimeout(function(){ + arrived = true; + }, 500); + }); - scrollTo(target, 'body', -100, -100, 500, 200, 0); waitsFor(function() { - return $(window).scrollTop() === 100; - }, 'Current position to be 100px'); + return arrived; + }, 'Should have scrolled', 1000); runs(function() { - expect($(window).scrollTop()).toEqual(100); + expect( bodyEle.scrollTop ).toBeGreaterThan(0); }); + }); }); });