From ea830fd4640d79bb84bb082aca4d5204ae4ebb19 Mon Sep 17 00:00:00 2001 From: joecoolish Date: Fri, 10 Jun 2016 00:10:22 -0400 Subject: [PATCH 1/5] Added TypeScript implementation of the library First stab at a TypeScript implementation of the library. The comments might not have fully gotten ported over, but I'm pretty sure the logic is all there! --- app/angular-ui-tour.ts | 1384 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1384 insertions(+) create mode 100644 app/angular-ui-tour.ts diff --git a/app/angular-ui-tour.ts b/app/angular-ui-tour.ts new file mode 100644 index 0000000..ef4d7fe --- /dev/null +++ b/app/angular-ui-tour.ts @@ -0,0 +1,1384 @@ +/// +/// +/// +/* global Tour: false */ + +module Tour { + export interface ITourScope extends ng.IScope { + tour: TourController; + tourStep: ITourStep; + + originScope: () => ITourScope; + isOpen: () => boolean; + } + + export interface ITourStep { + nextPath?; + prevPath?; + backdrop?; + stepId?; + trustedContent?; + content?: string; + order?: number; + templateUrl?: string; + element?: ng.IRootElementService; + enabled?: boolean; + preventScrolling?: boolean; + fixed?: boolean; + isNext?: boolean; + isPrev?: boolean; + redirectNext?: boolean; + redirectPrev?: boolean; + nextStep?: ITourStep; + prevStep?: ITourStep; + show?: () => PromiseLike; + hide?: () => PromiseLike; + onNext?: () => PromiseLike; + onPrev?: () => PromiseLike; + onShow?: () => PromiseLike; + onHide?: () => PromiseLike; + onShown?: () => PromiseLike; + onHidden?: () => PromiseLike; + config?: (string) => any; + } + + export interface ITourConfig { + get: (option: string) => any; + getAll: () => ITourConfigProperties; + } + + export interface ITourConfigProperties { + name?: string; + placement: string; + animation: boolean; + popupDelay: number; + closePopupDelay: number; + enable: boolean; + appendToBody: boolean; + popupClass: string; + orphan: boolean; + backdrop: boolean; + backdropZIndex: number; + scrollOffset: number; + scrollIntoView: boolean; + useUiRouter: boolean; + useHotkeys: boolean; + + onStart: (any?) => any; + onEnd: (any?) => any; + onPause: (any?) => any; + onResume: (any?) => any; + onNext: (any?) => any; + onPrev: (any?) => any; + onShow: (any?) => any; + onShown: (any?) => any; + onHide: (any?) => any; + onHidden: (any?) => any; + + } + + export class TourBackdrop { + private $body: ng.IRootElementService; + private viewWindow: { top: ng.IRootElementService, bottom: ng.IRootElementService, left: ng.IRootElementService, right: ng.IRootElementService }; + + private preventDefault(e) { + e.preventDefault(); + } + + private preventScrolling() { + this.$body.addClass('no-scrolling'); + this.$body.on('touchmove', this.preventDefault); + } + + private allowScrolling() { + this.$body.removeClass('no-scrolling'); + this.$body.off('touchmove', this.preventDefault); + } + + private createNoScrollingClass() { + var name = '.no-scrolling', + rules = 'height: 100%; overflow: hidden;', + style = document.createElement('style'); + style.type = 'text/css'; + document.getElementsByTagName('head')[0].appendChild(style); + + if (!style.sheet && !(style.sheet).insertRule) { + ((style).styleSheet || style.sheet).addRule(name, rules); + } else { + (style.sheet).insertRule(name + '{' + rules + '}', 0); + } + } + + private createBackdropComponent(backdrop) { + backdrop.addClass('tour-backdrop').addClass('not-shown').css({ + //display: 'none', + zIndex: this.TourConfig.get('backdropZIndex') + }); + this.$body.append(backdrop); + } + + private showBackdrop() { + this.viewWindow.top.removeClass('hidden'); + this.viewWindow.bottom.removeClass('hidden'); + this.viewWindow.left.removeClass('hidden'); + this.viewWindow.right.removeClass('hidden'); + + setTimeout(() => { + this.viewWindow.top.removeClass('not-shown'); + this.viewWindow.bottom.removeClass('not-shown'); + this.viewWindow.left.removeClass('not-shown'); + this.viewWindow.right.removeClass('not-shown'); + }, 33); + } + + private hideBackdrop() { + this.viewWindow.top.addClass('not-shown'); + this.viewWindow.bottom.addClass('not-shown'); + this.viewWindow.left.addClass('not-shown'); + this.viewWindow.right.addClass('not-shown'); + + setTimeout(() => { + this.viewWindow.top.addClass('hidden'); + this.viewWindow.bottom.addClass('hidden'); + this.viewWindow.left.addClass('hidden'); + this.viewWindow.right.addClass('hidden'); + }, 250); + } + + createForElement(element: ng.IRootElementService, shouldPreventScrolling: boolean, isFixedElement: boolean) { + var position, + viewportPosition, + bodyPosition; + + if (shouldPreventScrolling) { + this.preventScrolling(); + } + + position = this.$uibPosition.offset(element); + viewportPosition = this.$uibPosition.viewportOffset(element); + bodyPosition = this.$uibPosition.offset(this.$body); + + if (isFixedElement) { + angular.extend(position, viewportPosition); + } + + this.viewWindow.top.css({ + position: isFixedElement ? 'fixed' : 'absolute', + top: 0, + left: 0, + width: '100%', + height: Math.floor(position.top) + 'px' + }); + this.viewWindow.bottom.css({ + position: isFixedElement ? 'fixed' : 'absolute', + left: 0, + width: '100%', + height: Math.floor(bodyPosition.top + bodyPosition.height - position.top - position.height) + 'px', + top: (Math.floor(position.top + position.height)) + 'px' + }); + this.viewWindow.left.css({ + position: isFixedElement ? 'fixed' : 'absolute', + left: 0, + top: Math.floor(position.top) + 'px', + width: position.left + 'px', + height: Math.floor(position.height) + 'px' + }); + this.viewWindow.right.css({ + position: isFixedElement ? 'fixed' : 'absolute', + top: Math.floor(position.top) + 'px', + width: (bodyPosition.left + bodyPosition.width - position.left - position.width) + 'px', + height: Math.floor(position.height) + 'px', + left: (position.left + position.width) + 'px' + }); + + this.showBackdrop(); + + if (shouldPreventScrolling) { + this.preventScrolling(); + } + } + + hide() { + this.hideBackdrop(); + this.allowScrolling(); + } + + constructor(private TourConfig: ITourConfig, private $document: ng.IDocumentService, private $uibPosition: angular.ui.bootstrap.IPositionService, private $window: ng.IWindowService) { + var service = this; + var document = ($document[0]) + this.$body = angular.element(document.body); + this.viewWindow = { + top: angular.element(document.createElement('div')), + bottom: angular.element(document.createElement('div')), + left: angular.element(document.createElement('div')), + right: angular.element(document.createElement('div')) + } + + this.createNoScrollingClass(); + + this.createBackdropComponent(this.viewWindow.top); + this.createBackdropComponent(this.viewWindow.bottom); + this.createBackdropComponent(this.viewWindow.left); + this.createBackdropComponent(this.viewWindow.right); + + } + + static factory(TourConfig: ITourConfig, $document: ng.IDocumentService, $uibPosition: angular.ui.bootstrap.IPositionService, $window: ng.IWindowService) { + return new TourBackdrop(TourConfig, $document, $uibPosition, $window); + } + } + + export class TourConfigProvider { + config: ITourConfigProperties = { + placement: 'top', + animation: true, + popupDelay: 1, + closePopupDelay: 0, + enable: true, + appendToBody: false, + popupClass: '', + orphan: false, + backdrop: false, + backdropZIndex: 10000, + scrollOffset: 100, + scrollIntoView: true, + useUiRouter: false, + useHotkeys: false, + + onStart: null, + onEnd: null, + onPause: null, + onResume: null, + onNext: null, + onPrev: null, + onShow: null, + onShown: null, + onHide: null, + onHidden: null + }; + + set(option, value) { + this.config[option] = value; + } + + $get(): ITourConfig { + + return { + get: (option) => { + return this.config[option]; + }, + getAll: () => { + return angular.copy(this.config); + } + }; + } + + constructor($q: ng.IQService) { + angular.forEach(this.config, function (value, key) { + if (key.indexOf('on') === 0 && angular.isFunction(value)) { + this.config[key] = function () { + return $q.resolve(value()); + }; + } + }); + } + } + + export class TourController { + stepList: Array + currentStep: ITourStep + resumeWhenFound: (step: ITourStep) => void; + tourStatus: number; + options: ITourConfigProperties; + initialized: boolean; + emit: (string, any?) => any; + + statuses = { + OFF: 0, + ON: 1, + PAUSED: 2 + } + + constructor(private $timeout: ng.ITimeoutService, private $q: ng.IQService, private $filter: ng.IFilterService, TourConfig: ITourConfig, private uiTourBackdrop: TourBackdrop, private uiTourService: uiTourService, private EventEmitter, private hotkeys) { + this.tourStatus = this.statuses.OFF; + this.options = TourConfig.getAll(); + EventEmitter.mixin(this); + } + + /** + * Closer to $evalAsync, just resolves a promise + * after the next digest cycle + * + * @returns {Promise} + */ + digest() { + return this.$q((resolve) => { + this.$timeout(resolve); + }); + } + + /** + * return current step or null + * @returns {step} + */ + getCurrentStep() { + return this.currentStep; + } + + /** + * set the current step (doesnt do anything else) + * @param {step} step Current step + */ + setCurrentStep(step) { + this.currentStep = step; + } + + /** + * gets a step relative to current step + * + * @param {number} offset Positive integer to search right, negative to search left + * @returns {step} + */ + getStepByOffset(offset) { + if (!this.getCurrentStep()) { + return null; + } + return this.stepList[this.stepList.indexOf(this.getCurrentStep()) + offset]; + } + + /** + * retrieves a step (if it exists in the step list) by index, ID, or identity + * Note: I realize ID is short for identity, but ID is really the step name here + * + * @param {string | number | step} stepOrStepIdOrIndex Step to retrieve + * @returns {step} + */ + getStep(stepOrStepIdOrIndex) { + //index + if (angular.isNumber(stepOrStepIdOrIndex)) { + return this.stepList[stepOrStepIdOrIndex]; + } + + //ID string + if (angular.isString(stepOrStepIdOrIndex)) { + return this.stepList.filter((step) => step.stepId === stepOrStepIdOrIndex)[0]; + } + + //object + if (angular.isObject(stepOrStepIdOrIndex)) { + //step identity + if (~this.stepList.indexOf(stepOrStepIdOrIndex)) { + return stepOrStepIdOrIndex; + } + + //step copy + if (stepOrStepIdOrIndex.stepId) { + return this.stepList.filter((step) => step.stepId === stepOrStepIdOrIndex.stepId)[0]; + } + } + + return null; + } + + /** + * return next step or null + * @returns {step} + */ + getNextStep() { + return this.getStepByOffset(+1); + } + + /** + * return previous step or null + * @returns {step} + */ + getPrevStep() { + return this.getStepByOffset(-1); + } + + /** + * is there a next step + * + * @returns {boolean} + */ + isNext() { + return !!(this.getNextStep() || this.getCurrentStep().nextPath); + } + + /** + * is there a previous step + * + * @returns {boolean} + */ + isPrev() { + return !!(this.getPrevStep() || this.getCurrentStep().prevPath); + } + + /** + * Used by showStep and hideStep to trigger popover events + * + * @param step + * @param eventName + * @returns {*} + */ + dispatchEvent(step, eventName) { + return this.$q((resolve) => { + step.element[0].dispatchEvent(new CustomEvent(eventName)); + resolve(); + }); + } + + /** + * A safe way to invoke a possibly null event handler + * + * @param handler + * @returns {*} + */ + handleEvent(handler) { + return (handler || this.$q.resolve)(); + } + + /** + * Configures hot keys for controlling the tour with the keyboard + */ + setHotKeys() { + this.hotkeys.add({ + combo: 'esc', + description: 'End tour', + callback: () => { + this.end(); + } + }); + + this.hotkeys.add({ + combo: 'right', + description: 'Go to next step', + callback: () => { + if (this.isNext()) { + this.next(); + } + } + }); + + this.hotkeys.add({ + combo: 'left', + description: 'Go to previous step', + callback: () => { + if (this.isPrev()) { + this.prev(); + } + } + }); + } + + /** + * Turns off hot keys for when the tour isn't running + */ + unsetHotKeys() { + this.hotkeys.del('esc'); + this.hotkeys.del('right'); + this.hotkeys.del('left'); + } + + //---------------- Protected API ------------------- + /** + * Adds a step to the tour in order + * + * @param {object} step + */ + addStep(step: ITourStep) { + if (~this.stepList.indexOf(step)) { + return; + } + this.stepList.push(step); + this.stepList = this.$filter('orderBy')(this.stepList, 'order'); + this.emit('stepAdded', step); + if (this.resumeWhenFound) { + this.resumeWhenFound(step); + } + } + + /** + * Removes a step from the tour + * + * @param step + */ + removeStep(step: ITourStep) { + this.stepList.splice(this.stepList.indexOf(step), 1); + this.emit('stepRemoved', step); + } + + /** + * if a step's order was changed, replace it in the list + * @param step + */ + reorderStep(step: ITourStep) { + this.removeStep(step); + this.addStep(step); + this.emit('stepsReordered', step); + } + + /** + * Checks to see if a step exists by ID, index, or identity + * + * @protected + * @param {string | number | step} stepOrStepIdOrIndex Step to check + * @returns {boolean} + */ + protected hasStep(stepOrStepIdOrIndex) { + return !!this.getStep(stepOrStepIdOrIndex); + }; + + /** + * show supplied step + * @param step + * @returns {promise} + */ + protected showStep(step: ITourStep) { + if (!step) { + return this.$q.reject('No step.'); + } + + return this.handleEvent(step.config('onShow')).then(() => { + + if (!step.config('backdrop')) { + return; + } + + var delay = step.config('popupDelay'); + return this.$q((resolve) => { + this.$timeout(() => { + this.uiTourBackdrop.createForElement(step.element, step.config('preventScrolling'), step.config('fixed')); + resolve(); + }, delay); + }) + }).then(() => { + + step.element.addClass('ui-tour-active-step'); + return this.dispatchEvent(step, 'uiTourShow'); + + }).then(() => { + + return this.digest(); + + }).then(() => { + + return this.handleEvent(step.config('onShown')); + + }).then(() => { + + this.emit('stepShown', step); + step.isNext = this.isNext(); + step.isPrev = this.isPrev(); + + }); + } + + /** + * hides the supplied step + * @param step + * @returns {promise} + */ + protected hideStep(step: ITourStep) { + if (!step) { + return this.$q.reject('No step.'); + } + + return this.handleEvent(step.config('onHide')).then(() => { + + step.element.removeClass('ui-tour-active-step'); + return this.dispatchEvent(step, 'uiTourHide'); + + }).then(() => { + + return this.digest(); + + }).then(() => { + + return this.handleEvent(step.config('onHidden')); + + }).then(() => { + + this.emit('stepHidden', step); + + }); + } + + /** + * Returns the value for specified option + * + * @protected + * @param {string} option Name of option + * @returns {*} + */ + protected config(option) { + return this.options[option]; + } + + //------------------ end Protected API ------------------ + + + //------------------ Public API ------------------ + + /** + * Tells the tour to pause while ngView loads + * + * @param waitForStep + */ + waitFor(waitForStep) { + this.pause(); + this.resumeWhenFound = (step) => { + if (step.stepId === waitForStep) { + this.currentStep = this.stepList[this.stepList.indexOf(step)]; + this.resume(); + this.resumeWhenFound = null; + } + }; + } + + /** + * pass options from directive + * @param opts + */ + init(opts) { + this.options = angular.extend(this.options, opts); + this.uiTourService._registerTour(self); + this.initialized = true; + this.emit('initialized'); + return this; + } + + /** + * Unregisters with the tour service when tour is destroyed + * + * @protected + */ + + destroy() { + this.uiTourService._unregisterTour(self); + } + + /** + * starts the tour + */ + start() { + return this.startAt(0); + } + + /** + * starts the tour at a specified step, step index, or step ID + * + * @public + */ + startAt(stepOrStepIdOrIndex) { + return this.handleEvent(this.options.onStart).then(() => { + + var step = this.getStep(stepOrStepIdOrIndex); + this.setCurrentStep(step); + this.tourStatus = this.statuses.ON; + this.emit('started', step); + if (this.options.useHotkeys) { + this.setHotKeys(); + } + return this.showStep(this.getCurrentStep()); + + }); + }; + + /** + * ends the tour + */ + end() { + return this.handleEvent(this.options.onEnd).then(() => { + + if (this.getCurrentStep()) { + this.uiTourBackdrop.hide(); + return this.hideStep(this.getCurrentStep()); + } + + }).then(() => { + + this.setCurrentStep(null); + this.emit('ended'); + this.tourStatus = this.statuses.OFF; + + if (this.options.useHotkeys) { + this.unsetHotKeys(); + } + + }); + } + + /** + * pauses the tour + */ + pause() { + return this.handleEvent(this.options.onPause).then(() => { + this.tourStatus = this.statuses.PAUSED; + return this.hideStep(this.getCurrentStep()); + }).then(() => { + this.emit('paused', this.getCurrentStep()); + }); + } + + /** + * resumes a paused tour or starts it + */ + resume() { + return this.handleEvent(this.options.onResume).then(() => { + this.tourStatus = this.statuses.ON; + this.emit('resumed', this.getCurrentStep()); + return this.showStep(this.getCurrentStep()); + }); + } + + /** + * move to next step + * @returns {promise} + */ + next() { + return this.goTo('$next'); + } + + /** + * move to previous step + * @returns {promise} + */ + prev() { + return this.goTo('$prev'); + } + + /** + * Jumps to the provided step, step ID, or step index + * + * @param {step | string | number} goTo Step object, step ID string, or step index to jump to + * @returns {promise} Promise that resolves once the step is shown + */ + goTo(goTo) { + var currentStep = this.getCurrentStep(), + stepToShow = this.getStep(goTo), + actionMap = { + $prev: { + getStep: this.getPrevStep, + preEvent: 'onPrev', + navCheck: 'prevStep' + }, + $next: { + getStep: this.getNextStep, + preEvent: 'onNext', + navCheck: 'nextStep' + } + }; + + if (goTo === '$prev' || goTo === '$next') { + //trigger either onNext or onPrev here + //if next or previous requires a redirect, it will happen here + //the tour will pause here until the next view loads and + //the next/prev step is found + return this.handleEvent(currentStep.config(actionMap[goTo].preEvent)).then(() => { + + return this.hideStep(currentStep); + + }).then(() => { + + //if the next/prev step does not have a backdrop, hide it + if (this.getCurrentStep().config('backdrop') && !actionMap[goTo].getStep().config('backdrop')) { + this.uiTourBackdrop.hide(); + } + + //if a redirect occurred during onNext or onPrev, getCurrentStep() !== currentStep + //this will only be true if no redirect occurred, since the redirect sets current step + if (!currentStep[actionMap[goTo].navCheck] || currentStep[actionMap[goTo].navCheck] !== this.getCurrentStep().stepId) { + this.setCurrentStep(actionMap[goTo].getStep()); + this.emit('stepChanged', this.getCurrentStep()); + } + + }).then(() => { + + if (this.getCurrentStep()) { + return this.showStep(this.getCurrentStep()); + } else { + this.end(); + } + + }); + } + + //if no step found + if (!stepToShow) { + return this.$q.reject('No step.'); + } + + //take action + return this.hideStep(this.getCurrentStep()) + .then(() => { + //if the next/prev step does not have a backdrop, hide it + if (this.getCurrentStep().config('backdrop') && !stepToShow.config('backdrop')) { + this.uiTourBackdrop.hide(); + } + this.setCurrentStep(stepToShow); + this.emit('stepChanged', this.getCurrentStep()); + return this.showStep(stepToShow); + }); + }; + + + /** + * @typedef number TourStatus + */ + + /** + * Returns the current status of the tour + * @returns {TourStatus} + */ + getStatus() { + return this.tourStatus; + } + + status = this.statuses + + //some debugging functions + private _getSteps() { + return this.stepList; + } + private _getStatus() { + return this.tourStatus; + } + private _getCurrentStep = this.getCurrentStep; + private _setCurrentStep = this.setCurrentStep; + } + + export class TourHelper { + $state + + constructor(private $templateCache: ng.ITemplateCacheService, private $http: ng.IHttpService, private $compile: ng.ICompileService, private $location: ng.ILocationService, private TourConfig: ITourConfig, private $q: ng.IQService, private $injector) { + if ($injector.has('$state')) { + this.$state = $injector.get('$state'); + } + } + + /** + * Helper function that calls scope.$apply if a digest is not currently in progress + * Borrowed from: https://coderwall.com/p/ngisma + * + * @param {$rootScope.Scope} scope + * @param {Function} fn + */ + safeApply(scope: ng.IScope, fn: () => any) { + var phase = scope.$$phase; + if (phase === '$apply' || phase === '$digest') { + if (fn && (typeof (fn) === 'function')) { + fn(); + } + } else { + scope.$apply(fn); + } + } + + /** + * Converts a stringified boolean to a JS boolean + * + * @param string + * @returns {*} + */ + stringToBoolean(string) { + if (string === 'true') { + return true; + } else if (string === 'false') { + return false; + } + + return string; + } + + /** + * This will attach the properties native to Angular UI Tooltips. If there is a tour-level value set + * for any of them, this passes that value along to the step + * + * @param {$rootScope.Scope} scope The tour step's scope + * @param {Attributes} attrs The tour step's Attributes + * @param {Object} step Represents the tour step object + * @param {Array} properties The list of Tooltip properties + */ + attachTourConfigProperties(scope, attrs, step, properties) { + angular.forEach(properties, (property) => { + if (!attrs[this.getAttrName(property)] && angular.isDefined(step.config(property))) { + attrs.$set(this.getAttrName(property), String(step.config(property))); + } + }); + }; + + /** + * Helper function that attaches event handlers to options + * + * @param {$rootScope.Scope} scope + * @param {Attributes} attrs + * @param {Object} options represents the tour or step object + * @param {Array} events + * @param {boolean} prefix - used only by the tour directive + */ + attachEventHandlers(scope, attrs, options, events, prefix?) { + + angular.forEach(events, (eventName) => { + var attrName = this.getAttrName(eventName, prefix); + if (attrs[attrName]) { + options[eventName] = () => { + return this.$q((resolve) => { + this.safeApply(scope, () => { + resolve(scope.$eval(attrs[attrName])); + }); + }); + }; + } + }); + + }; + + /** + * Helper function that attaches observers to option attributes + * + * @param {Attributes} attrs + * @param {Object} options represents the tour or step object + * @param {Array} keys attribute names + * @param {boolean} prefix - used only by the tour directive + */ + attachInterpolatedValues(attrs, options, keys, prefix?) { + + angular.forEach(keys, (key) => { + var attrName = this.getAttrName(key, prefix); + if (attrs[attrName]) { + options[key] = this.stringToBoolean(attrs[attrName]); + attrs.$observe(attrName, (newValue) => { + options[key] = this.stringToBoolean(newValue); + }); + } + }); + + }; + + /** + * sets up a redirect when the next or previous step is in a different view + * + * @param step - the current step (not the next or prev one) + * @param ctrl - the tour controller + * @param direction - enum (onPrev, onNext) + * @param path - the url that the next step is on (will use $location.path()) + * @param targetName - the ID of the next or previous step + */ + setRedirect(step, ctrl, direction, path, targetName) { + var oldHandler = step[direction]; + step[direction] = (tour) => { + return this.$q((resolve) => { + if (oldHandler) { + oldHandler(tour); + } + ctrl.waitFor(targetName); + if (step.config('useUiRouter')) { + this.$state.transitionTo(path).then(resolve); + } else { + this.$location.path(path); + resolve(); + } + }); + }; + }; + + /** + * Returns the attribute name for an option depending on the prefix + * + * @param {string} option - name of option + * @param {string} prefix - should only be used by tour directive and set to 'uiTour' + * @returns {string} potentially prefixed name of option, or just name of option + */ + getAttrName(option, prefix?) { + return (prefix || 'tourStep') + option.charAt(0).toUpperCase() + option.substr(1); + }; + static factory($templateCache: ng.ITemplateCacheService, $http: ng.IHttpService, $compile: ng.ICompileService, $location: ng.ILocationService, TourConfig: ITourConfig, $q: ng.IQService, $injector: ng.IInjectStatic) { + return new TourHelper($templateCache, $http, $compile, $location, TourConfig, $q, $injector); + } + } + + export class uiTourService { + private tours: Array + + constructor(private $controller: ng.IControllerService) { } + + /** + * If there is only one tour, returns the tour + */ + getTour() { + return this.tours[0]; + } + + /** + * Look up a specific tour by name + * + * @param {string} name Name of tour + */ + getTourByName(name: string) { + return this.tours.filter((tour) => { + return tour.options.name === name; + })[0]; + } + + /** + * Finds the tour available to a specific element + * + * @param {jqLite | HTMLElement} element Element to use to look up tour + * @returns {*} + */ + getTourByElement(element) { + return angular.element(element).controller('uiTour'); + }; + + /** + * Creates a tour that is not attached to a DOM element (experimental) + * + * @param {string} name Name of the tour (required) + * @param {{}=} config Options to override defaults + */ + createDetachedTour(name: string, config: ITourConfigProperties) { + if (!name) { + throw { + name: 'ParameterMissingError', + message: 'A unique tour name is required for creating a detached tour.' + }; + } + + config = config || {}; + + config.name = name; + return (this.$controller('uiTourController')).init(config); + }; + + /** + * Used by uiTourController to register a tour + * + * @protected + * @param tour + */ + _registerTour(tour) { + this.tours.push(tour); + }; + + /** + * Used by uiTourController to remove a destroyed tour from the registry + * + * @protected + * @param tour + */ + _unregisterTour(tour) { + this.tours.splice(this.tours.indexOf(tour), 1); + }; + + static factory($controller: ng.IControllerService) { + return new uiTourService($controller); + } + } +} + +((app: ng.IModule) => { + 'use strict'; + + app.config(['$uibTooltipProvider', ($uibTooltipProvider: angular.ui.bootstrap.ITooltipProvider) => { + $uibTooltipProvider.setTriggers({ + 'uiTourShow': 'uiTourHide' + }); + }]); + +})(angular.module('bm.uiTour', ['ngSanitize', 'ui.bootstrap', 'smoothScroll', 'ezNg', 'cfp.hotkeys'])); + +(function (app: ng.IModule) { + 'use strict'; + + app.factory('uiTourBackdrop', ['TourConfig', '$document', '$uibPosition', '$window', Tour.TourBackdrop.factory]) + .factory('TourHelpers', ['$templateCache', '$http', '$compile', '$location', 'TourConfig', '$q', '$injector', Tour.TourHelper.factory]) + .factory('uiTourService', ['$controller', Tour.uiTourService.factory]) + .provider('TourConfig', ['$q', Tour.TourConfigProvider]) + .controller('uiTourController', ['$timeout', '$q', '$filter', 'TourConfig', 'uiTourBackdrop', 'uiTourService', 'ezEventEmitter', 'hotkeys', Tour.TourController]) + .directive('uiTour', ['TourHelpers', (TourHelpers: Tour.TourHelper) => { + + return { + restrict: 'EA', + scope: true, + controller: 'uiTourController', + link: (scope: Tour.ITourScope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) => { + + //Pass static options through or use defaults + var tour = { onReady: null }, + events = 'onReady onStart onEnd onShow onShown onHide onHidden onNext onPrev onPause onResume'.split(' '), + properties = 'placement animation popupDelay closePopupDelay trigger enable appendToBody tooltipClass orphan backdrop'.split(' '); + + //Pass interpolated values through + TourHelpers.attachInterpolatedValues(attrs, tour, properties, 'uiTour'); + + //Attach event handlers + TourHelpers.attachEventHandlers(scope, attrs, tour, events, 'uiTour'); + + //If there is an options argument passed, just use that instead + if (attrs[TourHelpers.getAttrName('options')]) { + angular.extend(tour, scope.$eval(attrs[TourHelpers.getAttrName('options')])); + } + + //Initialize tour + scope.tour = ctrl.init(tour); + if (typeof tour.onReady === 'function') { + tour.onReady(); + } + + scope.$on('$destroy', function () { + ctrl.destroy(); + }); + } + }; + + }]) + .directive('tourStep', ['TourConfig', 'TourHelpers', 'uiTourService', '$uibTooltip', '$q', '$sce', (TourConfig: Tour.ITourConfig, TourHelpers: Tour.TourHelper, TourService: Tour.uiTourService, $uibTooltip, $q: ng.IQService, $sce: ng.ISCEService) => { + + var tourStepDef = $uibTooltip('tourStep', 'tourStep', 'uiTourShow', { + popupDelay: 1 //needs to be non-zero for popping up after navigation + }); + + return { + restrict: 'EA', + scope: true, + require: '?^uiTour', + compile: (tElement, tAttrs) => { + + if (!tAttrs.tourStep) { + tAttrs.$set('tourStep', '\'PH\''); //a placeholder so popup will show + } + + var tourStepLinker = tourStepDef.compile(tElement, tAttrs); + + return (scope: Tour.ITourScope, element: ng.IRootElementService, attrs: ng.IAttributes, uiTourCtrl: Tour.TourController) => { + + var ctrl; + + //check if this step belongs to another tour + if (attrs[TourHelpers.getAttrName('belongsTo')]) { + ctrl = TourService.getTourByName(attrs[TourHelpers.getAttrName('belongsTo')]); + } else if (uiTourCtrl) { + ctrl = uiTourCtrl; + } + + if (!ctrl) { + throw { + name: 'DependencyMissingError', + message: 'No tour provided for tour step.' + }; + } + + //Assign required options + var step = { + stepId: (attrs).tourStep, + enabled: true, + config: (option) => { + if (angular.isDefined(step[option])) { + return step[option]; + } + return ctrl.config(option); + } + }, + events = 'onShow onShown onHide onHidden onNext onPrev'.split(' '), + options = 'content title animation placement backdrop orphan popupDelay popupCloseDelay popupClass fixed preventScrolling scrollIntoView nextStep prevStep nextPath prevPath scrollOffset'.split(' '), + tooltipAttrs = 'animation appendToBody placement popupDelay popupCloseDelay'.split(' '), + orderWatch, + enabledWatch; + + //Will add values to pass to $uibTooltip + var configureInheritedProperties = () => { + TourHelpers.attachTourConfigProperties(scope, attrs, step, tooltipAttrs/*, 'tourStep'*/); + tourStepLinker(scope, element, attrs); + } + + //Pass interpolated values through + TourHelpers.attachInterpolatedValues(attrs, step, options); + orderWatch = attrs.$observe(TourHelpers.getAttrName('order'), (order: number) => { + step.order = !isNaN(order * 1) ? order * 1 : 0; + ctrl.reorderStep(step); + }); + enabledWatch = attrs.$observe(TourHelpers.getAttrName('enabled'), function (isEnabled) { + step.enabled = isEnabled !== 'false'; + if (step.enabled) { + ctrl.addStep(step); + } else { + ctrl.removeStep(step); + } + }); + + //Attach event handlers + TourHelpers.attachEventHandlers(scope, attrs, step, events); + + if (attrs[TourHelpers.getAttrName('templateUrl')]) { + step.templateUrl = scope.$eval(attrs[TourHelpers.getAttrName('templateUrl')]); + } + + //If there is an options argument passed, just use that instead + if (attrs[TourHelpers.getAttrName('options')]) { + angular.extend(step, scope.$eval(attrs[TourHelpers.getAttrName('options')])); + } + + //set up redirects + if (step.nextPath) { + step.redirectNext = true; + TourHelpers.setRedirect(step, ctrl, 'onNext', step.nextPath, step.nextStep); + } + if (step.prevPath) { + step.redirectPrev = true; + TourHelpers.setRedirect(step, ctrl, 'onPrev', step.prevPath, step.prevStep); + } + + //on show and on hide + step.show = () => { + element.triggerHandler('uiTourShow'); + return $q((resolve) => { + element[0].dispatchEvent(new CustomEvent('uiTourShow')); + resolve(); + }); + }; + step.hide = () => { + return $q((resolve) => { + element[0].dispatchEvent(new CustomEvent('uiTourHide')); + resolve(); + }); + }; + + //for HTML content + step.trustedContent = $sce.trustAsHtml(step.content); + + //Add step to tour + scope.tourStep = step; + scope.tour = scope.tour || ctrl; + if (ctrl.initialized) { + configureInheritedProperties(); + ctrl.addStep(step); + } else { + ctrl.once('initialized', function () { + configureInheritedProperties(); + ctrl.addStep(step); + }); + } + + Object.defineProperties(step, { + element: { + value: element + } + }); + + //clean up when element is destroyed + scope.$on('$destroy', function () { + ctrl.removeStep(step); + orderWatch(); + enabledWatch(); + }); + }; + } + }; + + }]) + .directive('tourStepPopup', ['TourConfig', 'smoothScroll', 'ezComponentHelpers', (TourConfig: Tour.ITourConfig, smoothScroll, ezComponentHelpers) => { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', uibTitle: '@uibTitle', content: '@', placement: '@', animation: '&', isOpen: '&', originScope: '&' }, + templateUrl: 'tour-step-popup.html', + link: function (scope, element, attrs) { + var step = scope.originScope().tourStep, + ch = ezComponentHelpers.apply(null, arguments), + scrollOffset = step.config('scrollOffset'); + + //UI Bootstrap name changed in 1.3.0 + if (!scope.title && scope.uibTitle) { + scope.title = scope.uibTitle; + } + + //for arrow styles, unfortunately UI Bootstrap uses attributes for styling + attrs.$set('uib-popover-popup', 'uib-popover-popup'); + + element.css({ + zIndex: TourConfig.get('backdropZIndex') + 2, + display: 'block' + }); + + element.addClass(step.config('popupClass')); + + if (step.config('fixed')) { + element.css('position', 'fixed'); + } + + if (step.config('orphan')) { + ch.useStyles( + `:scope { + position: fixed; + top: 50% !important; + left: 50% !important; + margin: 0 !important; + -ms-transform: translateX(-50%) translateY(-50%); + -moz-transform: translateX(-50%) translateY(-50%); + -webkit-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + + .arrow + display: none; + }` + ); + } + + scope.$watch('isOpen', function (isOpen) { + if (isOpen() && !step.config('orphan') && step.config('scrollIntoView')) { + smoothScroll(element[0], { + offset: scrollOffset + }); + } + }); + } + }; + }]) + .run(['$templateCache', function ($templateCache) { + $templateCache.put("tour-step-popup.html", + `
+
+ +
+

+
+
+
+ `); + $templateCache.put("tour-step-template.html", + `
+
+
+
+ + + +
+ +
+
+ `); + }]); +} (angular.module('bm.uiTour'))); + +(function (window) { + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(window); From b768427627ad21eb660c5567446ff96735f74656 Mon Sep 17 00:00:00 2001 From: joecoolish Date: Sat, 11 Jun 2016 22:32:22 -0400 Subject: [PATCH 2/5] Bug fixes --- app/angular-ui-tour.ts | 68 +++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/app/angular-ui-tour.ts b/app/angular-ui-tour.ts index ef4d7fe..be20f7b 100644 --- a/app/angular-ui-tour.ts +++ b/app/angular-ui-tour.ts @@ -257,30 +257,30 @@ module Tour { onHidden: null }; + $get: [string, ($q: ng.IQService) => ITourConfig]; + set(option, value) { this.config[option] = value; } + constructor() { + this.$get = ['$q', ($q) => { + angular.forEach(this.config, function (value, key) { + if (key.indexOf('on') === 0 && angular.isFunction(value)) { + this.config[key] = function () { + return $q.resolve(value()); + }; + } + }); - $get(): ITourConfig { - - return { - get: (option) => { - return this.config[option]; - }, - getAll: () => { - return angular.copy(this.config); - } - }; - } - - constructor($q: ng.IQService) { - angular.forEach(this.config, function (value, key) { - if (key.indexOf('on') === 0 && angular.isFunction(value)) { - this.config[key] = function () { - return $q.resolve(value()); - }; - } - }); + return { + get: (option) => { + return this.config[option]; + }, + getAll: () => { + return angular.copy(this.config); + } + }; + }]; } } @@ -302,6 +302,7 @@ module Tour { constructor(private $timeout: ng.ITimeoutService, private $q: ng.IQService, private $filter: ng.IFilterService, TourConfig: ITourConfig, private uiTourBackdrop: TourBackdrop, private uiTourService: uiTourService, private EventEmitter, private hotkeys) { this.tourStatus = this.statuses.OFF; this.options = TourConfig.getAll(); + this.stepList = []; EventEmitter.mixin(this); } @@ -642,7 +643,7 @@ module Tour { */ init(opts) { this.options = angular.extend(this.options, opts); - this.uiTourService._registerTour(self); + this.uiTourService._registerTour(this); this.initialized = true; this.emit('initialized'); return this; @@ -759,12 +760,12 @@ module Tour { stepToShow = this.getStep(goTo), actionMap = { $prev: { - getStep: this.getPrevStep, + getStep: () => this.getPrevStep(), preEvent: 'onPrev', navCheck: 'prevStep' }, $next: { - getStep: this.getNextStep, + getStep: () => this.getNextStep(), preEvent: 'onNext', navCheck: 'nextStep' } @@ -1001,7 +1002,9 @@ module Tour { export class uiTourService { private tours: Array - constructor(private $controller: ng.IControllerService) { } + constructor(private $controller: ng.IControllerService) { + this.tours = []; + } /** * If there is only one tour, returns the tour @@ -1094,7 +1097,7 @@ module Tour { app.factory('uiTourBackdrop', ['TourConfig', '$document', '$uibPosition', '$window', Tour.TourBackdrop.factory]) .factory('TourHelpers', ['$templateCache', '$http', '$compile', '$location', 'TourConfig', '$q', '$injector', Tour.TourHelper.factory]) .factory('uiTourService', ['$controller', Tour.uiTourService.factory]) - .provider('TourConfig', ['$q', Tour.TourConfigProvider]) + .provider('TourConfig', [Tour.TourConfigProvider]) .controller('uiTourController', ['$timeout', '$q', '$filter', 'TourConfig', 'uiTourBackdrop', 'uiTourService', 'ezEventEmitter', 'hotkeys', Tour.TourController]) .directive('uiTour', ['TourHelpers', (TourHelpers: Tour.TourHelper) => { @@ -1105,9 +1108,13 @@ module Tour { link: (scope: Tour.ITourScope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) => { //Pass static options through or use defaults - var tour = { onReady: null }, + var tour = { + name: attrs.uiTour, + templateUrl: null, + onReady: null + }, events = 'onReady onStart onEnd onShow onShown onHide onHidden onNext onPrev onPause onResume'.split(' '), - properties = 'placement animation popupDelay closePopupDelay trigger enable appendToBody tooltipClass orphan backdrop'.split(' '); + properties = 'placement animation popupDelay closePopupDelay enable appendToBody popupClass orphan backdrop scrollOffset scrollIntoView useUiRouter useHotkeys'.split(' '); //Pass interpolated values through TourHelpers.attachInterpolatedValues(attrs, tour, properties, 'uiTour'); @@ -1115,6 +1122,11 @@ module Tour { //Attach event handlers TourHelpers.attachEventHandlers(scope, attrs, tour, events, 'uiTour'); + //override the template url + if (attrs[TourHelpers.getAttrName('templateUrl', 'uiTour')]) { + tour.templateUrl = scope.$eval(attrs[TourHelpers.getAttrName('templateUrl', 'uiTour')]); + } + //If there is an options argument passed, just use that instead if (attrs[TourHelpers.getAttrName('options')]) { angular.extend(tour, scope.$eval(attrs[TourHelpers.getAttrName('options')])); @@ -1187,7 +1199,7 @@ module Tour { enabledWatch; //Will add values to pass to $uibTooltip - var configureInheritedProperties = () => { + var configureInheritedProperties = () => { TourHelpers.attachTourConfigProperties(scope, attrs, step, tooltipAttrs/*, 'tourStep'*/); tourStepLinker(scope, element, attrs); } From 703cfa4bda150846a81f2ff2deebf11148a60a67 Mon Sep 17 00:00:00 2001 From: joecoolish Date: Sat, 18 Jun 2016 10:58:37 -0400 Subject: [PATCH 3/5] Create README.md --- app/TypeScript/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/TypeScript/README.md diff --git a/app/TypeScript/README.md b/app/TypeScript/README.md new file mode 100644 index 0000000..cc380b7 --- /dev/null +++ b/app/TypeScript/README.md @@ -0,0 +1 @@ +TODO: Add TS instructions From 8bd9a43f17b2c1ff27ded87b76267729aa44c8ea Mon Sep 17 00:00:00 2001 From: joecoolish Date: Sat, 18 Jun 2016 10:59:05 -0400 Subject: [PATCH 4/5] Added decomposed TS --- app/TypeScript/angular-ui-tour-backdrop.ts | 206 +++++++ .../angular-ui-tour-config-provider.ts | 57 ++ app/TypeScript/angular-ui-tour-controller.ts | 570 ++++++++++++++++++ app/TypeScript/angular-ui-tour-helper.ts | 151 +++++ app/TypeScript/angular-ui-tour-interfaces.ts | 81 +++ app/TypeScript/angular-ui-tour-service.ts | 82 +++ app/TypeScript/angular-ui-tour-step-popup.ts | 74 +++ app/TypeScript/angular-ui-tour-step.ts | 226 +++++++ app/TypeScript/angular-ui-tour.ts | 144 +++++ 9 files changed, 1591 insertions(+) create mode 100644 app/TypeScript/angular-ui-tour-backdrop.ts create mode 100644 app/TypeScript/angular-ui-tour-config-provider.ts create mode 100644 app/TypeScript/angular-ui-tour-controller.ts create mode 100644 app/TypeScript/angular-ui-tour-helper.ts create mode 100644 app/TypeScript/angular-ui-tour-interfaces.ts create mode 100644 app/TypeScript/angular-ui-tour-service.ts create mode 100644 app/TypeScript/angular-ui-tour-step-popup.ts create mode 100644 app/TypeScript/angular-ui-tour-step.ts create mode 100644 app/TypeScript/angular-ui-tour.ts diff --git a/app/TypeScript/angular-ui-tour-backdrop.ts b/app/TypeScript/angular-ui-tour-backdrop.ts new file mode 100644 index 0000000..4e7bdc3 --- /dev/null +++ b/app/TypeScript/angular-ui-tour-backdrop.ts @@ -0,0 +1,206 @@ +module Tour { + export class TourBackdrop { + private $body: ng.IRootElementService; + private viewWindow: { top: ng.IRootElementService, bottom: ng.IRootElementService, left: ng.IRootElementService, right: ng.IRootElementService, target: ng.IRootElementService }; + + private preventDefault(e) { + e.preventDefault(); + } + + private preventScrolling() { + this.$body.addClass('no-scrolling'); + this.$body.on('touchmove', this.preventDefault); + } + + private allowScrolling() { + this.$body.removeClass('no-scrolling'); + this.$body.off('touchmove', this.preventDefault); + } + + private createNoScrollingClass() { + var name = '.no-scrolling', + rules = 'height: 100%; overflow: hidden;', + style = document.createElement('style'); + style.type = 'text/css'; + document.getElementsByTagName('head')[0].appendChild(style); + + if (!style.sheet && !(style.sheet).insertRule) { + ((style).styleSheet || style.sheet).addRule(name, rules); + } else { + (style.sheet).insertRule(name + '{' + rules + '}', 0); + } + } + + private createBackdropComponent(backdrop) { + backdrop.addClass('tour-backdrop').addClass('not-shown').css({ + //display: 'none', + zIndex: this.TourConfig.get('backdropZIndex') + }); + this.$body.append(backdrop); + } + + private showBackdrop() { + this.viewWindow.top.removeClass('hidden'); + this.viewWindow.bottom.removeClass('hidden'); + this.viewWindow.left.removeClass('hidden'); + this.viewWindow.right.removeClass('hidden'); + + setTimeout(() => { + this.viewWindow.top.removeClass('not-shown'); + this.viewWindow.bottom.removeClass('not-shown'); + this.viewWindow.left.removeClass('not-shown'); + this.viewWindow.right.removeClass('not-shown'); + }, 33); + } + + private hideBackdrop() { + this.viewWindow.top.addClass('not-shown'); + this.viewWindow.bottom.addClass('not-shown'); + this.viewWindow.left.addClass('not-shown'); + this.viewWindow.right.addClass('not-shown'); + this.hideTarget(); + + setTimeout(() => { + this.viewWindow.top.addClass('hidden'); + this.viewWindow.bottom.addClass('hidden'); + this.viewWindow.left.addClass('hidden'); + this.viewWindow.right.addClass('hidden'); + }, 250); + } + + createForElement(element: ng.IRootElementService, shouldPreventScrolling: boolean, isFixedElement: boolean, padding: IPadding) { + var position, + viewportPosition, + bodyPosition; + + if (shouldPreventScrolling) { + this.preventScrolling(); + } + + position = this.$uibPosition.offset(element); + viewportPosition = this.$uibPosition.viewportOffset(element); + bodyPosition = this.$uibPosition.offset(this.$body); + + if (isFixedElement) { + angular.extend(position, viewportPosition); + } + + padding = this._processPadding(padding); + + var pTop = Math.floor(position.top) - padding.top; + var pLeft = Math.floor(position.left) - padding.left; + var pHeight = Math.floor(position.height) + padding.top + padding.bottom; + var pWidth = Math.floor(position.width) + padding.left + padding.right; + + var bTop = Math.floor(bodyPosition.top); + var bLeft = Math.floor(bodyPosition.left); + var bHeight = Math.floor(bodyPosition.height); + var bWidth = Math.floor(bodyPosition.width); + + this.viewWindow.top.css({ + position: isFixedElement ? 'fixed' : 'absolute', + top: 0, + left: 0, + width: '100%', + height: (pTop) + 'px' + }); + this.viewWindow.bottom.css({ + position: isFixedElement ? 'fixed' : 'absolute', + left: 0, + width: '100%', + height: (bTop + bHeight - pTop - pHeight) + 'px', + top: (pTop + pHeight) + 'px' + }); + this.viewWindow.left.css({ + position: isFixedElement ? 'fixed' : 'absolute', + left: 0, + top: pTop + 'px', + width: (pLeft) + 'px', + height: pHeight + 'px' + }); + this.viewWindow.right.css({ + position: isFixedElement ? 'fixed' : 'absolute', + top: pTop + 'px', + width: (bLeft + bWidth - pLeft - pWidth) + 'px', + height: pHeight + 'px', + left: (pLeft + pWidth) + 'px' + }); + this.viewWindow.target.css({ + position: isFixedElement ? 'fixed' : 'absolute', + top: pTop + 'px', + width: pWidth + 'px', + height: pHeight + 'px', + left: pLeft + 'px' + }); + + this.showBackdrop(); + + if (shouldPreventScrolling) { + this.preventScrolling(); + } + } + + hide() { + this.hideBackdrop(); + this.hideTarget(); + this.allowScrolling(); + } + + hideTarget(removeNotShow = true) { + this.viewWindow.target.addClass('not-shown'); + + if (!removeNotShow) + return; + + setTimeout(() => { + this.viewWindow.target.addClass('hidden'); + }, 250); + } + + showTarget() { + this.viewWindow.target.removeClass('hidden'); + + setTimeout(() => { + this.viewWindow.target.removeClass('not-shown'); + }, 33); + } + + constructor(private TourConfig: ITourConfig, private $document: ng.IDocumentService, private $uibPosition: angular.ui.bootstrap.IPositionService, private $window: ng.IWindowService) { + var service = this; + var document = ($document[0]) + this.$body = angular.element(document.body); + this.viewWindow = { + top: angular.element(document.createElement('div')), + bottom: angular.element(document.createElement('div')), + left: angular.element(document.createElement('div')), + right: angular.element(document.createElement('div')), + target: angular.element(document.createElement('div')) + } + + this.createNoScrollingClass(); + + this.createBackdropComponent(this.viewWindow.top); + this.createBackdropComponent(this.viewWindow.bottom); + this.createBackdropComponent(this.viewWindow.left); + this.createBackdropComponent(this.viewWindow.right); + this.createBackdropComponent(this.viewWindow.target); + + } + + static factory(TourConfig: ITourConfig, $document: ng.IDocumentService, $uibPosition: angular.ui.bootstrap.IPositionService, $window: ng.IWindowService) { + return new TourBackdrop(TourConfig, $document, $uibPosition, $window); + } + + _processPadding(padding: IPadding) { + if (!padding) + padding = { top: 0, left: 0, right: 0, bottom: 0 } + + padding.top = padding.top || 0; + padding.left = padding.left || 0; + padding.right = padding.right || 0; + padding.bottom = padding.bottom || 0; + + return padding; + } + } +} \ No newline at end of file diff --git a/app/TypeScript/angular-ui-tour-config-provider.ts b/app/TypeScript/angular-ui-tour-config-provider.ts new file mode 100644 index 0000000..839a3fc --- /dev/null +++ b/app/TypeScript/angular-ui-tour-config-provider.ts @@ -0,0 +1,57 @@ +module Tour { + export class TourConfigProvider { + config: ITourConfigProperties = { + placement: 'top', + animation: true, + popupDelay: 1, + closePopupDelay: 0, + enable: true, + appendToBody: false, + popupClass: '', + orphan: false, + backdrop: false, + backdropZIndex: 10000, + scrollOffset: 100, + scrollIntoView: true, + useUiRouter: false, + useHotkeys: false, + + onStart: null, + onEnd: null, + onPause: null, + onResume: null, + onNext: null, + onPrev: null, + onShow: null, + onShown: null, + onHide: null, + onHidden: null + }; + + $get: [string, ($q: ng.IQService) => ITourConfig]; + + set(option, value) { + this.config[option] = value; + } + constructor() { + this.$get = ['$q', ($q) => { + angular.forEach(this.config, function (value, key) { + if (key.indexOf('on') === 0 && angular.isFunction(value)) { + this.config[key] = function () { + return $q.resolve(value()); + }; + } + }); + + return { + get: (option) => { + return this.config[option]; + }, + getAll: () => { + return angular.copy(this.config); + } + }; + }]; + } + } +} \ No newline at end of file diff --git a/app/TypeScript/angular-ui-tour-controller.ts b/app/TypeScript/angular-ui-tour-controller.ts new file mode 100644 index 0000000..f1676ca --- /dev/null +++ b/app/TypeScript/angular-ui-tour-controller.ts @@ -0,0 +1,570 @@ +module Tour { + export class TourController { + stepList: Array + currentStep: ITourStep + resumeWhenFound: (step: ITourStep) => void; + tourStatus: number; + options: ITourConfigProperties; + initialized: boolean; + emit: (string, any?) => any; + once: (string, fn: () => void) => any; + + statuses = { + OFF: 0, + ON: 1, + PAUSED: 2 + } + + constructor(private $timeout: ng.ITimeoutService, private $q: ng.IQService, private $filter: ng.IFilterService, TourConfig: ITourConfig, private uiTourBackdrop: TourBackdrop, private uiTourService: uiTourService, private EventEmitter, private hotkeys) { + this.tourStatus = this.statuses.OFF; + this.options = TourConfig.getAll(); + this.stepList = []; + EventEmitter.mixin(this); + } + + /** + * Closer to $evalAsync, just resolves a promise + * after the next digest cycle + * + * @returns {Promise} + */ + digest() { + return this.$q((resolve) => { + this.$timeout(resolve); + }); + } + + /** + * return current step or null + * @returns {step} + */ + getCurrentStep() { + return this.currentStep; + } + + /** + * set the current step (doesnt do anything else) + * @param {step} step Current step + */ + setCurrentStep(step) { + this.currentStep = step; + } + + /** + * gets a step relative to current step + * + * @param {number} offset Positive integer to search right, negative to search left + * @returns {step} + */ + getStepByOffset(offset) { + if (!this.getCurrentStep()) { + return null; + } + return this.stepList[this.stepList.indexOf(this.getCurrentStep()) + offset]; + } + + /** + * retrieves a step (if it exists in the step list) by index, ID, or identity + * Note: I realize ID is short for identity, but ID is really the step name here + * + * @param {string | number | step} stepOrStepIdOrIndex Step to retrieve + * @returns {step} + */ + getStep(stepOrStepIdOrIndex) { + //index + if (angular.isNumber(stepOrStepIdOrIndex)) { + return this.stepList[stepOrStepIdOrIndex]; + } + + //ID string + if (angular.isString(stepOrStepIdOrIndex)) { + return this.stepList.filter((step) => step.stepId === stepOrStepIdOrIndex)[0]; + } + + //object + if (angular.isObject(stepOrStepIdOrIndex)) { + //step identity + if (~this.stepList.indexOf(stepOrStepIdOrIndex)) { + return stepOrStepIdOrIndex; + } + + //step copy + if (stepOrStepIdOrIndex.stepId) { + return this.stepList.filter((step) => step.stepId === stepOrStepIdOrIndex.stepId)[0]; + } + } + + return null; + } + + /** + * return next step or null + * @returns {step} + */ + getNextStep() { + return this.getStepByOffset(+1); + } + + /** + * return previous step or null + * @returns {step} + */ + getPrevStep() { + return this.getStepByOffset(-1); + } + + /** + * is there a next step + * + * @returns {boolean} + */ + isNext() { + return !!(this.getNextStep() || this.getCurrentStep().nextPath); + } + + /** + * is there a previous step + * + * @returns {boolean} + */ + isPrev() { + return !!(this.getPrevStep() || this.getCurrentStep().prevPath); + } + + /** + * Used by showStep and hideStep to trigger popover events + * + * @param step + * @param eventName + * @returns {*} + */ + dispatchEvent(step, eventName) { + return this.$q((resolve) => { + step.element[0].dispatchEvent(new CustomEvent(eventName)); + resolve(); + }); + } + + /** + * A safe way to invoke a possibly null event handler + * + * @param handler + * @returns {*} + */ + handleEvent(handler) { + return (handler || this.$q.resolve)(); + } + + /** + * Configures hot keys for controlling the tour with the keyboard + */ + setHotKeys() { + this.hotkeys.add({ + combo: 'esc', + description: 'End tour', + callback: () => { + this.end(); + } + }); + + this.hotkeys.add({ + combo: 'right', + description: 'Go to next step', + callback: () => { + if (this.isNext()) { + this.next(); + } + } + }); + + this.hotkeys.add({ + combo: 'left', + description: 'Go to previous step', + callback: () => { + if (this.isPrev()) { + this.prev(); + } + } + }); + } + + /** + * Turns off hot keys for when the tour isn't running + */ + unsetHotKeys() { + this.hotkeys.del('esc'); + this.hotkeys.del('right'); + this.hotkeys.del('left'); + } + + //---------------- Protected API ------------------- + /** + * Adds a step to the tour in order + * + * @param {object} step + */ + addStep(step: ITourStep) { + if (~this.stepList.indexOf(step)) { + return; + } + this.stepList.push(step); + this.stepList = this.$filter('orderBy')(this.stepList, 'order'); + this.emit('stepAdded', step); + if (this.resumeWhenFound) { + this.resumeWhenFound(step); + } + } + + /** + * Removes a step from the tour + * + * @param step + */ + removeStep(step: ITourStep) { + this.stepList.splice(this.stepList.indexOf(step), 1); + this.emit('stepRemoved', step); + } + + /** + * if a step's order was changed, replace it in the list + * @param step + */ + reorderStep(step: ITourStep) { + this.stepList = this.$filter('orderBy')(this.stepList, 'order'); + this.emit('stepsReordered', step); + } + + /** + * Checks to see if a step exists by ID, index, or identity + * + * @protected + * @param {string | number | step} stepOrStepIdOrIndex Step to check + * @returns {boolean} + */ + protected hasStep(stepOrStepIdOrIndex) { + return !!this.getStep(stepOrStepIdOrIndex); + }; + + /** + * show supplied step + * @param step + * @returns {promise} + */ + protected showStep(step: ITourStep) { + if (!step) { + return this.$q.reject('No step.'); + } + + return this.handleEvent(step.config('onShow')).then(() => { + + if (!step.config('backdrop')) { + return; + } + + var delay = step.config('popupDelay'); + if (step.config('animation') && delay < 100) + delay = 250 + + return this.$q((resolve) => { + this.$timeout(() => { + this.uiTourBackdrop.createForElement(step.element, step.config('preventScrolling'), step.config('fixed'), step.config('backdropPadding')); + this.uiTourBackdrop.hideTarget(false); + resolve(); + }, delay); + }) + }).then(() => { + + step.element.addClass('ui-tour-active-step'); + return this.dispatchEvent(step, 'uiTourShow'); + + }).then(() => { + + return this.digest(); + + }).then(() => { + + return this.handleEvent(step.config('onShown')); + + }).then(() => { + + this.emit('stepShown', step); + step.isNext = this.isNext(); + step.isPrev = this.isPrev(); + + }); + } + + /** + * hides the supplied step + * @param step + * @returns {promise} + */ + protected hideStep(step: ITourStep) { + if (!step) { + return this.$q.reject('No step.'); + } + + return this.handleEvent(step.config('onHide')).then(() => { + + step.element.removeClass('ui-tour-active-step'); + return this.dispatchEvent(step, 'uiTourHide'); + + }).then(() => { + + return this.digest(); + + }).then(() => { + + return this.handleEvent(step.config('onHidden')); + + }).then(() => { + + this.emit('stepHidden', step); + + }); + } + + //------------------ end Protected API ------------------ + + + //------------------ Public API ------------------ + + /** + * Returns the value for specified option + * + * @protected + * @param {string} option Name of option + * @returns {*} + */ + public config(option) { + return this.options[option]; + } + + /** + * Tells the tour to pause while ngView loads + * + * @param waitForStep + */ + waitFor(waitForStep) { + this.pause(); + this.resumeWhenFound = (step) => { + if (step.stepId === waitForStep) { + this.currentStep = this.stepList[this.stepList.indexOf(step)]; + this.resume(); + this.resumeWhenFound = null; + } + }; + } + + /** + * pass options from directive + * @param opts + */ + init(opts) { + this.options = angular.extend(this.options, opts); + this.uiTourService._registerTour(this); + this.initialized = true; + this.emit('initialized'); + return this; + } + + /** + * Unregisters with the tour service when tour is destroyed + * + * @protected + */ + + destroy() { + this.uiTourService._unregisterTour(self); + } + + /** + * starts the tour + */ + start() { + return this.startAt(0); + } + + /** + * starts the tour at a specified step, step index, or step ID + * + * @public + */ + startAt(stepOrStepIdOrIndex) { + return this.handleEvent(this.options.onStart).then(() => { + + var step = this.getStep(stepOrStepIdOrIndex); + this.setCurrentStep(step); + this.tourStatus = this.statuses.ON; + this.emit('started', step); + if (this.options.useHotkeys) { + this.setHotKeys(); + } + return this.showStep(this.getCurrentStep()); + + }); + }; + + /** + * ends the tour + */ + end() { + return this.handleEvent(this.options.onEnd).then(() => { + var step = this.getCurrentStep(); + if (step) { + this.uiTourBackdrop.hide(); + return this.hideStep(step); + } + + }).then(() => { + + this.setCurrentStep(null); + this.emit('ended'); + this.tourStatus = this.statuses.OFF; + + if (this.options.useHotkeys) { + this.unsetHotKeys(); + } + + }); + } + + /** + * pauses the tour + */ + pause() { + return this.handleEvent(this.options.onPause).then(() => { + this.tourStatus = this.statuses.PAUSED; + return this.hideStep(this.getCurrentStep()); + }).then(() => { + this.emit('paused', this.getCurrentStep()); + }); + } + + /** + * resumes a paused tour or starts it + */ + resume() { + return this.handleEvent(this.options.onResume).then(() => { + this.tourStatus = this.statuses.ON; + this.emit('resumed', this.getCurrentStep()); + return this.showStep(this.getCurrentStep()); + }); + } + + /** + * move to next step + * @returns {promise} + */ + next() { + return this.goTo('$next'); + } + + /** + * move to previous step + * @returns {promise} + */ + prev() { + return this.goTo('$prev'); + } + + /** + * Jumps to the provided step, step ID, or step index + * + * @param {step | string | number} goTo Step object, step ID string, or step index to jump to + * @returns {promise} Promise that resolves once the step is shown + */ + goTo(goTo) { + var currentStep = this.getCurrentStep(), + stepToShow = this.getStep(goTo), + actionMap = { + $prev: { + getStep: () => this.getPrevStep(), + preEvent: 'onPrev', + navCheck: 'prevStep' + }, + $next: { + getStep: () => this.getNextStep(), + preEvent: 'onNext', + navCheck: 'nextStep' + } + }; + + if (goTo === '$prev' || goTo === '$next') { + //trigger either onNext or onPrev here + //if next or previous requires a redirect, it will happen here + //the tour will pause here until the next view loads and + //the next/prev step is found + return this.handleEvent(currentStep.config(actionMap[goTo].preEvent)).then(() => { + currentStep.backdrop && this.uiTourBackdrop.showTarget(); + return this.hideStep(currentStep); + + }).then(() => { + + //if the next/prev step does not have a backdrop, hide it + if (this.getCurrentStep().config('backdrop') && !actionMap[goTo].getStep().config('backdrop')) { + this.uiTourBackdrop.hide(); + } + + //if a redirect occurred during onNext or onPrev, getCurrentStep() !== currentStep + //this will only be true if no redirect occurred, since the redirect sets current step + if (!currentStep[actionMap[goTo].navCheck] || currentStep[actionMap[goTo].navCheck] !== this.getCurrentStep().stepId) { + this.setCurrentStep(actionMap[goTo].getStep()); + this.emit('stepChanged', this.getCurrentStep()); + } + + }).then(() => { + + if (this.getCurrentStep()) { + return this.showStep(this.getCurrentStep()); + } else { + this.end(); + } + + }); + } + + //if no step found + if (!stepToShow) { + return this.$q.reject('No step.'); + } + + //take action + return this.hideStep(this.getCurrentStep()) + .then(() => { + //if the next/prev step does not have a backdrop, hide it + if (this.getCurrentStep().config('backdrop') && !stepToShow.config('backdrop')) { + this.uiTourBackdrop.hide(); + } + this.setCurrentStep(stepToShow); + this.emit('stepChanged', this.getCurrentStep()); + return this.showStep(stepToShow); + }); + }; + + + /** + * @typedef number TourStatus + */ + + /** + * Returns the current status of the tour + * @returns {TourStatus} + */ + getStatus() { + return this.tourStatus; + } + + status = this.statuses + + //some debugging functions + private _getSteps() { + return this.stepList; + } + private _getStatus() { + return this.tourStatus; + } + private _getCurrentStep = this.getCurrentStep; + private _setCurrentStep = this.setCurrentStep; + } +} \ No newline at end of file diff --git a/app/TypeScript/angular-ui-tour-helper.ts b/app/TypeScript/angular-ui-tour-helper.ts new file mode 100644 index 0000000..f2551b2 --- /dev/null +++ b/app/TypeScript/angular-ui-tour-helper.ts @@ -0,0 +1,151 @@ +module Tour { + export class TourHelper { + $state + + constructor(private $templateCache: ng.ITemplateCacheService, private $http: ng.IHttpService, private $compile: ng.ICompileService, private $location: ng.ILocationService, private TourConfig: ITourConfig, private $q: ng.IQService, private $injector) { + if ($injector.has('$state')) { + this.$state = $injector.get('$state'); + } + } + + /** + * Helper function that calls scope.$apply if a digest is not currently in progress + * Borrowed from: https://coderwall.com/p/ngisma + * + * @param {$rootScope.Scope} scope + * @param {Function} fn + */ + safeApply(scope: ng.IScope, fn: () => any) { + var phase = scope.$$phase; + if (phase === '$apply' || phase === '$digest') { + if (fn && (typeof (fn) === 'function')) { + fn(); + } + } else { + scope.$apply(fn); + } + } + + /** + * Converts a stringified boolean to a JS boolean + * + * @param string + * @returns {*} + */ + stringToBoolean(string) { + if (string === 'true') { + return true; + } else if (string === 'false') { + return false; + } + + return string; + } + + /** + * This will attach the properties native to Angular UI Tooltips. If there is a tour-level value set + * for any of them, this passes that value along to the step + * + * @param {$rootScope.Scope} scope The tour step's scope + * @param {Attributes} attrs The tour step's Attributes + * @param {Object} step Represents the tour step object + * @param {Array} properties The list of Tooltip properties + */ + attachTourConfigProperties(scope, attrs, step, properties) { + angular.forEach(properties, (property) => { + if (!attrs[this.getAttrName(property)] && angular.isDefined(step.config(property))) { + attrs.$set(this.getAttrName(property), String(step.config(property))); + } + }); + }; + + /** + * Helper function that attaches event handlers to options + * + * @param {$rootScope.Scope} scope + * @param {Attributes} attrs + * @param {Object} options represents the tour or step object + * @param {Array} events + * @param {boolean} prefix - used only by the tour directive + */ + attachEventHandlers(scope, attrs, options, events, prefix?) { + + angular.forEach(events, (eventName) => { + var attrName = this.getAttrName(eventName, prefix); + if (attrs[attrName]) { + options[eventName] = () => { + return this.$q((resolve) => { + this.safeApply(scope, () => { + resolve(scope.$eval(attrs[attrName])); + }); + }); + }; + } + }); + + }; + + /** + * Helper function that attaches observers to option attributes + * + * @param {Attributes} attrs + * @param {Object} options represents the tour or step object + * @param {Array} keys attribute names + * @param {boolean} prefix - used only by the tour directive + */ + attachInterpolatedValues(attrs, options, keys, prefix?) { + + angular.forEach(keys, (key) => { + var attrName = this.getAttrName(key, prefix); + if (attrs[attrName]) { + options[key] = this.stringToBoolean(attrs[attrName]); + attrs.$observe(attrName, (newValue) => { + options[key] = this.stringToBoolean(newValue); + }); + } + }); + + }; + + /** + * sets up a redirect when the next or previous step is in a different view + * + * @param step - the current step (not the next or prev one) + * @param ctrl - the tour controller + * @param direction - enum (onPrev, onNext) + * @param path - the url that the next step is on (will use $location.path()) + * @param targetName - the ID of the next or previous step + */ + setRedirect(step, ctrl, direction, path, targetName) { + var oldHandler = step[direction]; + step[direction] = (tour) => { + return this.$q((resolve) => { + if (oldHandler) { + oldHandler(tour); + } + ctrl.waitFor(targetName); + if (step.config('useUiRouter')) { + this.$state.transitionTo(path).then(resolve); + } else { + this.$location.path(path); + resolve(); + } + }); + }; + }; + + /** + * Returns the attribute name for an option depending on the prefix + * + * @param {string} option - name of option + * @param {string} prefix - should only be used by tour directive and set to 'uiTour' + * @returns {string} potentially prefixed name of option, or just name of option + */ + getAttrName(option, prefix?) { + return (prefix || 'tourStep') + option.charAt(0).toUpperCase() + option.substr(1); + }; + static factory($templateCache: ng.ITemplateCacheService, $http: ng.IHttpService, $compile: ng.ICompileService, $location: ng.ILocationService, TourConfig: ITourConfig, $q: ng.IQService, $injector: ng.IInjectStatic) { + return new TourHelper($templateCache, $http, $compile, $location, TourConfig, $q, $injector); + } + } +} \ No newline at end of file diff --git a/app/TypeScript/angular-ui-tour-interfaces.ts b/app/TypeScript/angular-ui-tour-interfaces.ts new file mode 100644 index 0000000..f3800f3 --- /dev/null +++ b/app/TypeScript/angular-ui-tour-interfaces.ts @@ -0,0 +1,81 @@ +module Tour { + export interface IPadding { + top: number; + left: number; + bottom: number; + right: number; + } + + export interface ITourScope extends ng.IScope { + tour: TourController; + tourStep: ITourStep; + + originScope: () => ITourScope; + isOpen: () => boolean; + } + + export interface ITourStep { + nextPath?; + prevPath?; + backdrop?; + stepId?; + trustedContent?; + content?: string; + order?: number; + templateUrl?: string; + element?: ng.IRootElementService; + enabled?: boolean; + preventScrolling?: boolean; + fixed?: boolean; + isNext?: boolean; + isPrev?: boolean; + redirectNext?: boolean; + redirectPrev?: boolean; + nextStep?: ITourStep; + prevStep?: ITourStep; + show?: () => PromiseLike; + hide?: () => PromiseLike; + onNext?: () => PromiseLike; + onPrev?: () => PromiseLike; + onShow?: () => PromiseLike; + onHide?: () => PromiseLike; + onShown?: () => PromiseLike; + onHidden?: () => PromiseLike; + config?: (string) => any; + } + + export interface ITourConfig { + get: (option: string) => any; + getAll: () => ITourConfigProperties; + } + + export interface ITourConfigProperties { + name?: string; + placement: string; + animation: boolean; + popupDelay: number; + closePopupDelay: number; + enable: boolean; + appendToBody: boolean; + popupClass: string; + orphan: boolean; + backdrop: boolean; + backdropZIndex: number; + scrollOffset: number; + scrollIntoView: boolean; + useUiRouter: boolean; + useHotkeys: boolean; + + onStart: (any?) => any; + onEnd: (any?) => any; + onPause: (any?) => any; + onResume: (any?) => any; + onNext: (any?) => any; + onPrev: (any?) => any; + onShow: (any?) => any; + onShown: (any?) => any; + onHide: (any?) => any; + onHidden: (any?) => any; + + } +} \ No newline at end of file diff --git a/app/TypeScript/angular-ui-tour-service.ts b/app/TypeScript/angular-ui-tour-service.ts new file mode 100644 index 0000000..dee20d5 --- /dev/null +++ b/app/TypeScript/angular-ui-tour-service.ts @@ -0,0 +1,82 @@ + +module Tour { + export class uiTourService { + private tours: Array + + constructor(private $controller: ng.IControllerService) { + this.tours = []; + } + + /** + * If there is only one tour, returns the tour + */ + getTour() { + return this.tours[0]; + } + + /** + * Look up a specific tour by name + * + * @param {string} name Name of tour + */ + getTourByName(name: string) { + return this.tours.filter((tour) => { + return tour.options.name === name; + })[0]; + } + + /** + * Finds the tour available to a specific element + * + * @param {jqLite | HTMLElement} element Element to use to look up tour + * @returns {*} + */ + getTourByElement(element) { + return angular.element(element).controller('uiTour'); + }; + + /** + * Creates a tour that is not attached to a DOM element (experimental) + * + * @param {string} name Name of the tour (required) + * @param {{}=} config Options to override defaults + */ + createDetachedTour(name: string, config: ITourConfigProperties) { + if (!name) { + throw { + name: 'ParameterMissingError', + message: 'A unique tour name is required for creating a detached tour.' + }; + } + + config = config || {}; + + config.name = name; + return (this.$controller('uiTourController')).init(config); + }; + + /** + * Used by uiTourController to register a tour + * + * @protected + * @param tour + */ + _registerTour(tour) { + this.tours.push(tour); + }; + + /** + * Used by uiTourController to remove a destroyed tour from the registry + * + * @protected + * @param tour + */ + _unregisterTour(tour) { + this.tours.splice(this.tours.indexOf(tour), 1); + }; + + static factory($controller: ng.IControllerService) { + return new uiTourService($controller); + } + } +} diff --git a/app/TypeScript/angular-ui-tour-step-popup.ts b/app/TypeScript/angular-ui-tour-step-popup.ts new file mode 100644 index 0000000..71a97d4 --- /dev/null +++ b/app/TypeScript/angular-ui-tour-step-popup.ts @@ -0,0 +1,74 @@ +module Tour { + export class TourStepPopupDirective { + public restrict = 'EA'; + public replace = true; + public scope = { title: '@', uibTitle: '@uibTitle', content: '@', placement: '@', animation: '&', isOpen: '&', originScope: '&' }; + public templateUrl = 'tour-step-popup.html'; + public link: (scope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) => void; + + constructor(TourConfig: Tour.ITourConfig, smoothScroll, ezComponentHelpers) { + TourStepPopupDirective.prototype.link = function (scope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) { + var step = scope.originScope().tourStep, + ch = ezComponentHelpers.apply(null, arguments), + scrollOffset = step.config('scrollOffset'); + + //UI Bootstrap name changed in 1.3.0 + if (!scope.title && scope.uibTitle) { + scope.title = scope.uibTitle; + } + + //for arrow styles, unfortunately UI Bootstrap uses attributes for styling + attrs.$set('uib-popover-popup', 'uib-popover-popup'); + + element.css({ + zIndex: TourConfig.get('backdropZIndex') + 2, + display: 'block' + }); + + element.addClass(step.config('popupClass')); + + if (step.config('fixed')) { + element.css('position', 'fixed'); + } + + if (step.config('orphan')) { + ch.useStyles( + `:scope { + position: fixed; + top: 50% !important; + left: 50% !important; + margin: 0 !important; + -ms-transform: translateX(-50%) translateY(-50%); + -moz-transform: translateX(-50%) translateY(-50%); + -webkit-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + + .arrow + display: none; + }` + ); + } + + scope.$watch('isOpen', (isOpen: () => boolean) => { + if (isOpen() && !step.config('orphan') && step.config('scrollIntoView')) { + smoothScroll(element[0], { + offset: scrollOffset + }); + } + }); + }; + } + + public static Factory() { + + var directive = (TourConfig: Tour.ITourConfig, smoothScroll, ezComponentHelpers) => { + return new TourStepPopupDirective(TourConfig, smoothScroll, ezComponentHelpers); + }; + + directive['$inject'] = ['TourConfig', 'smoothScroll', 'ezComponentHelpers']; + + return directive; + } + } +} \ No newline at end of file diff --git a/app/TypeScript/angular-ui-tour-step.ts b/app/TypeScript/angular-ui-tour-step.ts new file mode 100644 index 0000000..fcbf5f4 --- /dev/null +++ b/app/TypeScript/angular-ui-tour-step.ts @@ -0,0 +1,226 @@ +module Tour { + export class TourStepCompiler { + private ctrl: TourController + private step: ITourStep + private events: Array + private options: Array + private tooltipAttrs: Array + private orderWatch + private enabledWatch + + public TourHelpers: TourHelper + public TourService: uiTourService + public $q: ng.IQService + public $sce: ng.ISCEService + public tourStepLinker + + constructor(private scope: Tour.ITourScope, private element: ng.IRootElementService, private attrs: ng.IAttributes, private uiTourCtrl: Tour.TourController) { + this.initializeVariables(); + if (attrs[this.TourHelpers.getAttrName('if')] !== undefined && attrs[this.TourHelpers.getAttrName('if')] === "false") { + return; + } + + this.addWatches(); + this.finalizeStep(); + this.addStepToScope(); + Object.defineProperties(this.step, { + element: { + value: element + } + }); + + //clean up when element is destroyed + scope.$on('$destroy', function () { + this.ctrl.removeStep(this.step); + this.orderWatch(); + this.enabledWatch(); + }); + } + + public static Factory() { + + var compiler = (scope: Tour.ITourScope, element: ng.IRootElementService, attrs: ng.IAttributes, uiTourCtrl: Tour.TourController) => { + return new TourStepCompiler(scope, element, attrs, uiTourCtrl); + }; + + compiler['$inject'] = ['scope', 'element', 'attrs', 'uiTourCtrl']; + + return compiler; + } + + private initializeVariables() { + this.ctrl = this.getCtrl(this.attrs, this.uiTourCtrl); + this.step = { + show: () => { + this.element.triggerHandler('uiTourShow'); + return this.$q((resolve) => { + this.element[0].dispatchEvent(new CustomEvent('uiTourShow')); + resolve(); + }); + }, + hide: () => { + return this.$q((resolve) => { + this.element[0].dispatchEvent(new CustomEvent('uiTourHide')); + resolve(); + }); + }, + stepId: (this.attrs).tourStep, + enabled: true, + config: (option) => { + if (angular.isDefined(this.step[option])) { + return this.step[option]; + } + return this.ctrl.config(option); + } + }; + this.events = 'onShow onShown onHide onHidden onNext onPrev'.split(' '); + this.options = 'content title animation placement backdrop orphan popupDelay popupCloseDelay popupClass fixed preventScrolling scrollIntoView nextStep prevStep nextPath prevPath scrollOffset'.split(' '); + this.tooltipAttrs = 'animation appendToBody placement popupDelay popupCloseDelay'.split(' '); + } + + private addWatches() { + this.TourHelpers.attachInterpolatedValues(this.attrs, this.step, this.options); + this.orderWatch = this.attrs.$observe(this.TourHelpers.getAttrName('order'), (order: number) => { + this.step.order = !isNaN(order * 1) ? order * 1 : 0; + this.ctrl.reorderStep(this.step); + }); + this.enabledWatch = this.attrs.$observe(this.TourHelpers.getAttrName('enabled'), function (isEnabled) { + this.step.enabled = isEnabled !== 'false'; + if (this.step.enabled) { + this.ctrl.addStep(this.step); + } else { + this.ctrl.removeStep(this.step); + } + }); + } + + private finalizeStep() { + //Attach event handlers + this.TourHelpers.attachEventHandlers(this.scope, this.attrs, this.step, this.events); + + if (this.attrs[this.TourHelpers.getAttrName('templateUrl')]) { + this.step.templateUrl = this.scope.$eval(this.attrs[this.TourHelpers.getAttrName('templateUrl')]); + } + + //If there is an options argument passed, just use that instead + if (this.attrs[this.TourHelpers.getAttrName('options')]) { + angular.extend(this.step, this.scope.$eval(this.attrs[this.TourHelpers.getAttrName('options')])); + } + + //set up redirects + if (this.step.nextPath) { + this.step.redirectNext = true; + this.TourHelpers.setRedirect(this.step, this.ctrl, 'onNext', this.step.nextPath, this.step.nextStep); + } + if (this.step.prevPath) { + this.step.redirectPrev = true; + this.TourHelpers.setRedirect(this.step, this.ctrl, 'onPrev', this.step.prevPath, this.step.prevStep); + } + + //for HTML content + this.step.trustedContent = this.$sce.trustAsHtml(this.step.content); + } + + private addStepToScope() { + + //Add step to tour + this.scope.tourStep = this.step; + this.scope.tour = this.scope.tour || this.ctrl; + if (this.ctrl.initialized) { + this.configureInheritedProperties(); + this.ctrl.addStep(this.step); + } else { + this.ctrl.once('initialized', () => { + this.configureInheritedProperties(); + this.ctrl.addStep(this.step); + }); + } + } + + private configureInheritedProperties() { + this.TourHelpers.attachTourConfigProperties(this.scope, this.attrs, this.step, this.tooltipAttrs/*, 'tourStep'*/); + this.tourStepLinker(this.scope, this.element, this.attrs); + } + + private getCtrl(attrs, uiTourCtrl) { + var ctrl: Tour.TourController; + + if (attrs[this.TourHelpers.getAttrName('belongsTo')]) { + ctrl = this.TourService.getTourByName(attrs[this.TourHelpers.getAttrName('belongsTo')]); + } else if (uiTourCtrl) { + ctrl = uiTourCtrl; + } + + if (!ctrl) { + throw { + name: 'DependencyMissingError', + message: 'No tour provided for tour step.' + }; + } + + return ctrl; + } + } + + export class TourStepDirective { + private tourStepDef; + + public restrict: string; + public scope: boolean; + public require: string; + public compile: (element: ng.IAugmentedJQuery, attr: ng.IAttributes) => (...any) => TourStepCompiler; + + constructor(private TourConfig: Tour.ITourConfig, private TourHelpers: Tour.TourHelper, private TourService: Tour.uiTourService, private $uibTooltip, private $q: ng.IQService, private $sce: ng.ISCEService) { + this.restrict = 'EA'; + this.scope = true; + this.require = '?^uiTour'; + this.tourStepDef = $uibTooltip('tourStep', 'tourStep', 'uiTourShow', { + popupDelay: 1 //needs to be non-zero for popping up after navigation + }); + + Tour.TourStepDirective.prototype.compile = (tElement, tAttrs) => { + TourStepCompiler.prototype.$q = $q; + TourStepCompiler.prototype.$sce = $sce; + TourStepCompiler.prototype.TourHelpers = TourHelpers; + TourStepCompiler.prototype.TourService = TourService; + TourStepCompiler.prototype.tourStepLinker = this.tourStepDef.compile(tElement, tAttrs); + + if (!(tAttrs).tourStep) { + tAttrs.$set('tourStep', '\'PH\''); //a placeholder so popup will show + } + + return TourStepCompiler.Factory(); + } + } + + public static Factory() { + + var directive = (TourConfig: Tour.ITourConfig, TourHelpers: Tour.TourHelper, TourService: Tour.uiTourService, $uibTooltip, $q: ng.IQService, $sce: ng.ISCEService) => { + return new TourStepDirective(TourConfig, TourHelpers, TourService, $uibTooltip, $q, $sce); + }; + + directive['$inject'] = ['TourConfig', 'TourHelpers', 'uiTourService', '$uibTooltip', '$q', '$sce']; + + return directive; + } + + private getCtrl(attrs, uiTourCtrl) { + var ctrl: Tour.TourController; + + if (attrs[this.TourHelpers.getAttrName('belongsTo')]) { + ctrl = this.TourService.getTourByName(attrs[this.TourHelpers.getAttrName('belongsTo')]); + } else if (uiTourCtrl) { + ctrl = uiTourCtrl; + } + + if (!ctrl) { + throw { + name: 'DependencyMissingError', + message: 'No tour provided for tour step.' + }; + } + + return ctrl; + } + } +} diff --git a/app/TypeScript/angular-ui-tour.ts b/app/TypeScript/angular-ui-tour.ts new file mode 100644 index 0000000..6195ac6 --- /dev/null +++ b/app/TypeScript/angular-ui-tour.ts @@ -0,0 +1,144 @@ +/// +/// +/// +/* global Tour: false */ + +module Tour { + export class TourDirective { + public restrict = 'EA'; + public scope = true; + public controller = 'uiTourController'; + public link: (scope: Tour.ITourScope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) => void; + + constructor(private TourHelpers: Tour.TourHelper) { + TourDirective.prototype.link = (scope: Tour.ITourScope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) => { + //Pass static options through or use defaults + var tour = { + name: attrs.uiTour, + templateUrl: null, + onReady: null + } + + this.interpolateValues(scope, attrs, tour); + this.finalizeTour(scope, attrs, tour); + this.finalizeScope(scope, tour, ctrl); + }; + } + + public static Factory() { + + var directive = (TourHelpers: Tour.TourHelper) => { + return new TourDirective(TourHelpers); + }; + + directive['$inject'] = ['TourHelpers']; + + return directive; + } + + private interpolateValues(scope, attrs, tour) { + var events = 'onReady onStart onEnd onShow onShown onHide onHidden onNext onPrev onPause onResume'.split(' '), + properties = 'placement animation popupDelay closePopupDelay enable appendToBody popupClass orphan backdrop scrollOffset scrollIntoView useUiRouter useHotkeys'.split(' '); + + //Pass interpolated values through + this.TourHelpers.attachInterpolatedValues(attrs, tour, properties, 'uiTour'); + + //Attach event handlers + this.TourHelpers.attachEventHandlers(scope, attrs, tour, events, 'uiTour'); + + } + + private finalizeTour(scope, attrs, tour) { + //override the template url + if (attrs[this.TourHelpers.getAttrName('templateUrl', 'uiTour')]) { + tour.templateUrl = scope.$eval(attrs[this.TourHelpers.getAttrName('templateUrl', 'uiTour')]); + } + + //If there is an options argument passed, just use that instead + if (attrs[this.TourHelpers.getAttrName('options')]) { + angular.extend(tour, scope.$eval(attrs[this.TourHelpers.getAttrName('options')])); + } + } + + private finalizeScope(scope, tour, ctrl) { + //Initialize tour + scope.tour = ctrl.init(tour); + if (typeof tour.onReady === 'function') { + tour.onReady(); + } + + scope.$on('$destroy', function () { + ctrl.destroy(); + }); + } + } +} + +((app: ng.IModule) => { + 'use strict'; + + app.config(['$uibTooltipProvider', ($uibTooltipProvider: angular.ui.bootstrap.ITooltipProvider) => { + $uibTooltipProvider.setTriggers({ + 'uiTourShow': 'uiTourHide' + }); + }]); + +})(angular.module('bm.uiTour', ['ngSanitize', 'ui.bootstrap', 'smoothScroll', 'ezNg', 'cfp.hotkeys'])); + +(function (app: ng.IModule) { + 'use strict'; + + app.factory('uiTourBackdrop', ['TourConfig', '$document', '$uibPosition', '$window', Tour.TourBackdrop.factory]) + .factory('TourHelpers', ['$templateCache', '$http', '$compile', '$location', 'TourConfig', '$q', '$injector', Tour.TourHelper.factory]) + .factory('uiTourService', ['$controller', Tour.uiTourService.factory]) + .provider('TourConfig', [Tour.TourConfigProvider]) + .controller('uiTourController', ['$timeout', '$q', '$filter', 'TourConfig', 'uiTourBackdrop', 'uiTourService', 'ezEventEmitter', 'hotkeys', Tour.TourController]) + .directive('uiTour', ['TourHelpers', Tour.TourDirective.Factory()]) + .directive('tourStepPopup', ['TourConfig', 'smoothScroll', 'ezComponentHelpers', Tour.TourStepPopupDirective.Factory()]) + .directive('tourStep', ['TourConfig', 'TourHelpers', 'uiTourService', '$uibTooltip', '$q', '$sce', Tour.TourStepDirective.Factory()]) + .run(['$templateCache', function ($templateCache) { + $templateCache.put("tour-step-popup.html", + `
+
+ +
+

+
+
+
+ `); + $templateCache.put("tour-step-template.html", + `
+
+
+
+ + + +
+ +
+
+ `); + }]); +} (angular.module('bm.uiTour'))); + +(function (window) { + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(window); From 833816635010cd37a06175e501244383d8f4d20c Mon Sep 17 00:00:00 2001 From: joecoolish Date: Sat, 18 Jun 2016 10:59:36 -0400 Subject: [PATCH 5/5] Decomposed, don't need --- app/angular-ui-tour.ts | 1396 ---------------------------------------- 1 file changed, 1396 deletions(-) delete mode 100644 app/angular-ui-tour.ts diff --git a/app/angular-ui-tour.ts b/app/angular-ui-tour.ts deleted file mode 100644 index be20f7b..0000000 --- a/app/angular-ui-tour.ts +++ /dev/null @@ -1,1396 +0,0 @@ -/// -/// -/// -/* global Tour: false */ - -module Tour { - export interface ITourScope extends ng.IScope { - tour: TourController; - tourStep: ITourStep; - - originScope: () => ITourScope; - isOpen: () => boolean; - } - - export interface ITourStep { - nextPath?; - prevPath?; - backdrop?; - stepId?; - trustedContent?; - content?: string; - order?: number; - templateUrl?: string; - element?: ng.IRootElementService; - enabled?: boolean; - preventScrolling?: boolean; - fixed?: boolean; - isNext?: boolean; - isPrev?: boolean; - redirectNext?: boolean; - redirectPrev?: boolean; - nextStep?: ITourStep; - prevStep?: ITourStep; - show?: () => PromiseLike; - hide?: () => PromiseLike; - onNext?: () => PromiseLike; - onPrev?: () => PromiseLike; - onShow?: () => PromiseLike; - onHide?: () => PromiseLike; - onShown?: () => PromiseLike; - onHidden?: () => PromiseLike; - config?: (string) => any; - } - - export interface ITourConfig { - get: (option: string) => any; - getAll: () => ITourConfigProperties; - } - - export interface ITourConfigProperties { - name?: string; - placement: string; - animation: boolean; - popupDelay: number; - closePopupDelay: number; - enable: boolean; - appendToBody: boolean; - popupClass: string; - orphan: boolean; - backdrop: boolean; - backdropZIndex: number; - scrollOffset: number; - scrollIntoView: boolean; - useUiRouter: boolean; - useHotkeys: boolean; - - onStart: (any?) => any; - onEnd: (any?) => any; - onPause: (any?) => any; - onResume: (any?) => any; - onNext: (any?) => any; - onPrev: (any?) => any; - onShow: (any?) => any; - onShown: (any?) => any; - onHide: (any?) => any; - onHidden: (any?) => any; - - } - - export class TourBackdrop { - private $body: ng.IRootElementService; - private viewWindow: { top: ng.IRootElementService, bottom: ng.IRootElementService, left: ng.IRootElementService, right: ng.IRootElementService }; - - private preventDefault(e) { - e.preventDefault(); - } - - private preventScrolling() { - this.$body.addClass('no-scrolling'); - this.$body.on('touchmove', this.preventDefault); - } - - private allowScrolling() { - this.$body.removeClass('no-scrolling'); - this.$body.off('touchmove', this.preventDefault); - } - - private createNoScrollingClass() { - var name = '.no-scrolling', - rules = 'height: 100%; overflow: hidden;', - style = document.createElement('style'); - style.type = 'text/css'; - document.getElementsByTagName('head')[0].appendChild(style); - - if (!style.sheet && !(style.sheet).insertRule) { - ((style).styleSheet || style.sheet).addRule(name, rules); - } else { - (style.sheet).insertRule(name + '{' + rules + '}', 0); - } - } - - private createBackdropComponent(backdrop) { - backdrop.addClass('tour-backdrop').addClass('not-shown').css({ - //display: 'none', - zIndex: this.TourConfig.get('backdropZIndex') - }); - this.$body.append(backdrop); - } - - private showBackdrop() { - this.viewWindow.top.removeClass('hidden'); - this.viewWindow.bottom.removeClass('hidden'); - this.viewWindow.left.removeClass('hidden'); - this.viewWindow.right.removeClass('hidden'); - - setTimeout(() => { - this.viewWindow.top.removeClass('not-shown'); - this.viewWindow.bottom.removeClass('not-shown'); - this.viewWindow.left.removeClass('not-shown'); - this.viewWindow.right.removeClass('not-shown'); - }, 33); - } - - private hideBackdrop() { - this.viewWindow.top.addClass('not-shown'); - this.viewWindow.bottom.addClass('not-shown'); - this.viewWindow.left.addClass('not-shown'); - this.viewWindow.right.addClass('not-shown'); - - setTimeout(() => { - this.viewWindow.top.addClass('hidden'); - this.viewWindow.bottom.addClass('hidden'); - this.viewWindow.left.addClass('hidden'); - this.viewWindow.right.addClass('hidden'); - }, 250); - } - - createForElement(element: ng.IRootElementService, shouldPreventScrolling: boolean, isFixedElement: boolean) { - var position, - viewportPosition, - bodyPosition; - - if (shouldPreventScrolling) { - this.preventScrolling(); - } - - position = this.$uibPosition.offset(element); - viewportPosition = this.$uibPosition.viewportOffset(element); - bodyPosition = this.$uibPosition.offset(this.$body); - - if (isFixedElement) { - angular.extend(position, viewportPosition); - } - - this.viewWindow.top.css({ - position: isFixedElement ? 'fixed' : 'absolute', - top: 0, - left: 0, - width: '100%', - height: Math.floor(position.top) + 'px' - }); - this.viewWindow.bottom.css({ - position: isFixedElement ? 'fixed' : 'absolute', - left: 0, - width: '100%', - height: Math.floor(bodyPosition.top + bodyPosition.height - position.top - position.height) + 'px', - top: (Math.floor(position.top + position.height)) + 'px' - }); - this.viewWindow.left.css({ - position: isFixedElement ? 'fixed' : 'absolute', - left: 0, - top: Math.floor(position.top) + 'px', - width: position.left + 'px', - height: Math.floor(position.height) + 'px' - }); - this.viewWindow.right.css({ - position: isFixedElement ? 'fixed' : 'absolute', - top: Math.floor(position.top) + 'px', - width: (bodyPosition.left + bodyPosition.width - position.left - position.width) + 'px', - height: Math.floor(position.height) + 'px', - left: (position.left + position.width) + 'px' - }); - - this.showBackdrop(); - - if (shouldPreventScrolling) { - this.preventScrolling(); - } - } - - hide() { - this.hideBackdrop(); - this.allowScrolling(); - } - - constructor(private TourConfig: ITourConfig, private $document: ng.IDocumentService, private $uibPosition: angular.ui.bootstrap.IPositionService, private $window: ng.IWindowService) { - var service = this; - var document = ($document[0]) - this.$body = angular.element(document.body); - this.viewWindow = { - top: angular.element(document.createElement('div')), - bottom: angular.element(document.createElement('div')), - left: angular.element(document.createElement('div')), - right: angular.element(document.createElement('div')) - } - - this.createNoScrollingClass(); - - this.createBackdropComponent(this.viewWindow.top); - this.createBackdropComponent(this.viewWindow.bottom); - this.createBackdropComponent(this.viewWindow.left); - this.createBackdropComponent(this.viewWindow.right); - - } - - static factory(TourConfig: ITourConfig, $document: ng.IDocumentService, $uibPosition: angular.ui.bootstrap.IPositionService, $window: ng.IWindowService) { - return new TourBackdrop(TourConfig, $document, $uibPosition, $window); - } - } - - export class TourConfigProvider { - config: ITourConfigProperties = { - placement: 'top', - animation: true, - popupDelay: 1, - closePopupDelay: 0, - enable: true, - appendToBody: false, - popupClass: '', - orphan: false, - backdrop: false, - backdropZIndex: 10000, - scrollOffset: 100, - scrollIntoView: true, - useUiRouter: false, - useHotkeys: false, - - onStart: null, - onEnd: null, - onPause: null, - onResume: null, - onNext: null, - onPrev: null, - onShow: null, - onShown: null, - onHide: null, - onHidden: null - }; - - $get: [string, ($q: ng.IQService) => ITourConfig]; - - set(option, value) { - this.config[option] = value; - } - constructor() { - this.$get = ['$q', ($q) => { - angular.forEach(this.config, function (value, key) { - if (key.indexOf('on') === 0 && angular.isFunction(value)) { - this.config[key] = function () { - return $q.resolve(value()); - }; - } - }); - - return { - get: (option) => { - return this.config[option]; - }, - getAll: () => { - return angular.copy(this.config); - } - }; - }]; - } - } - - export class TourController { - stepList: Array - currentStep: ITourStep - resumeWhenFound: (step: ITourStep) => void; - tourStatus: number; - options: ITourConfigProperties; - initialized: boolean; - emit: (string, any?) => any; - - statuses = { - OFF: 0, - ON: 1, - PAUSED: 2 - } - - constructor(private $timeout: ng.ITimeoutService, private $q: ng.IQService, private $filter: ng.IFilterService, TourConfig: ITourConfig, private uiTourBackdrop: TourBackdrop, private uiTourService: uiTourService, private EventEmitter, private hotkeys) { - this.tourStatus = this.statuses.OFF; - this.options = TourConfig.getAll(); - this.stepList = []; - EventEmitter.mixin(this); - } - - /** - * Closer to $evalAsync, just resolves a promise - * after the next digest cycle - * - * @returns {Promise} - */ - digest() { - return this.$q((resolve) => { - this.$timeout(resolve); - }); - } - - /** - * return current step or null - * @returns {step} - */ - getCurrentStep() { - return this.currentStep; - } - - /** - * set the current step (doesnt do anything else) - * @param {step} step Current step - */ - setCurrentStep(step) { - this.currentStep = step; - } - - /** - * gets a step relative to current step - * - * @param {number} offset Positive integer to search right, negative to search left - * @returns {step} - */ - getStepByOffset(offset) { - if (!this.getCurrentStep()) { - return null; - } - return this.stepList[this.stepList.indexOf(this.getCurrentStep()) + offset]; - } - - /** - * retrieves a step (if it exists in the step list) by index, ID, or identity - * Note: I realize ID is short for identity, but ID is really the step name here - * - * @param {string | number | step} stepOrStepIdOrIndex Step to retrieve - * @returns {step} - */ - getStep(stepOrStepIdOrIndex) { - //index - if (angular.isNumber(stepOrStepIdOrIndex)) { - return this.stepList[stepOrStepIdOrIndex]; - } - - //ID string - if (angular.isString(stepOrStepIdOrIndex)) { - return this.stepList.filter((step) => step.stepId === stepOrStepIdOrIndex)[0]; - } - - //object - if (angular.isObject(stepOrStepIdOrIndex)) { - //step identity - if (~this.stepList.indexOf(stepOrStepIdOrIndex)) { - return stepOrStepIdOrIndex; - } - - //step copy - if (stepOrStepIdOrIndex.stepId) { - return this.stepList.filter((step) => step.stepId === stepOrStepIdOrIndex.stepId)[0]; - } - } - - return null; - } - - /** - * return next step or null - * @returns {step} - */ - getNextStep() { - return this.getStepByOffset(+1); - } - - /** - * return previous step or null - * @returns {step} - */ - getPrevStep() { - return this.getStepByOffset(-1); - } - - /** - * is there a next step - * - * @returns {boolean} - */ - isNext() { - return !!(this.getNextStep() || this.getCurrentStep().nextPath); - } - - /** - * is there a previous step - * - * @returns {boolean} - */ - isPrev() { - return !!(this.getPrevStep() || this.getCurrentStep().prevPath); - } - - /** - * Used by showStep and hideStep to trigger popover events - * - * @param step - * @param eventName - * @returns {*} - */ - dispatchEvent(step, eventName) { - return this.$q((resolve) => { - step.element[0].dispatchEvent(new CustomEvent(eventName)); - resolve(); - }); - } - - /** - * A safe way to invoke a possibly null event handler - * - * @param handler - * @returns {*} - */ - handleEvent(handler) { - return (handler || this.$q.resolve)(); - } - - /** - * Configures hot keys for controlling the tour with the keyboard - */ - setHotKeys() { - this.hotkeys.add({ - combo: 'esc', - description: 'End tour', - callback: () => { - this.end(); - } - }); - - this.hotkeys.add({ - combo: 'right', - description: 'Go to next step', - callback: () => { - if (this.isNext()) { - this.next(); - } - } - }); - - this.hotkeys.add({ - combo: 'left', - description: 'Go to previous step', - callback: () => { - if (this.isPrev()) { - this.prev(); - } - } - }); - } - - /** - * Turns off hot keys for when the tour isn't running - */ - unsetHotKeys() { - this.hotkeys.del('esc'); - this.hotkeys.del('right'); - this.hotkeys.del('left'); - } - - //---------------- Protected API ------------------- - /** - * Adds a step to the tour in order - * - * @param {object} step - */ - addStep(step: ITourStep) { - if (~this.stepList.indexOf(step)) { - return; - } - this.stepList.push(step); - this.stepList = this.$filter('orderBy')(this.stepList, 'order'); - this.emit('stepAdded', step); - if (this.resumeWhenFound) { - this.resumeWhenFound(step); - } - } - - /** - * Removes a step from the tour - * - * @param step - */ - removeStep(step: ITourStep) { - this.stepList.splice(this.stepList.indexOf(step), 1); - this.emit('stepRemoved', step); - } - - /** - * if a step's order was changed, replace it in the list - * @param step - */ - reorderStep(step: ITourStep) { - this.removeStep(step); - this.addStep(step); - this.emit('stepsReordered', step); - } - - /** - * Checks to see if a step exists by ID, index, or identity - * - * @protected - * @param {string | number | step} stepOrStepIdOrIndex Step to check - * @returns {boolean} - */ - protected hasStep(stepOrStepIdOrIndex) { - return !!this.getStep(stepOrStepIdOrIndex); - }; - - /** - * show supplied step - * @param step - * @returns {promise} - */ - protected showStep(step: ITourStep) { - if (!step) { - return this.$q.reject('No step.'); - } - - return this.handleEvent(step.config('onShow')).then(() => { - - if (!step.config('backdrop')) { - return; - } - - var delay = step.config('popupDelay'); - return this.$q((resolve) => { - this.$timeout(() => { - this.uiTourBackdrop.createForElement(step.element, step.config('preventScrolling'), step.config('fixed')); - resolve(); - }, delay); - }) - }).then(() => { - - step.element.addClass('ui-tour-active-step'); - return this.dispatchEvent(step, 'uiTourShow'); - - }).then(() => { - - return this.digest(); - - }).then(() => { - - return this.handleEvent(step.config('onShown')); - - }).then(() => { - - this.emit('stepShown', step); - step.isNext = this.isNext(); - step.isPrev = this.isPrev(); - - }); - } - - /** - * hides the supplied step - * @param step - * @returns {promise} - */ - protected hideStep(step: ITourStep) { - if (!step) { - return this.$q.reject('No step.'); - } - - return this.handleEvent(step.config('onHide')).then(() => { - - step.element.removeClass('ui-tour-active-step'); - return this.dispatchEvent(step, 'uiTourHide'); - - }).then(() => { - - return this.digest(); - - }).then(() => { - - return this.handleEvent(step.config('onHidden')); - - }).then(() => { - - this.emit('stepHidden', step); - - }); - } - - /** - * Returns the value for specified option - * - * @protected - * @param {string} option Name of option - * @returns {*} - */ - protected config(option) { - return this.options[option]; - } - - //------------------ end Protected API ------------------ - - - //------------------ Public API ------------------ - - /** - * Tells the tour to pause while ngView loads - * - * @param waitForStep - */ - waitFor(waitForStep) { - this.pause(); - this.resumeWhenFound = (step) => { - if (step.stepId === waitForStep) { - this.currentStep = this.stepList[this.stepList.indexOf(step)]; - this.resume(); - this.resumeWhenFound = null; - } - }; - } - - /** - * pass options from directive - * @param opts - */ - init(opts) { - this.options = angular.extend(this.options, opts); - this.uiTourService._registerTour(this); - this.initialized = true; - this.emit('initialized'); - return this; - } - - /** - * Unregisters with the tour service when tour is destroyed - * - * @protected - */ - - destroy() { - this.uiTourService._unregisterTour(self); - } - - /** - * starts the tour - */ - start() { - return this.startAt(0); - } - - /** - * starts the tour at a specified step, step index, or step ID - * - * @public - */ - startAt(stepOrStepIdOrIndex) { - return this.handleEvent(this.options.onStart).then(() => { - - var step = this.getStep(stepOrStepIdOrIndex); - this.setCurrentStep(step); - this.tourStatus = this.statuses.ON; - this.emit('started', step); - if (this.options.useHotkeys) { - this.setHotKeys(); - } - return this.showStep(this.getCurrentStep()); - - }); - }; - - /** - * ends the tour - */ - end() { - return this.handleEvent(this.options.onEnd).then(() => { - - if (this.getCurrentStep()) { - this.uiTourBackdrop.hide(); - return this.hideStep(this.getCurrentStep()); - } - - }).then(() => { - - this.setCurrentStep(null); - this.emit('ended'); - this.tourStatus = this.statuses.OFF; - - if (this.options.useHotkeys) { - this.unsetHotKeys(); - } - - }); - } - - /** - * pauses the tour - */ - pause() { - return this.handleEvent(this.options.onPause).then(() => { - this.tourStatus = this.statuses.PAUSED; - return this.hideStep(this.getCurrentStep()); - }).then(() => { - this.emit('paused', this.getCurrentStep()); - }); - } - - /** - * resumes a paused tour or starts it - */ - resume() { - return this.handleEvent(this.options.onResume).then(() => { - this.tourStatus = this.statuses.ON; - this.emit('resumed', this.getCurrentStep()); - return this.showStep(this.getCurrentStep()); - }); - } - - /** - * move to next step - * @returns {promise} - */ - next() { - return this.goTo('$next'); - } - - /** - * move to previous step - * @returns {promise} - */ - prev() { - return this.goTo('$prev'); - } - - /** - * Jumps to the provided step, step ID, or step index - * - * @param {step | string | number} goTo Step object, step ID string, or step index to jump to - * @returns {promise} Promise that resolves once the step is shown - */ - goTo(goTo) { - var currentStep = this.getCurrentStep(), - stepToShow = this.getStep(goTo), - actionMap = { - $prev: { - getStep: () => this.getPrevStep(), - preEvent: 'onPrev', - navCheck: 'prevStep' - }, - $next: { - getStep: () => this.getNextStep(), - preEvent: 'onNext', - navCheck: 'nextStep' - } - }; - - if (goTo === '$prev' || goTo === '$next') { - //trigger either onNext or onPrev here - //if next or previous requires a redirect, it will happen here - //the tour will pause here until the next view loads and - //the next/prev step is found - return this.handleEvent(currentStep.config(actionMap[goTo].preEvent)).then(() => { - - return this.hideStep(currentStep); - - }).then(() => { - - //if the next/prev step does not have a backdrop, hide it - if (this.getCurrentStep().config('backdrop') && !actionMap[goTo].getStep().config('backdrop')) { - this.uiTourBackdrop.hide(); - } - - //if a redirect occurred during onNext or onPrev, getCurrentStep() !== currentStep - //this will only be true if no redirect occurred, since the redirect sets current step - if (!currentStep[actionMap[goTo].navCheck] || currentStep[actionMap[goTo].navCheck] !== this.getCurrentStep().stepId) { - this.setCurrentStep(actionMap[goTo].getStep()); - this.emit('stepChanged', this.getCurrentStep()); - } - - }).then(() => { - - if (this.getCurrentStep()) { - return this.showStep(this.getCurrentStep()); - } else { - this.end(); - } - - }); - } - - //if no step found - if (!stepToShow) { - return this.$q.reject('No step.'); - } - - //take action - return this.hideStep(this.getCurrentStep()) - .then(() => { - //if the next/prev step does not have a backdrop, hide it - if (this.getCurrentStep().config('backdrop') && !stepToShow.config('backdrop')) { - this.uiTourBackdrop.hide(); - } - this.setCurrentStep(stepToShow); - this.emit('stepChanged', this.getCurrentStep()); - return this.showStep(stepToShow); - }); - }; - - - /** - * @typedef number TourStatus - */ - - /** - * Returns the current status of the tour - * @returns {TourStatus} - */ - getStatus() { - return this.tourStatus; - } - - status = this.statuses - - //some debugging functions - private _getSteps() { - return this.stepList; - } - private _getStatus() { - return this.tourStatus; - } - private _getCurrentStep = this.getCurrentStep; - private _setCurrentStep = this.setCurrentStep; - } - - export class TourHelper { - $state - - constructor(private $templateCache: ng.ITemplateCacheService, private $http: ng.IHttpService, private $compile: ng.ICompileService, private $location: ng.ILocationService, private TourConfig: ITourConfig, private $q: ng.IQService, private $injector) { - if ($injector.has('$state')) { - this.$state = $injector.get('$state'); - } - } - - /** - * Helper function that calls scope.$apply if a digest is not currently in progress - * Borrowed from: https://coderwall.com/p/ngisma - * - * @param {$rootScope.Scope} scope - * @param {Function} fn - */ - safeApply(scope: ng.IScope, fn: () => any) { - var phase = scope.$$phase; - if (phase === '$apply' || phase === '$digest') { - if (fn && (typeof (fn) === 'function')) { - fn(); - } - } else { - scope.$apply(fn); - } - } - - /** - * Converts a stringified boolean to a JS boolean - * - * @param string - * @returns {*} - */ - stringToBoolean(string) { - if (string === 'true') { - return true; - } else if (string === 'false') { - return false; - } - - return string; - } - - /** - * This will attach the properties native to Angular UI Tooltips. If there is a tour-level value set - * for any of them, this passes that value along to the step - * - * @param {$rootScope.Scope} scope The tour step's scope - * @param {Attributes} attrs The tour step's Attributes - * @param {Object} step Represents the tour step object - * @param {Array} properties The list of Tooltip properties - */ - attachTourConfigProperties(scope, attrs, step, properties) { - angular.forEach(properties, (property) => { - if (!attrs[this.getAttrName(property)] && angular.isDefined(step.config(property))) { - attrs.$set(this.getAttrName(property), String(step.config(property))); - } - }); - }; - - /** - * Helper function that attaches event handlers to options - * - * @param {$rootScope.Scope} scope - * @param {Attributes} attrs - * @param {Object} options represents the tour or step object - * @param {Array} events - * @param {boolean} prefix - used only by the tour directive - */ - attachEventHandlers(scope, attrs, options, events, prefix?) { - - angular.forEach(events, (eventName) => { - var attrName = this.getAttrName(eventName, prefix); - if (attrs[attrName]) { - options[eventName] = () => { - return this.$q((resolve) => { - this.safeApply(scope, () => { - resolve(scope.$eval(attrs[attrName])); - }); - }); - }; - } - }); - - }; - - /** - * Helper function that attaches observers to option attributes - * - * @param {Attributes} attrs - * @param {Object} options represents the tour or step object - * @param {Array} keys attribute names - * @param {boolean} prefix - used only by the tour directive - */ - attachInterpolatedValues(attrs, options, keys, prefix?) { - - angular.forEach(keys, (key) => { - var attrName = this.getAttrName(key, prefix); - if (attrs[attrName]) { - options[key] = this.stringToBoolean(attrs[attrName]); - attrs.$observe(attrName, (newValue) => { - options[key] = this.stringToBoolean(newValue); - }); - } - }); - - }; - - /** - * sets up a redirect when the next or previous step is in a different view - * - * @param step - the current step (not the next or prev one) - * @param ctrl - the tour controller - * @param direction - enum (onPrev, onNext) - * @param path - the url that the next step is on (will use $location.path()) - * @param targetName - the ID of the next or previous step - */ - setRedirect(step, ctrl, direction, path, targetName) { - var oldHandler = step[direction]; - step[direction] = (tour) => { - return this.$q((resolve) => { - if (oldHandler) { - oldHandler(tour); - } - ctrl.waitFor(targetName); - if (step.config('useUiRouter')) { - this.$state.transitionTo(path).then(resolve); - } else { - this.$location.path(path); - resolve(); - } - }); - }; - }; - - /** - * Returns the attribute name for an option depending on the prefix - * - * @param {string} option - name of option - * @param {string} prefix - should only be used by tour directive and set to 'uiTour' - * @returns {string} potentially prefixed name of option, or just name of option - */ - getAttrName(option, prefix?) { - return (prefix || 'tourStep') + option.charAt(0).toUpperCase() + option.substr(1); - }; - static factory($templateCache: ng.ITemplateCacheService, $http: ng.IHttpService, $compile: ng.ICompileService, $location: ng.ILocationService, TourConfig: ITourConfig, $q: ng.IQService, $injector: ng.IInjectStatic) { - return new TourHelper($templateCache, $http, $compile, $location, TourConfig, $q, $injector); - } - } - - export class uiTourService { - private tours: Array - - constructor(private $controller: ng.IControllerService) { - this.tours = []; - } - - /** - * If there is only one tour, returns the tour - */ - getTour() { - return this.tours[0]; - } - - /** - * Look up a specific tour by name - * - * @param {string} name Name of tour - */ - getTourByName(name: string) { - return this.tours.filter((tour) => { - return tour.options.name === name; - })[0]; - } - - /** - * Finds the tour available to a specific element - * - * @param {jqLite | HTMLElement} element Element to use to look up tour - * @returns {*} - */ - getTourByElement(element) { - return angular.element(element).controller('uiTour'); - }; - - /** - * Creates a tour that is not attached to a DOM element (experimental) - * - * @param {string} name Name of the tour (required) - * @param {{}=} config Options to override defaults - */ - createDetachedTour(name: string, config: ITourConfigProperties) { - if (!name) { - throw { - name: 'ParameterMissingError', - message: 'A unique tour name is required for creating a detached tour.' - }; - } - - config = config || {}; - - config.name = name; - return (this.$controller('uiTourController')).init(config); - }; - - /** - * Used by uiTourController to register a tour - * - * @protected - * @param tour - */ - _registerTour(tour) { - this.tours.push(tour); - }; - - /** - * Used by uiTourController to remove a destroyed tour from the registry - * - * @protected - * @param tour - */ - _unregisterTour(tour) { - this.tours.splice(this.tours.indexOf(tour), 1); - }; - - static factory($controller: ng.IControllerService) { - return new uiTourService($controller); - } - } -} - -((app: ng.IModule) => { - 'use strict'; - - app.config(['$uibTooltipProvider', ($uibTooltipProvider: angular.ui.bootstrap.ITooltipProvider) => { - $uibTooltipProvider.setTriggers({ - 'uiTourShow': 'uiTourHide' - }); - }]); - -})(angular.module('bm.uiTour', ['ngSanitize', 'ui.bootstrap', 'smoothScroll', 'ezNg', 'cfp.hotkeys'])); - -(function (app: ng.IModule) { - 'use strict'; - - app.factory('uiTourBackdrop', ['TourConfig', '$document', '$uibPosition', '$window', Tour.TourBackdrop.factory]) - .factory('TourHelpers', ['$templateCache', '$http', '$compile', '$location', 'TourConfig', '$q', '$injector', Tour.TourHelper.factory]) - .factory('uiTourService', ['$controller', Tour.uiTourService.factory]) - .provider('TourConfig', [Tour.TourConfigProvider]) - .controller('uiTourController', ['$timeout', '$q', '$filter', 'TourConfig', 'uiTourBackdrop', 'uiTourService', 'ezEventEmitter', 'hotkeys', Tour.TourController]) - .directive('uiTour', ['TourHelpers', (TourHelpers: Tour.TourHelper) => { - - return { - restrict: 'EA', - scope: true, - controller: 'uiTourController', - link: (scope: Tour.ITourScope, element: ng.IRootElementService, attrs, ctrl: Tour.TourController) => { - - //Pass static options through or use defaults - var tour = { - name: attrs.uiTour, - templateUrl: null, - onReady: null - }, - events = 'onReady onStart onEnd onShow onShown onHide onHidden onNext onPrev onPause onResume'.split(' '), - properties = 'placement animation popupDelay closePopupDelay enable appendToBody popupClass orphan backdrop scrollOffset scrollIntoView useUiRouter useHotkeys'.split(' '); - - //Pass interpolated values through - TourHelpers.attachInterpolatedValues(attrs, tour, properties, 'uiTour'); - - //Attach event handlers - TourHelpers.attachEventHandlers(scope, attrs, tour, events, 'uiTour'); - - //override the template url - if (attrs[TourHelpers.getAttrName('templateUrl', 'uiTour')]) { - tour.templateUrl = scope.$eval(attrs[TourHelpers.getAttrName('templateUrl', 'uiTour')]); - } - - //If there is an options argument passed, just use that instead - if (attrs[TourHelpers.getAttrName('options')]) { - angular.extend(tour, scope.$eval(attrs[TourHelpers.getAttrName('options')])); - } - - //Initialize tour - scope.tour = ctrl.init(tour); - if (typeof tour.onReady === 'function') { - tour.onReady(); - } - - scope.$on('$destroy', function () { - ctrl.destroy(); - }); - } - }; - - }]) - .directive('tourStep', ['TourConfig', 'TourHelpers', 'uiTourService', '$uibTooltip', '$q', '$sce', (TourConfig: Tour.ITourConfig, TourHelpers: Tour.TourHelper, TourService: Tour.uiTourService, $uibTooltip, $q: ng.IQService, $sce: ng.ISCEService) => { - - var tourStepDef = $uibTooltip('tourStep', 'tourStep', 'uiTourShow', { - popupDelay: 1 //needs to be non-zero for popping up after navigation - }); - - return { - restrict: 'EA', - scope: true, - require: '?^uiTour', - compile: (tElement, tAttrs) => { - - if (!tAttrs.tourStep) { - tAttrs.$set('tourStep', '\'PH\''); //a placeholder so popup will show - } - - var tourStepLinker = tourStepDef.compile(tElement, tAttrs); - - return (scope: Tour.ITourScope, element: ng.IRootElementService, attrs: ng.IAttributes, uiTourCtrl: Tour.TourController) => { - - var ctrl; - - //check if this step belongs to another tour - if (attrs[TourHelpers.getAttrName('belongsTo')]) { - ctrl = TourService.getTourByName(attrs[TourHelpers.getAttrName('belongsTo')]); - } else if (uiTourCtrl) { - ctrl = uiTourCtrl; - } - - if (!ctrl) { - throw { - name: 'DependencyMissingError', - message: 'No tour provided for tour step.' - }; - } - - //Assign required options - var step = { - stepId: (attrs).tourStep, - enabled: true, - config: (option) => { - if (angular.isDefined(step[option])) { - return step[option]; - } - return ctrl.config(option); - } - }, - events = 'onShow onShown onHide onHidden onNext onPrev'.split(' '), - options = 'content title animation placement backdrop orphan popupDelay popupCloseDelay popupClass fixed preventScrolling scrollIntoView nextStep prevStep nextPath prevPath scrollOffset'.split(' '), - tooltipAttrs = 'animation appendToBody placement popupDelay popupCloseDelay'.split(' '), - orderWatch, - enabledWatch; - - //Will add values to pass to $uibTooltip - var configureInheritedProperties = () => { - TourHelpers.attachTourConfigProperties(scope, attrs, step, tooltipAttrs/*, 'tourStep'*/); - tourStepLinker(scope, element, attrs); - } - - //Pass interpolated values through - TourHelpers.attachInterpolatedValues(attrs, step, options); - orderWatch = attrs.$observe(TourHelpers.getAttrName('order'), (order: number) => { - step.order = !isNaN(order * 1) ? order * 1 : 0; - ctrl.reorderStep(step); - }); - enabledWatch = attrs.$observe(TourHelpers.getAttrName('enabled'), function (isEnabled) { - step.enabled = isEnabled !== 'false'; - if (step.enabled) { - ctrl.addStep(step); - } else { - ctrl.removeStep(step); - } - }); - - //Attach event handlers - TourHelpers.attachEventHandlers(scope, attrs, step, events); - - if (attrs[TourHelpers.getAttrName('templateUrl')]) { - step.templateUrl = scope.$eval(attrs[TourHelpers.getAttrName('templateUrl')]); - } - - //If there is an options argument passed, just use that instead - if (attrs[TourHelpers.getAttrName('options')]) { - angular.extend(step, scope.$eval(attrs[TourHelpers.getAttrName('options')])); - } - - //set up redirects - if (step.nextPath) { - step.redirectNext = true; - TourHelpers.setRedirect(step, ctrl, 'onNext', step.nextPath, step.nextStep); - } - if (step.prevPath) { - step.redirectPrev = true; - TourHelpers.setRedirect(step, ctrl, 'onPrev', step.prevPath, step.prevStep); - } - - //on show and on hide - step.show = () => { - element.triggerHandler('uiTourShow'); - return $q((resolve) => { - element[0].dispatchEvent(new CustomEvent('uiTourShow')); - resolve(); - }); - }; - step.hide = () => { - return $q((resolve) => { - element[0].dispatchEvent(new CustomEvent('uiTourHide')); - resolve(); - }); - }; - - //for HTML content - step.trustedContent = $sce.trustAsHtml(step.content); - - //Add step to tour - scope.tourStep = step; - scope.tour = scope.tour || ctrl; - if (ctrl.initialized) { - configureInheritedProperties(); - ctrl.addStep(step); - } else { - ctrl.once('initialized', function () { - configureInheritedProperties(); - ctrl.addStep(step); - }); - } - - Object.defineProperties(step, { - element: { - value: element - } - }); - - //clean up when element is destroyed - scope.$on('$destroy', function () { - ctrl.removeStep(step); - orderWatch(); - enabledWatch(); - }); - }; - } - }; - - }]) - .directive('tourStepPopup', ['TourConfig', 'smoothScroll', 'ezComponentHelpers', (TourConfig: Tour.ITourConfig, smoothScroll, ezComponentHelpers) => { - return { - restrict: 'EA', - replace: true, - scope: { title: '@', uibTitle: '@uibTitle', content: '@', placement: '@', animation: '&', isOpen: '&', originScope: '&' }, - templateUrl: 'tour-step-popup.html', - link: function (scope, element, attrs) { - var step = scope.originScope().tourStep, - ch = ezComponentHelpers.apply(null, arguments), - scrollOffset = step.config('scrollOffset'); - - //UI Bootstrap name changed in 1.3.0 - if (!scope.title && scope.uibTitle) { - scope.title = scope.uibTitle; - } - - //for arrow styles, unfortunately UI Bootstrap uses attributes for styling - attrs.$set('uib-popover-popup', 'uib-popover-popup'); - - element.css({ - zIndex: TourConfig.get('backdropZIndex') + 2, - display: 'block' - }); - - element.addClass(step.config('popupClass')); - - if (step.config('fixed')) { - element.css('position', 'fixed'); - } - - if (step.config('orphan')) { - ch.useStyles( - `:scope { - position: fixed; - top: 50% !important; - left: 50% !important; - margin: 0 !important; - -ms-transform: translateX(-50%) translateY(-50%); - -moz-transform: translateX(-50%) translateY(-50%); - -webkit-transform: translateX(-50%) translateY(-50%); - transform: translateX(-50%) translateY(-50%); - } - - .arrow - display: none; - }` - ); - } - - scope.$watch('isOpen', function (isOpen) { - if (isOpen() && !step.config('orphan') && step.config('scrollIntoView')) { - smoothScroll(element[0], { - offset: scrollOffset - }); - } - }); - } - }; - }]) - .run(['$templateCache', function ($templateCache) { - $templateCache.put("tour-step-popup.html", - `
-
- -
-

-
-
-
- `); - $templateCache.put("tour-step-template.html", - `
-
-
-
- - - -
- -
-
- `); - }]); -} (angular.module('bm.uiTour'))); - -(function (window) { - function CustomEvent(event, params) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); - return evt; - } - - CustomEvent.prototype = window.Event.prototype; - - window.CustomEvent = CustomEvent; -})(window);