From 1e8006c4ebc1d302b08df5a531d5d9a948d63bac Mon Sep 17 00:00:00 2001 From: Alex Galays Date: Tue, 5 Nov 2013 20:39:06 +0100 Subject: [PATCH] Refactor the build - No more local dependency files - Use browserify to output a UMD compatible module - The abyssa internal modules have clearer dependencies (require calls at the top) - Get rid or Bower, which is redundant when using NPM/browserify - The default build file (abyssa.js) is now the one without embedded dependencies --- .gitignore | 3 +- Gruntfile.js | 67 +- bower.json | 25 - lib/crossroads.js | 690 ----- lib/history.iegte8.js | 974 ------- lib/signals.js | 425 --- lib/when.js | 926 ------- package.json | 11 +- src/Router.js | 27 +- src/State.js | 15 +- src/StateWithParams.js | 84 +- src/Transition.js | 6 +- src/anchorClicks.js | 121 +- src/footer.js | 4 - src/header.js | 19 - src/main.js | 10 + src/util.js | 12 +- target/abyssa-nodeps.js | 1073 -------- target/abyssa-nodeps.min.js | 3 - target/abyssa-with-deps.js | 4186 +++++++++++++++++++++++++++++ target/abyssa-with-deps.min.js | 4 + target/abyssa.js | 4554 ++++++-------------------------- target/abyssa.min.js | 4 +- test/testRunner.html | 2 +- 24 files changed, 5165 insertions(+), 8080 deletions(-) delete mode 100644 bower.json delete mode 100644 lib/crossroads.js delete mode 100644 lib/history.iegte8.js delete mode 100644 lib/signals.js delete mode 100644 lib/when.js delete mode 100644 src/footer.js delete mode 100644 src/header.js create mode 100644 src/main.js delete mode 100644 target/abyssa-nodeps.js delete mode 100644 target/abyssa-nodeps.min.js create mode 100644 target/abyssa-with-deps.js create mode 100644 target/abyssa-with-deps.min.js diff --git a/.gitignore b/.gitignore index bb93d68..b512c09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -node_modules -bower_components \ No newline at end of file +node_modules \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index d1e5cad..f508583 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,52 +2,60 @@ module.exports = function(grunt) { var banner = '/* <%= pkg.name %> <%= pkg.version %> - <%= pkg.description %> */\n\n'; - var libFiles = [ - 'lib/signals.js', - 'lib/crossroads.js', - 'lib/when.js', - 'lib/history.iegte8.js', - ]; - - var srcFiles = [ - 'src/header.js', - 'src/util.js', - 'src/StateWithParams.js', - 'src/Transition.js', - 'src/State.js', - 'src/Router.js', - 'src/anchorClicks.js', - 'src/footer.js' + var dependencies = [ + 'when', + 'signals', + 'crossroads', + 'html5-history-api/history.iegte8' ]; grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), + + browserify: { + build: { + files: {'target/abyssa.js': ['src/main.js']}, + options: { + standalone: 'Abyssa', + ignore: dependencies + } + }, + buildWithDeps: { + files: {'target/abyssa-with-deps.js': ['src/main.js'] }, + options: { + standalone: 'Abyssa' + } + } + }, + concat: { options: { banner: banner }, - buildWithDeps: { - src: libFiles.concat(srcFiles), - dest: 'target/<%= pkg.name %>.js' + build: { + src: ['target/abyssa.js'], + dest: 'target/abyssa.js' }, - buildWithoutDeps: { - src: srcFiles, - dest: 'target/<%= pkg.name %>-nodeps.js' + buildWithDeps: { + src: ['target/abyssa-with-deps.js'], + dest: 'target/abyssa-with-deps.js' } }, + uglify: { options: { banner: banner }, - buildWithDeps: { - src: 'target/<%= pkg.name %>.js', - dest: 'target/<%= pkg.name %>.min.js' + build: { + src: 'target/abyssa.js', + dest: 'target/abyssa.min.js' }, - buildWithoutDeps: { - src: 'target/<%= pkg.name %>-nodeps.js', - dest: 'target/<%= pkg.name %>-nodeps.min.js' + buildWithDeps: { + src: 'target/abyssa-with-deps.js', + dest: 'target/abyssa-with-deps.min.js' } }, + watch: { all: { files: ['src/*.js'], @@ -59,7 +67,8 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-browserify'); - grunt.registerTask('default', ['concat', 'uglify']); + grunt.registerTask('default', ['browserify', 'concat', 'uglify']); grunt.registerTask('dev', ['default', 'watch']); }; \ No newline at end of file diff --git a/bower.json b/bower.json deleted file mode 100644 index bdc6e76..0000000 --- a/bower.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "abyssa", - "version": "1.2.4", - "description": "A stateful router library for single page applications", - "keywords": ["routes", "routing", "router", "hierarchical", "stateful", "pushState"], - "main": "target/abyssa.js", - "homepage": "https://github.com/AlexGalays/abyssa-js/", - "author": { - "name": "Alexandre Galays", - "url": "https://github.com/AlexGalays/" - }, - "repository": { - "type": "git", - "url": "https://github.com/AlexGalays/abyssa-js.git" - }, - "licenses": [ - "http://www.opensource.org/licenses/mit-license.php" - ], - "dependencies": { - "when": "2.5.1", - "js-signals": "1.0.0", - "crossroads.js": "0.12.0", - "html5-history-api": "9be1cdadcd22435fb73850209f665a192248e3e4" - } -} diff --git a/lib/crossroads.js b/lib/crossroads.js deleted file mode 100644 index c1e00aa..0000000 --- a/lib/crossroads.js +++ /dev/null @@ -1,690 +0,0 @@ -/** @license - * crossroads - * Author: Miller Medeiros | MIT License - * v0.12.0 (2013/01/21 13:47) - */ - -var crossroads = (function () { -var factory = function (signals) { - - var crossroads, - _hasOptionalGroupBug, - UNDEF; - - // Helpers ----------- - //==================== - - // IE 7-8 capture optional groups as empty strings while other browsers - // capture as `undefined` - _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === ''; - - function arrayIndexOf(arr, val) { - if (arr.indexOf) { - return arr.indexOf(val); - } else { - //Array.indexOf doesn't work on IE 6-7 - var n = arr.length; - while (n--) { - if (arr[n] === val) { - return n; - } - } - return -1; - } - } - - function arrayRemove(arr, item) { - var i = arrayIndexOf(arr, item); - if (i !== -1) { - arr.splice(i, 1); - } - } - - function isKind(val, kind) { - return '[object '+ kind +']' === Object.prototype.toString.call(val); - } - - function isRegExp(val) { - return isKind(val, 'RegExp'); - } - - function isArray(val) { - return isKind(val, 'Array'); - } - - function isFunction(val) { - return typeof val === 'function'; - } - - //borrowed from AMD-utils - function typecastValue(val) { - var r; - if (val === null || val === 'null') { - r = null; - } else if (val === 'true') { - r = true; - } else if (val === 'false') { - r = false; - } else if (val === UNDEF || val === 'undefined') { - r = UNDEF; - } else if (val === '' || isNaN(val)) { - //isNaN('') returns false - r = val; - } else { - //parseFloat(null || '') returns NaN - r = parseFloat(val); - } - return r; - } - - function typecastArrayValues(values) { - var n = values.length, - result = []; - while (n--) { - result[n] = typecastValue(values[n]); - } - return result; - } - - //borrowed from AMD-Utils - function decodeQueryString(str, shouldTypecast) { - var queryArr = (str || '').replace('?', '').split('&'), - n = queryArr.length, - obj = {}, - item, val; - while (n--) { - item = queryArr[n].split('='); - val = shouldTypecast ? typecastValue(item[1]) : item[1]; - obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val; - } - return obj; - } - - - // Crossroads -------- - //==================== - - /** - * @constructor - */ - function Crossroads() { - this.bypassed = new signals.Signal(); - this.routed = new signals.Signal(); - this._routes = []; - this._prevRoutes = []; - this._piped = []; - this.resetState(); - } - - Crossroads.prototype = { - - greedy : false, - - greedyEnabled : true, - - ignoreCase : true, - - ignoreState : false, - - shouldTypecast : false, - - normalizeFn : null, - - resetState : function(){ - this._prevRoutes.length = 0; - this._prevMatchedRequest = null; - this._prevBypassedRequest = null; - }, - - create : function () { - return new Crossroads(); - }, - - addRoute : function (pattern, callback, priority) { - var route = new Route(pattern, callback, priority, this); - this._sortedInsert(route); - return route; - }, - - removeRoute : function (route) { - arrayRemove(this._routes, route); - route._destroy(); - }, - - removeAllRoutes : function () { - var n = this.getNumRoutes(); - while (n--) { - this._routes[n]._destroy(); - } - this._routes.length = 0; - }, - - parse : function (request, defaultArgs) { - request = request || ''; - defaultArgs = defaultArgs || []; - - // should only care about different requests if ignoreState isn't true - if ( !this.ignoreState && - (request === this._prevMatchedRequest || - request === this._prevBypassedRequest) ) { - return; - } - - var routes = this._getMatchedRoutes(request), - i = 0, - n = routes.length, - cur; - - if (n) { - this._prevMatchedRequest = request; - - this._notifyPrevRoutes(routes, request); - this._prevRoutes = routes; - //should be incremental loop, execute routes in order - while (i < n) { - cur = routes[i]; - cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params)); - cur.isFirst = !i; - this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur])); - i += 1; - } - } else { - this._prevBypassedRequest = request; - this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request])); - } - - this._pipeParse(request, defaultArgs); - }, - - _notifyPrevRoutes : function(matchedRoutes, request) { - var i = 0, prev; - while (prev = this._prevRoutes[i++]) { - //check if switched exist since route may be disposed - if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) { - prev.route.switched.dispatch(request); - } - } - }, - - _didSwitch : function (route, matchedRoutes){ - var matched, - i = 0; - while (matched = matchedRoutes[i++]) { - // only dispatch switched if it is going to a different route - if (matched.route === route) { - return false; - } - } - return true; - }, - - _pipeParse : function(request, defaultArgs) { - var i = 0, route; - while (route = this._piped[i++]) { - route.parse(request, defaultArgs); - } - }, - - getNumRoutes : function () { - return this._routes.length; - }, - - _sortedInsert : function (route) { - //simplified insertion sort - var routes = this._routes, - n = routes.length; - do { --n; } while (routes[n] && route._priority <= routes[n]._priority); - routes.splice(n+1, 0, route); - }, - - _getMatchedRoutes : function (request) { - var res = [], - routes = this._routes, - n = routes.length, - route; - //should be decrement loop since higher priorities are added at the end of array - while (route = routes[--n]) { - if ((!res.length || this.greedy || route.greedy) && route.match(request)) { - res.push({ - route : route, - params : route._getParamsArray(request) - }); - } - if (!this.greedyEnabled && res.length) { - break; - } - } - return res; - }, - - pipe : function (otherRouter) { - this._piped.push(otherRouter); - }, - - unpipe : function (otherRouter) { - arrayRemove(this._piped, otherRouter); - }, - - toString : function () { - return '[crossroads numRoutes:'+ this.getNumRoutes() +']'; - } - }; - - //"static" instance - crossroads = new Crossroads(); - crossroads.VERSION = '0.12.0'; - - crossroads.NORM_AS_ARRAY = function (req, vals) { - return [vals.vals_]; - }; - - crossroads.NORM_AS_OBJECT = function (req, vals) { - return [vals]; - }; - - - // Route -------------- - //===================== - - /** - * @constructor - */ - function Route(pattern, callback, priority, router) { - var isRegexPattern = isRegExp(pattern), - patternLexer = router.patternLexer; - this._router = router; - this._pattern = pattern; - this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern); - this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern); - this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase); - this.matched = new signals.Signal(); - this.switched = new signals.Signal(); - if (callback) { - this.matched.add(callback); - } - this._priority = priority || 0; - } - - Route.prototype = { - - greedy : false, - - rules : void(0), - - match : function (request) { - request = request || ''; - return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule. - }, - - _validateParams : function (request) { - var rules = this.rules, - values = this._getParamsObject(request), - key; - for (key in rules) { - // normalize_ isn't a validation rule... (#39) - if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){ - return false; - } - } - return true; - }, - - _isValidParam : function (request, prop, values) { - var validationRule = this.rules[prop], - val = values[prop], - isValid = false, - isQuery = (prop.indexOf('?') === 0); - - if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) { - isValid = true; - } - else if (isRegExp(validationRule)) { - if (isQuery) { - val = values[prop +'_']; //use raw string - } - isValid = validationRule.test(val); - } - else if (isArray(validationRule)) { - if (isQuery) { - val = values[prop +'_']; //use raw string - } - isValid = this._isValidArrayRule(validationRule, val); - } - else if (isFunction(validationRule)) { - isValid = validationRule(val, request, values); - } - - return isValid; //fail silently if validationRule is from an unsupported type - }, - - _isValidArrayRule : function (arr, val) { - if (! this._router.ignoreCase) { - return arrayIndexOf(arr, val) !== -1; - } - - if (typeof val === 'string') { - val = val.toLowerCase(); - } - - var n = arr.length, - item, - compareVal; - - while (n--) { - item = arr[n]; - compareVal = (typeof item === 'string')? item.toLowerCase() : item; - if (compareVal === val) { - return true; - } - } - return false; - }, - - _getParamsObject : function (request) { - var shouldTypecast = this._router.shouldTypecast, - values = this._router.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast), - o = {}, - n = values.length, - param, val; - while (n--) { - val = values[n]; - if (this._paramsIds) { - param = this._paramsIds[n]; - if (param.indexOf('?') === 0 && val) { - //make a copy of the original string so array and - //RegExp validation can be applied properly - o[param +'_'] = val; - //update vals_ array as well since it will be used - //during dispatch - val = decodeQueryString(val, shouldTypecast); - values[n] = val; - } - // IE will capture optional groups as empty strings while other - // browsers will capture `undefined` so normalize behavior. - // see: #gh-58, #gh-59, #gh-60 - if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) { - val = void(0); - values[n] = val; - } - o[param] = val; - } - //alias to paths and for RegExp pattern - o[n] = val; - } - o.request_ = shouldTypecast? typecastValue(request) : request; - o.vals_ = values; - return o; - }, - - _getParamsArray : function (request) { - var norm = this.rules? this.rules.normalize_ : null, - params; - norm = norm || this._router.normalizeFn; // default normalize - if (norm && isFunction(norm)) { - params = norm(request, this._getParamsObject(request)); - } else { - params = this._getParamsObject(request).vals_; - } - return params; - }, - - interpolate : function(replacements) { - var str = this._router.patternLexer.interpolate(this._pattern, replacements); - if (! this._validateParams(str) ) { - throw new Error('Generated string doesn\'t validate against `Route.rules`.'); - } - return str; - }, - - dispose : function () { - this._router.removeRoute(this); - }, - - _destroy : function () { - this.matched.dispose(); - this.switched.dispose(); - this.matched = this.switched = this._pattern = this._matchRegexp = null; - }, - - toString : function () { - return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']'; - } - - }; - - - - // Pattern Lexer ------ - //===================== - - Crossroads.prototype.patternLexer = (function () { - - var - //match chars that should be escaped on string regexp - ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, - - //trailing slashes (begin/end of string) - LOOSE_SLASHES_REGEXP = /^\/|\/$/g, - LEGACY_SLASHES_REGEXP = /\/$/g, - - //params - everything between `{ }` or `: :` - PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, - - //used to save params during compile (avoid escaping things that - //shouldn't be escaped). - TOKENS = { - 'OS' : { - //optional slashes - //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?` - rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g, - save : '$1{{id}}$2', - res : '\\/?' - }, - 'RS' : { - //required slashes - //used to insert slash between `:{` and `}{` - rgx : /([:}])\/?(\{)/g, - save : '$1{{id}}$2', - res : '\\/' - }, - 'RQ' : { - //required query string - everything in between `{? }` - rgx : /\{\?([^}]+)\}/g, - //everything from `?` till `#` or end of string - res : '\\?([^#]+)' - }, - 'OQ' : { - //optional query string - everything in between `:? :` - rgx : /:\?([^:]+):/g, - //everything from `?` till `#` or end of string - res : '(?:\\?([^#]*))?' - }, - 'OR' : { - //optional rest - everything in between `: *:` - rgx : /:([^:]+)\*:/g, - res : '(.*)?' // optional group to avoid passing empty string as captured - }, - 'RR' : { - //rest param - everything in between `{ *}` - rgx : /\{([^}]+)\*\}/g, - res : '(.+)' - }, - // required/optional params should come after rest segments - 'RP' : { - //required params - everything between `{ }` - rgx : /\{([^}]+)\}/g, - res : '([^\\/?]+)' - }, - 'OP' : { - //optional params - everything between `: :` - rgx : /:([^:]+):/g, - res : '([^\\/?]+)?\/?' - } - }, - - LOOSE_SLASH = 1, - STRICT_SLASH = 2, - LEGACY_SLASH = 3, - - _slashMode = LOOSE_SLASH; - - - function precompileTokens(){ - var key, cur; - for (key in TOKENS) { - if (TOKENS.hasOwnProperty(key)) { - cur = TOKENS[key]; - cur.id = '__CR_'+ key +'__'; - cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id; - cur.rRestore = new RegExp(cur.id, 'g'); - } - } - } - precompileTokens(); - - - function captureVals(regex, pattern) { - var vals = [], match; - // very important to reset lastIndex since RegExp can have "g" flag - // and multiple runs might affect the result, specially if matching - // same string multiple times on IE 7-8 - regex.lastIndex = 0; - while (match = regex.exec(pattern)) { - vals.push(match[1]); - } - return vals; - } - - function getParamIds(pattern) { - return captureVals(PARAMS_REGEXP, pattern); - } - - function getOptionalParamsIds(pattern) { - return captureVals(TOKENS.OP.rgx, pattern); - } - - function compilePattern(pattern, ignoreCase) { - pattern = pattern || ''; - - if(pattern){ - if (_slashMode === LOOSE_SLASH) { - pattern = pattern.replace(LOOSE_SLASHES_REGEXP, ''); - } - else if (_slashMode === LEGACY_SLASH) { - pattern = pattern.replace(LEGACY_SLASHES_REGEXP, ''); - } - - //save tokens - pattern = replaceTokens(pattern, 'rgx', 'save'); - //regexp escape - pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&'); - //restore tokens - pattern = replaceTokens(pattern, 'rRestore', 'res'); - - if (_slashMode === LOOSE_SLASH) { - pattern = '\\/?'+ pattern; - } - } - - if (_slashMode !== STRICT_SLASH) { - //single slash is treated as empty and end slash is optional - pattern += '\\/?'; - } - return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : ''); - } - - function replaceTokens(pattern, regexpName, replaceName) { - var cur, key; - for (key in TOKENS) { - if (TOKENS.hasOwnProperty(key)) { - cur = TOKENS[key]; - pattern = pattern.replace(cur[regexpName], cur[replaceName]); - } - } - return pattern; - } - - function getParamValues(request, regexp, shouldTypecast) { - var vals = regexp.exec(request); - if (vals) { - vals.shift(); - if (shouldTypecast) { - vals = typecastArrayValues(vals); - } - } - return vals; - } - - function interpolate(pattern, replacements) { - if (typeof pattern !== 'string') { - throw new Error('Route pattern should be a string.'); - } - - var replaceFn = function(match, prop){ - var val; - prop = (prop.substr(0, 1) === '?')? prop.substr(1) : prop; - if (replacements[prop] != null) { - if (typeof replacements[prop] === 'object') { - var queryParts = []; - for(var key in replacements[prop]) { - queryParts.push(encodeURI(key + '=' + replacements[prop][key])); - } - val = '?' + queryParts.join('&'); - } else { - // make sure value is a string see #gh-54 - val = String(replacements[prop]); - } - - if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) { - throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".'); - } - } - else if (match.indexOf('{') !== -1) { - throw new Error('The segment '+ match +' is required.'); - } - else { - val = ''; - } - return val; - }; - - if (! TOKENS.OS.trail) { - TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$'); - } - - return pattern - .replace(TOKENS.OS.rgx, TOKENS.OS.save) - .replace(PARAMS_REGEXP, replaceFn) - .replace(TOKENS.OS.trail, '') // remove trailing - .replace(TOKENS.OS.rRestore, '/'); // add slash between segments - } - - //API - return { - strict : function(){ - _slashMode = STRICT_SLASH; - }, - loose : function(){ - _slashMode = LOOSE_SLASH; - }, - legacy : function(){ - _slashMode = LEGACY_SLASH; - }, - getParamIds : getParamIds, - getOptionalParamsIds : getOptionalParamsIds, - getParamValues : getParamValues, - compilePattern : compilePattern, - interpolate : interpolate - }; - - }()); - - - return crossroads; -}; - - -return factory(window['signals']); - - -}()); diff --git a/lib/history.iegte8.js b/lib/history.iegte8.js deleted file mode 100644 index 47cc4e2..0000000 --- a/lib/history.iegte8.js +++ /dev/null @@ -1,974 +0,0 @@ -/*! - * History API JavaScript Library v4.0.8 - * - * Support: IE8+, FF3+, Opera 9+, Safari, Chrome and other - * - * Copyright 2011-2013, Dmitrii Pakhtinov ( spb.piksel@gmail.com ) - * - * http://spb-piksel.ru/ - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Update: 2013-10-31 16:06 - */ -(function(window) { - // Prevent the code from running if there is no window.history object - if (!window.history) return; - // symlink to document - var document = window.document; - // HTML element - var documentElement = document.documentElement; - // symlink to sessionStorage - var sessionStorage = null; - // symlink to constructor of Object - var Object = window['Object']; - // symlink to JSON Object - var JSON = window['JSON']; - // symlink to instance object of 'Location' - var windowLocation = window.location; - // symlink to instance object of 'History' - var windowHistory = window.history; - // new instance of 'History'. The default is a reference to the original object instance - var historyObject = windowHistory; - // symlink to method 'history.pushState' - var historyPushState = windowHistory.pushState; - // symlink to method 'history.replaceState' - var historyReplaceState = windowHistory.replaceState; - // if the browser supports HTML5-History-API - var isSupportHistoryAPI = !!historyPushState; - // verifies the presence of an object 'state' in interface 'History' - var isSupportStateObjectInHistory = 'state' in windowHistory; - // symlink to method 'Object.defineProperty' - var defineProperty = Object.defineProperty; - // new instance of 'Location', for IE8 will use the element HTMLAnchorElement, instead of pure object - var locationObject = redefineProperty({}, 't') ? {} : document.createElement('a'); - // prefix for the names of events - var eventNamePrefix = ''; - // String that will contain the name of the method - var addEventListenerName = window.addEventListener ? 'addEventListener' : (eventNamePrefix = 'on') && 'attachEvent'; - // String that will contain the name of the method - var removeEventListenerName = window.removeEventListener ? 'removeEventListener' : 'detachEvent'; - // String that will contain the name of the method - var dispatchEventName = window.dispatchEvent ? 'dispatchEvent' : 'fireEvent'; - // reference native methods for the events - var addEvent = window[addEventListenerName]; - var removeEvent = window[removeEventListenerName]; - var dispatch = window[dispatchEventName]; - // default settings - var settings = {"basepath": '/', "redirect": 0, "type": '/'}; - // key for the sessionStorage - var sessionStorageKey = '__historyAPI__'; - // Anchor Element for parseURL function - var anchorElement = document.createElement('a'); - // last URL before change to new URL - var lastURL = windowLocation.href; - // Control URL, need to fix the bug in Opera - var checkUrlForPopState = ''; - // trigger event 'onpopstate' on page load - var isFireInitialState = false; - // store a list of 'state' objects in the current session - var stateStorage = {}; - // in this object will be stored custom handlers - var eventsList = {}; - // stored last title - var lastTitle = document.title; - - /** - * Properties that will be replaced in the global - * object 'window', to prevent conflicts - * - * @type {Object} - */ - var eventsDescriptors = { - "onhashchange": null, - "onpopstate": null - }; - - /** - * Fix for Chrome in iOS - * See https://github.com/devote/HTML5-History-API/issues/29 - */ - var fastFixChrome = function(method, args) { - var isNeedFix = window.history !== windowHistory; - if (isNeedFix) { - window.history = windowHistory; - } - method.apply(windowHistory, args); - if (isNeedFix) { - window.history = historyObject; - } - }; - - /** - * Properties that will be replaced/added to object - * 'window.history', includes the object 'history.location', - * for a complete the work with the URL address - * - * @type {Object} - */ - var historyDescriptors = { - /** - * @namespace history - * @param {String} [type] - * @param {String} [basepath] - */ - "redirect": function(type, basepath) { - settings["basepath"] = basepath = basepath == null ? settings["basepath"] : basepath; - settings["type"] = type = type == null ? settings["type"] : type; - if (window.top == window.self) { - var relative = parseURL(null, false, true)._relative; - var path = windowLocation.pathname + windowLocation.search; - if (isSupportHistoryAPI) { - path = path.replace(/([^\/])$/, '$1/'); - if (relative != basepath && (new RegExp("^" + basepath + "$", "i")).test(path)) { - windowLocation.replace(relative); - } - } else if (path != basepath) { - path = path.replace(/([^\/])\?/, '$1/?'); - if ((new RegExp("^" + basepath, "i")).test(path)) { - windowLocation.replace(basepath + '#' + path. - replace(new RegExp("^" + basepath, "i"), type) + windowLocation.hash); - } - } - } - }, - /** - * The method adds a state object entry - * to the history. - * - * @namespace history - * @param {Object} state - * @param {string} title - * @param {string} [url] - */ - pushState: function(state, title, url) { - var t = document.title; - if (lastTitle != null) { - document.title = lastTitle; - } - historyPushState && fastFixChrome(historyPushState, arguments); - changeState(state, url); - document.title = t; - lastTitle = title; - }, - /** - * The method updates the state object, - * title, and optionally the URL of the - * current entry in the history. - * - * @namespace history - * @param {Object} state - * @param {string} title - * @param {string} [url] - */ - replaceState: function(state, title, url) { - var t = document.title; - if (lastTitle != null) { - document.title = lastTitle; - } - delete stateStorage[windowLocation.href]; - historyReplaceState && fastFixChrome(historyReplaceState, arguments); - changeState(state, url, true); - document.title = t; - lastTitle = title; - }, - /** - * Object 'history.location' is similar to the - * object 'window.location', except that in - * HTML4 browsers it will behave a bit differently - * - * @namespace history - */ - "location": { - set: function(value) { - window.location = value; - }, - get: function() { - return isSupportHistoryAPI ? windowLocation : locationObject; - } - }, - /** - * A state object is an object representing - * a user interface state. - * - * @namespace history - */ - "state": { - get: function() { - return stateStorage[windowLocation.href] || null; - } - } - }; - - /** - * Properties for object 'history.location'. - * Object 'history.location' is similar to the - * object 'window.location', except that in - * HTML4 browsers it will behave a bit differently - * - * @type {Object} - */ - var locationDescriptors = { - /** - * Navigates to the given page. - * - * @namespace history.location - */ - assign: function(url) { - if (('' + url).indexOf('#') === 0) { - changeState(null, url); - } else { - windowLocation.assign(url); - } - }, - /** - * Reloads the current page. - * - * @namespace history.location - */ - reload: function() { - windowLocation.reload(); - }, - /** - * Removes the current page from - * the session history and navigates - * to the given page. - * - * @namespace history.location - */ - replace: function(url) { - if (('' + url).indexOf('#') === 0) { - changeState(null, url, true); - } else { - windowLocation.replace(url); - } - }, - /** - * Returns the current page's location. - * - * @namespace history.location - */ - toString: function() { - return this.href; - }, - /** - * Returns the current page's location. - * Can be set, to navigate to another page. - * - * @namespace history.location - */ - "href": { - get: function() { - return parseURL()._href; - } - }, - /** - * Returns the current page's protocol. - * - * @namespace history.location - */ - "protocol": null, - /** - * Returns the current page's host and port number. - * - * @namespace history.location - */ - "host": null, - /** - * Returns the current page's host. - * - * @namespace history.location - */ - "hostname": null, - /** - * Returns the current page's port number. - * - * @namespace history.location - */ - "port": null, - /** - * Returns the current page's path only. - * - * @namespace history.location - */ - "pathname": { - get: function() { - return parseURL()._pathname; - } - }, - /** - * Returns the current page's search - * string, beginning with the character - * '?' and to the symbol '#' - * - * @namespace history.location - */ - "search": { - get: function() { - return parseURL()._search; - } - }, - /** - * Returns the current page's hash - * string, beginning with the character - * '#' and to the end line - * - * @namespace history.location - */ - "hash": { - set: function(value) { - changeState(null, ('' + value).replace(/^(#|)/, '#'), false, lastURL); - }, - get: function() { - return parseURL()._hash; - } - } - }; - - /** - * Just empty function - * - * @return void - */ - function emptyFunction() { - // dummy - } - - /** - * Prepares a parts of the current or specified reference for later use in the library - * - * @param {string} [href] - * @param {boolean} [isWindowLocation] - * @param {boolean} [isNotAPI] - * @return {Object} - */ - function parseURL(href, isWindowLocation, isNotAPI) { - var re = /(?:([\w0-9]+:))?(?:\/\/(?:[^@]*@)?([^\/:\?#]+)(?::([0-9]+))?)?([^\?#]*)(?:(\?[^#]+)|\?)?(?:(#.*))?/; - if (href != null && href !== '' && !isWindowLocation) { - var current = parseURL(), _pathname = current._pathname, _protocol = current._protocol; - // convert to type of string - href = '' + href; - // convert relative link to the absolute - href = /^(?:[\w0-9]+\:)?\/\//.test(href) ? href.indexOf("/") === 0 - ? _protocol + href : href : _protocol + "//" + current._host + ( - href.indexOf("/") === 0 ? href : href.indexOf("?") === 0 - ? _pathname + href : href.indexOf("#") === 0 - ? _pathname + current._search + href : _pathname.replace(/[^\/]+$/g, '') + href - ); - } else { - href = isWindowLocation ? href : windowLocation.href; - // if current browser not support History-API - if (!isSupportHistoryAPI || isNotAPI) { - // get hash fragment - href = href.replace(/^[^#]*/, '') || "#"; - // form the absolute link from the hash - href = windowLocation.protocol + '//' + windowLocation.host + settings['basepath'] - + href.replace(new RegExp("^#[\/]?(?:" + settings["type"] + ")?"), ""); - } - } - // that would get rid of the links of the form: /../../ - anchorElement.href = href; - // decompose the link in parts - var result = re.exec(anchorElement.href); - // host name with the port number - var host = result[2] + (result[3] ? ':' + result[3] : ''); - // folder - var pathname = result[4] || '/'; - // the query string - var search = result[5] || ''; - // hash - var hash = result[6] === '#' ? '' : (result[6] || ''); - // relative link, no protocol, no host - var relative = pathname + search + hash; - // special links for set to hash-link, if browser not support History API - var nohash = pathname.replace(new RegExp("^" + settings["basepath"], "i"), settings["type"]) + search; - // result - return { - _href: result[1] + '//' + host + relative, - _protocol: result[1], - _host: host, - _hostname: result[2], - _port: result[3] || '', - _pathname: pathname, - _search: search, - _hash: hash, - _relative: relative, - _nohash: nohash, - _special: nohash + hash - } - } - - /** - * Initializing storage for the custom state's object - */ - function storageInitialize() { - var storage = ''; - if (sessionStorage) { - // get cache from the storage in browser - storage += sessionStorage.getItem(sessionStorageKey); - } else { - var cookie = document.cookie.split(sessionStorageKey + "="); - if (cookie.length > 1) { - storage += (cookie.pop().split(";").shift() || 'null'); - } - } - try { - stateStorage = JSON.parse(storage) || {}; - } catch(_e_) { - stateStorage = {}; - } - // hang up the event handler to event unload page - addEvent(eventNamePrefix + 'unload', function() { - if (sessionStorage) { - // save current state's object - sessionStorage.setItem(sessionStorageKey, JSON.stringify(stateStorage)); - } else { - // save the current 'state' in the cookie - var state = {}; - if (state[windowLocation.href] = historyObject.state) { - document.cookie = sessionStorageKey + '=' + JSON.stringify(state); - } - } - }, false); - } - - /** - * This method is implemented to override the built-in(native) - * properties in the browser, unfortunately some browsers are - * not allowed to override all the properties and even add. - * For this reason, this was written by a method that tries to - * do everything necessary to get the desired result. - * - * @param {Object} object The object in which will be overridden/added property - * @param {String} prop The property name to be overridden/added - * @param {Object} [descriptor] An object containing properties set/get - * @param {Function} [onWrapped] The function to be called when the wrapper is created - * @return {Object|Boolean} Returns an object on success, otherwise returns false - */ - function redefineProperty(object, prop, descriptor, onWrapped) { - // test only if descriptor is undefined - descriptor = descriptor || {set: emptyFunction}; - // variable will have a value of true the success of attempts to set descriptors - var isDefinedSetter = !descriptor.set; - var isDefinedGetter = !descriptor.get; - // for tests of attempts to set descriptors - var test = {configurable: true, set: function() { - isDefinedSetter = 1; - }, get: function() { - isDefinedGetter = 1; - }}; - - try { - // testing for the possibility of overriding/adding properties - defineProperty(object, prop, test); - // running the test - object[prop] = object[prop]; - // attempt to override property using the standard method - defineProperty(object, prop, descriptor); - } catch(_e_) { - } - - // If the variable 'isDefined' has a false value, it means that need to try other methods - if (!isDefinedSetter || !isDefinedGetter) { - // try to override/add the property, using deprecated functions - if (object.__defineGetter__) { - // testing for the possibility of overriding/adding properties - object.__defineGetter__(prop, test.get); - object.__defineSetter__(prop, test.set); - // running the test - object[prop] = object[prop]; - // attempt to override property using the deprecated functions - descriptor.get && object.__defineGetter__(prop, descriptor.get); - descriptor.set && object.__defineSetter__(prop, descriptor.set); - } - - // Browser refused to override the property, using the standard and deprecated methods - if ((!isDefinedSetter || !isDefinedGetter) && object === window) { - try { - // save original value from this property - var originalValue = object[prop]; - // set null to built-in(native) property - object[prop] = null; - } catch(_e_) { - } - // This rule for Internet Explorer 8 - if ('execScript' in window) { - /** - * to IE8 override the global properties using - * VBScript, declaring it in global scope with - * the same names. - */ - window['execScript']('Public ' + prop, 'VBScript'); - } else { - try { - /** - * This hack allows to override a property - * with the set 'configurable: false', working - * in the hack 'Safari' to 'Mac' - */ - defineProperty(object, prop, {value: emptyFunction}); - } catch(_e_) { - } - } - // set old value to new variable - object[prop] = originalValue; - - } else if (!isDefinedSetter || !isDefinedGetter) { - // the last stage of trying to override the property - try { - try { - // wrap the object in a new empty object - var temp = Object.create(object); - defineProperty(Object.getPrototypeOf(temp) === object ? temp : object, prop, descriptor); - for(var key in object) { - // need to bind a function to the original object - if (typeof object[key] === 'function') { - temp[key] = object[key].bind(object); - } - } - try { - // to run a function that will inform about what the object was to wrapped - onWrapped.call(temp, temp, object); - } catch(_e_) { - } - object = temp; - } catch(_e_) { - // sometimes works override simply by assigning the prototype property of the constructor - defineProperty(object.constructor.prototype, prop, descriptor); - } - } catch(_e_) { - // all methods have failed - return false; - } - } - } - - return object; - } - - /** - * Adds the missing property in descriptor - * - * @param {Object} object An object that stores values - * @param {String} prop Name of the property in the object - * @param {Object|null} descriptor Descriptor - * @return {Object} Returns the generated descriptor - */ - function prepareDescriptorsForObject(object, prop, descriptor) { - descriptor = descriptor || {}; - // the default for the object 'location' is the standard object 'window.location' - object = object === locationDescriptors ? windowLocation : object; - // setter for object properties - descriptor.set = (descriptor.set || function(value) { - object[prop] = value; - }); - // getter for object properties - descriptor.get = (descriptor.get || function() { - return object[prop]; - }); - return descriptor; - } - - /** - * Wrapper for the methods 'addEventListener/attachEvent' in the context of the 'window' - * - * @param {String} event The event type for which the user is registering - * @param {Function} listener The method to be called when the event occurs. - * @param {Boolean} capture If true, capture indicates that the user wishes to initiate capture. - * @return void - */ - function addEventListener(event, listener, capture) { - if (event in eventsList) { - // here stored the event listeners 'popstate/hashchange' - eventsList[event].push(listener); - } else { - // FireFox support non-standart four argument aWantsUntrusted - // https://github.com/devote/HTML5-History-API/issues/13 - if (arguments.length > 3) { - addEvent(event, listener, capture, arguments[3]); - } else { - addEvent(event, listener, capture); - } - } - } - - /** - * Wrapper for the methods 'removeEventListener/detachEvent' in the context of the 'window' - * - * @param {String} event The event type for which the user is registered - * @param {Function} listener The parameter indicates the Listener to be removed. - * @param {Boolean} capture Was registered as a capturing listener or not. - * @return void - */ - function removeEventListener(event, listener, capture) { - var list = eventsList[event]; - if (list) { - for(var i = list.length; --i;) { - if (list[i] === listener) { - list.splice(i, 1); - break; - } - } - } else { - removeEvent(event, listener, capture); - } - } - - /** - * Wrapper for the methods 'dispatchEvent/fireEvent' in the context of the 'window' - * - * @param {Event|String} event Instance of Event or event type string if 'eventObject' used - * @param {*} [eventObject] For Internet Explorer 8 required event object on this argument - * @return {Boolean} If 'preventDefault' was called the value is false, else the value is true. - */ - function dispatchEvent(event, eventObject) { - var eventType = ('' + (typeof event === "string" ? event : event.type)).replace(/^on/, ''); - var list = eventsList[eventType]; - if (list) { - // need to understand that there is one object of Event - eventObject = typeof event === "string" ? eventObject : event; - if (eventObject.target == null) { - // need to override some of the properties of the Event object - for(var props = ['target', 'currentTarget', 'srcElement', 'type']; event = props.pop();) { - // use 'redefineProperty' to override the properties - eventObject = redefineProperty(eventObject, event, { - get: event === 'type' ? function() { - return eventType; - } : function() { - return window; - } - }); - } - } - // run function defined in the attributes 'onpopstate/onhashchange' in the 'window' context - ((eventType === 'popstate' ? window.onpopstate : window.onhashchange) - || emptyFunction).call(window, eventObject); - // run other functions that are in the list of handlers - for(var i = 0, len = list.length; i < len; i++) { - list[i].call(window, eventObject); - } - return true; - } else { - return dispatch(event, eventObject); - } - } - - /** - * dispatch current state event - */ - function firePopState() { - var o = document.createEvent ? document.createEvent('Event') : document.createEventObject(); - if (o.initEvent) { - o.initEvent('popstate', false, false); - } else { - o.type = 'popstate'; - } - o.state = historyObject.state; - // send a newly created events to be processed - dispatchEvent(o); - } - - /** - * fire initial state for non-HTML5 browsers - */ - function fireInitialState() { - if (isFireInitialState) { - isFireInitialState = false; - firePopState(); - } - } - - /** - * Change the data of the current history for HTML4 browsers - * - * @param {Object} state - * @param {string} [url] - * @param {Boolean} [replace] - * @param {string} [lastURLValue] - * @return void - */ - function changeState(state, url, replace, lastURLValue) { - if (!isSupportHistoryAPI) { - // normalization url - var urlObject = parseURL(url); - // if current url not equal new url - if (urlObject._relative !== parseURL()._relative) { - // if empty lastURLValue to skip hash change event - lastURL = lastURLValue; - if (replace) { - // only replace hash, not store to history - windowLocation.replace("#" + urlObject._special); - } else { - // change hash and add new record to history - windowLocation.hash = urlObject._special; - } - } - } - if (!isSupportStateObjectInHistory && state) { - stateStorage[windowLocation.href] = state; - } - isFireInitialState = false; - } - - /** - * Event handler function changes the hash in the address bar - * - * @param {Event} event - * @return void - */ - function onHashChange(event) { - // if not empty lastURL, otherwise skipped the current handler event - if (lastURL) { - // if checkUrlForPopState equal current url, this means that the event was raised popstate browser - if (checkUrlForPopState !== windowLocation.href) { - // otherwise, - // the browser does not support popstate event or just does not run the event by changing the hash. - firePopState(); - } - // current event object - event = event || window.event; - - var oldURLObject = parseURL(lastURL, true); - var newURLObject = parseURL(); - // HTML4 browser not support properties oldURL/newURL - if (!event.oldURL) { - event.oldURL = oldURLObject._href; - event.newURL = newURLObject._href; - } - if (oldURLObject._hash !== newURLObject._hash) { - // if current hash not equal previous hash - dispatchEvent(event); - } - } - // new value to lastURL - lastURL = windowLocation.href; - } - - /** - * The event handler is fully loaded document - * - * @param {*} [noScroll] - * @return void - */ - function onLoad(noScroll) { - // Get rid of the events popstate when the first loading a document in the webkit browsers - setTimeout(function() { - // hang up the event handler for the built-in popstate event in the browser - addEvent('popstate', function(e) { - // set the current url, that suppress the creation of the popstate event by changing the hash - checkUrlForPopState = windowLocation.href; - // for Safari browser in OS Windows not implemented 'state' object in 'History' interface - // and not implemented in old HTML4 browsers - if (!isSupportStateObjectInHistory) { - e = redefineProperty(e, 'state', {get: function() { - return historyObject.state; - }}); - } - // send events to be processed - dispatchEvent(e); - }, false); - }, 0); - // for non-HTML5 browsers - if (!isSupportHistoryAPI && noScroll !== true && historyObject.location) { - // scroll window to anchor element - scrollToAnchorId(historyObject.location.hash); - // fire initial state for non-HTML5 browser after load page - fireInitialState(); - } - } - - /** - * Finds the closest ancestor anchor element (including the target itself). - * - * @param {HTMLElement} target The element to start scanning from. - * @return {HTMLElement} An element which is the closest ancestor anchor. - */ - function anchorTarget(target) { - while (target) { - if (target.nodeName === 'A') return target; - target = target.parentNode; - } - } - - /** - * Handles anchor elements with a hash fragment for non-HTML5 browsers - * - * @param {Event} e - */ - function onAnchorClick(e) { - var event = e || window.event; - var target = anchorTarget(event.target || event.srcElement); - var defaultPrevented = "defaultPrevented" in event ? event['defaultPrevented'] : event.returnValue === false; - if (target && target.nodeName === "A" && !defaultPrevented) { - var current = parseURL(); - var expect = parseURL(target.getAttribute("href", 2)); - var isEqualBaseURL = current._href.split('#').shift() === expect._href.split('#').shift(); - if (isEqualBaseURL && expect._hash) { - if (current._hash !== expect._hash) { - historyObject.location.hash = expect._hash; - } - scrollToAnchorId(expect._hash); - if (event.preventDefault) { - event.preventDefault(); - } else { - event.returnValue = false; - } - } - } - } - - /** - * Scroll page to current anchor in url-hash - * - * @param hash - */ - function scrollToAnchorId(hash) { - var target = document.getElementById(hash = (hash || '').replace(/^#/, '')); - if (target && target.id === hash && target.nodeName === "A") { - var rect = target.getBoundingClientRect(); - window.scrollTo((documentElement.scrollLeft || 0), rect.top + (documentElement.scrollTop || 0) - - (documentElement.clientTop || 0)); - } - } - - /** - * Library initialization - * - * @return {Boolean} return true if all is well, otherwise return false value - */ - function initialize() { - /** - * Get custom settings from the query string - */ - var scripts = document.getElementsByTagName('script'); - var src = (scripts[scripts.length - 1] || {}).src || ''; - var arg = src.indexOf('?') !== -1 ? src.split('?').pop() : ''; - arg.replace(/(\w+)(?:=([^&]*))?/g, function(a, key, value) { - settings[key] = (value || (key === 'basepath' ? '/' : '')).replace(/^(0|false)$/, ''); - }); - - /** - * sessionStorage throws error when cookies are disabled - * Chrome content settings when running the site in a Facebook IFrame. - * see: https://github.com/devote/HTML5-History-API/issues/34 - */ - try { - sessionStorage = window['sessionStorage']; - } catch(_e_) {} - - /** - * hang up the event handler to listen to the events hashchange - */ - addEvent(eventNamePrefix + 'hashchange', onHashChange, false); - - // a list of objects with pairs of descriptors/object - var data = [locationDescriptors, locationObject, eventsDescriptors, window, historyDescriptors, historyObject]; - - // if browser support object 'state' in interface 'History' - if (isSupportStateObjectInHistory) { - // remove state property from descriptor - delete historyDescriptors['state']; - } - - // initializing descriptors - for(var i = 0; i < data.length; i += 2) { - for(var prop in data[i]) { - if (data[i].hasOwnProperty(prop)) { - if (typeof data[i][prop] === 'function') { - // If the descriptor is a simple function, simply just assign it an object - data[i + 1][prop] = data[i][prop]; - } else { - // prepare the descriptor the required format - var descriptor = prepareDescriptorsForObject(data[i], prop, data[i][prop]); - // try to set the descriptor object - if (!redefineProperty(data[i + 1], prop, descriptor, function(n, o) { - // is satisfied if the failed override property - if (o === historyObject) { - // the problem occurs in Safari on the Mac - window.history = historyObject = data[i + 1] = n; - } - })) { - // if there is no possibility override. - // This browser does not support descriptors, such as IE7 - - // remove previously hung event handlers - removeEvent(eventNamePrefix + 'hashchange', onHashChange, false); - - // fail to initialize :( - return false; - } - - // create a repository for custom handlers onpopstate/onhashchange - if (data[i + 1] === window) { - eventsList[prop] = eventsList[prop.substr(2)] = []; - } - } - } - } - } - - // redirect if necessary - if (settings['redirect']) { - historyObject['redirect'](); - } - - // If browser does not support object 'state' in interface 'History' - if (!isSupportStateObjectInHistory && JSON) { - storageInitialize(); - } - - // track clicks on anchors - if (!isSupportHistoryAPI) { - document[addEventListenerName](eventNamePrefix + "click", onAnchorClick, false); - } - - if (document.readyState === 'complete') { - onLoad(true); - } else { - if (!isSupportHistoryAPI && parseURL()._relative !== settings["basepath"]) { - isFireInitialState = true; - } - /** - * Need to avoid triggering events popstate the initial page load. - * Hang handler popstate as will be fully loaded document that - * would prevent triggering event onpopstate - */ - addEvent(eventNamePrefix + 'load', onLoad, false); - } - - // everything went well - return true; - } - - /** - * Starting the library - */ - if (!initialize()) { - // if unable to initialize descriptors - // therefore quite old browser and there - // is no sense to continue to perform - return; - } - - /** - * If the property history.emulate will be true, - * this will be talking about what's going on - * emulation capabilities HTML5-History-API. - * Otherwise there is no emulation, ie the - * built-in browser capabilities. - * - * @type {boolean} - * @const - */ - historyObject['emulate'] = !isSupportHistoryAPI; - - /** - * Replace the original methods on the wrapper - */ - window[addEventListenerName] = addEventListener; - window[removeEventListenerName] = removeEventListener; - window[dispatchEventName] = dispatchEvent; - -})(window); \ No newline at end of file diff --git a/lib/signals.js b/lib/signals.js deleted file mode 100644 index f1ea6f2..0000000 --- a/lib/signals.js +++ /dev/null @@ -1,425 +0,0 @@ -/*jslint indent:4, white:true, nomen:true, plusplus:true */ -/*global define:false, require:false, exports:false, module:false, signals:false */ - -/** @license - * JS Signals - * Released under the MIT license - * Author: Miller Medeiros - * Version: 0.8.1 - Build: 266 (2012/07/31 03:33 PM) - */ - -var Signal = (function(global){ - - // SignalBinding ------------------------------------------------- - //================================================================ - - /** - * Object that represents a binding between a Signal and a listener function. - *
- This is an internal constructor and shouldn't be called by regular users. - *
- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes. - * @author Miller Medeiros - * @constructor - * @internal - * @name SignalBinding - * @param {Signal} signal Reference to Signal object that listener is currently bound to. - * @param {Function} listener Handler function bound to the signal. - * @param {boolean} isOnce If binding should be executed just once. - * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @param {Number} [priority] The priority level of the event listener. (default = 0). - */ - function SignalBinding(signal, listener, isOnce, listenerContext, priority) { - - /** - * Handler function bound to the signal. - * @type Function - * @private - */ - this._listener = listener; - - /** - * If binding should be executed just once. - * @type boolean - * @private - */ - this._isOnce = isOnce; - - /** - * Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @memberOf SignalBinding.prototype - * @name context - * @type Object|undefined|null - */ - this.context = listenerContext; - - /** - * Reference to Signal object that listener is currently bound to. - * @type Signal - * @private - */ - this._signal = signal; - - /** - * Listener priority - * @type Number - * @private - */ - this._priority = priority || 0; - } - - SignalBinding.prototype = { - - /** - * If binding is active and should be executed. - * @type boolean - */ - active : true, - - /** - * Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute`. (curried parameters) - * @type Array|null - */ - params : null, - - /** - * Call listener passing arbitrary parameters. - *

If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch.

- * @param {Array} [paramsArr] Array of parameters that should be passed to the listener - * @return {*} Value returned by the listener. - */ - execute : function (paramsArr) { - var handlerReturn, params; - if (this.active && !!this._listener) { - params = this.params? this.params.concat(paramsArr) : paramsArr; - handlerReturn = this._listener.apply(this.context, params); - if (this._isOnce) { - this.detach(); - } - } - return handlerReturn; - }, - - /** - * Detach binding from signal. - * - alias to: mySignal.remove(myBinding.getListener()); - * @return {Function|null} Handler function bound to the signal or `null` if binding was previously detached. - */ - detach : function () { - return this.isBound()? this._signal.remove(this._listener, this.context) : null; - }, - - /** - * @return {Boolean} `true` if binding is still bound to the signal and have a listener. - */ - isBound : function () { - return (!!this._signal && !!this._listener); - }, - - /** - * @return {Function} Handler function bound to the signal. - */ - getListener : function () { - return this._listener; - }, - - /** - * Delete instance properties - * @private - */ - _destroy : function () { - delete this._signal; - delete this._listener; - delete this.context; - }, - - /** - * @return {boolean} If SignalBinding will only be executed once. - */ - isOnce : function () { - return this._isOnce; - }, - - /** - * @return {string} String representation of the object. - */ - toString : function () { - return '[SignalBinding isOnce:' + this._isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']'; - } - - }; - - -/*global SignalBinding:false*/ - - // Signal -------------------------------------------------------- - //================================================================ - - function validateListener(listener, fnName) { - if (typeof listener !== 'function') { - throw new Error( 'listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName) ); - } - } - - /** - * Custom event broadcaster - *
- inspired by Robert Penner's AS3 Signals. - * @name Signal - * @author Miller Medeiros - * @constructor - */ - function Signal() { - /** - * @type Array. - * @private - */ - this._bindings = []; - this._prevParams = null; - } - - Signal.prototype = { - - /** - * Signals Version Number - * @type String - * @const - */ - VERSION : '0.8.1', - - /** - * If Signal should keep record of previously dispatched parameters and - * automatically execute listener during `add()`/`addOnce()` if Signal was - * already dispatched before. - * @type boolean - */ - memorize : false, - - /** - * @type boolean - * @private - */ - _shouldPropagate : true, - - /** - * If Signal is active and should broadcast events. - *

IMPORTANT: Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead.

- * @type boolean - */ - active : true, - - /** - * @param {Function} listener - * @param {boolean} isOnce - * @param {Object} [listenerContext] - * @param {Number} [priority] - * @return {SignalBinding} - * @private - */ - _registerListener : function (listener, isOnce, listenerContext, priority) { - - var prevIndex = this._indexOfListener(listener, listenerContext), - binding; - - if (prevIndex !== -1) { - binding = this._bindings[prevIndex]; - if (binding.isOnce() !== isOnce) { - throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.'); - } - } else { - binding = new SignalBinding(this, listener, isOnce, listenerContext, priority); - this._addBinding(binding); - } - - if(this.memorize && this._prevParams){ - binding.execute(this._prevParams); - } - - return binding; - }, - - /** - * @param {SignalBinding} binding - * @private - */ - _addBinding : function (binding) { - //simplified insertion sort - var n = this._bindings.length; - do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); - this._bindings.splice(n + 1, 0, binding); - }, - - /** - * @param {Function} listener - * @return {number} - * @private - */ - _indexOfListener : function (listener, context) { - var n = this._bindings.length, - cur; - while (n--) { - cur = this._bindings[n]; - if (cur._listener === listener && cur.context === context) { - return n; - } - } - return -1; - }, - - /** - * Check if listener was attached to Signal. - * @param {Function} listener - * @param {Object} [context] - * @return {boolean} if Signal has the specified listener. - */ - has : function (listener, context) { - return this._indexOfListener(listener, context) !== -1; - }, - - /** - * Add a listener to the signal. - * @param {Function} listener Signal handler function. - * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) - * @return {SignalBinding} An Object representing the binding between the Signal and listener. - */ - add : function (listener, listenerContext, priority) { - validateListener(listener, 'add'); - return this._registerListener(listener, false, listenerContext, priority); - }, - - /** - * Add listener to the signal that should be removed after first execution (will be executed only once). - * @param {Function} listener Signal handler function. - * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) - * @return {SignalBinding} An Object representing the binding between the Signal and listener. - */ - addOnce : function (listener, listenerContext, priority) { - validateListener(listener, 'addOnce'); - return this._registerListener(listener, true, listenerContext, priority); - }, - - /** - * Remove a single listener from the dispatch queue. - * @param {Function} listener Handler function that should be removed. - * @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context). - * @return {Function} Listener handler function. - */ - remove : function (listener, context) { - validateListener(listener, 'remove'); - - var i = this._indexOfListener(listener, context); - if (i !== -1) { - this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal - this._bindings.splice(i, 1); - } - return listener; - }, - - /** - * Remove all listeners from the Signal. - */ - removeAll : function () { - var n = this._bindings.length; - while (n--) { - this._bindings[n]._destroy(); - } - this._bindings.length = 0; - }, - - /** - * @return {number} Number of listeners attached to the Signal. - */ - getNumListeners : function () { - return this._bindings.length; - }, - - /** - * Stop propagation of the event, blocking the dispatch to next listeners on the queue. - *

IMPORTANT: should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.

- * @see Signal.prototype.disable - */ - halt : function () { - this._shouldPropagate = false; - }, - - /** - * Dispatch/Broadcast Signal to all listeners added to the queue. - * @param {...*} [params] Parameters that should be passed to each handler. - */ - dispatch : function (params) { - if (! this.active) { - return; - } - - var paramsArr = Array.prototype.slice.call(arguments), - n = this._bindings.length, - bindings; - - if (this.memorize) { - this._prevParams = paramsArr; - } - - if (! n) { - //should come after memorize - return; - } - - bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch - this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. - - //execute all callbacks until end of the list or until a callback returns `false` or stops propagation - //reverse loop since listeners with higher priority will be added at the end of the list - do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); - }, - - /** - * Forget memorized arguments. - * @see Signal.memorize - */ - forget : function(){ - this._prevParams = null; - }, - - /** - * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). - *

IMPORTANT: calling any method on the signal instance after calling dispose will throw errors.

- */ - dispose : function () { - this.removeAll(); - delete this._bindings; - delete this._prevParams; - }, - - /** - * @return {string} String representation of the object. - */ - toString : function () { - return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']'; - } - - }; - - - // Namespace ----------------------------------------------------- - //================================================================ - - /** - * Signals namespace - * @namespace - * @name signals - */ - var signals = Signal; - - /** - * Custom event broadcaster - * @see Signal - */ - // alias for backwards compatibility (see #gh-44) - signals.Signal = Signal; - - - global['signals'] = signals; - - - return Signal; - -}(window)); \ No newline at end of file diff --git a/lib/when.js b/lib/when.js deleted file mode 100644 index 4d047d6..0000000 --- a/lib/when.js +++ /dev/null @@ -1,926 +0,0 @@ -/** @license MIT License (c) copyright 2011-2013 original author or authors */ - -/** - * A lightweight CommonJS Promises/A and when() implementation - * when is part of the cujo.js family of libraries (http://cujojs.com/) - * - * Licensed under the MIT License at: - * http://www.opensource.org/licenses/mit-license.php - * - * @author Brian Cavalier - * @author John Hann - * @version 2.5.1 - */ -var when = (function(global) { 'use strict'; - - var require; - - // Public API - - when.promise = promise; // Create a pending promise - when.resolve = resolve; // Create a resolved promise - when.reject = reject; // Create a rejected promise - when.defer = defer; // Create a {promise, resolver} pair - - when.join = join; // Join 2 or more promises - - when.all = all; // Resolve a list of promises - when.map = map; // Array.map() for promises - when.reduce = reduce; // Array.reduce() for promises - when.settle = settle; // Settle a list of promises - - when.any = any; // One-winner race - when.some = some; // Multi-winner race - - when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike - when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable - - /** - * Register an observer for a promise or immediate value. - * - * @param {*} promiseOrValue - * @param {function?} [onFulfilled] callback to be called when promiseOrValue is - * successfully fulfilled. If promiseOrValue is an immediate value, callback - * will be invoked immediately. - * @param {function?} [onRejected] callback to be called when promiseOrValue is - * rejected. - * @param {function?} [onProgress] callback to be called when progress updates - * are issued for promiseOrValue. - * @returns {Promise} a new {@link Promise} that will complete with the return - * value of callback or errback or the completion value of promiseOrValue if - * callback and/or errback is not supplied. - */ - function when(promiseOrValue, onFulfilled, onRejected, onProgress) { - // Get a trusted promise for the input promiseOrValue, and then - // register promise handlers - return cast(promiseOrValue).then(onFulfilled, onRejected, onProgress); - } - - function cast(x) { - return x instanceof Promise ? x : resolve(x); - } - - /** - * Trusted Promise constructor. A Promise created from this constructor is - * a trusted when.js promise. Any other duck-typed promise is considered - * untrusted. - * @constructor - * @param {function} sendMessage function to deliver messages to the promise's handler - * @param {function?} inspect function that reports the promise's state - * @name Promise - */ - function Promise(sendMessage, inspect) { - this._message = sendMessage; - this.inspect = inspect; - } - - Promise.prototype = { - /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise - */ - then: function(onFulfilled, onRejected, onProgress) { - /*jshint unused:false*/ - var args, sendMessage; - - args = arguments; - sendMessage = this._message; - - return _promise(function(resolve, reject, notify) { - sendMessage('when', args, resolve, notify); - }, this._status && this._status.observed()); - }, - - /** - * Register a rejection handler. Shortcut for .then(undefined, onRejected) - * @param {function?} onRejected - * @return {Promise} - */ - otherwise: function(onRejected) { - return this.then(undef, onRejected); - }, - - /** - * Ensures that onFulfilledOrRejected will be called regardless of whether - * this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT - * receive the promises' value or reason. Any returned value will be disregarded. - * onFulfilledOrRejected may throw or return a rejected promise to signal - * an additional error. - * @param {function} onFulfilledOrRejected handler to be called regardless of - * fulfillment or rejection - * @returns {Promise} - */ - ensure: function(onFulfilledOrRejected) { - return typeof onFulfilledOrRejected === 'function' - ? this.then(injectHandler, injectHandler)['yield'](this) - : this; - - function injectHandler() { - return resolve(onFulfilledOrRejected()); - } - }, - - /** - * Shortcut for .then(function() { return value; }) - * @param {*} value - * @return {Promise} a promise that: - * - is fulfilled if value is not a promise, or - * - if value is a promise, will fulfill with its value, or reject - * with its reason. - */ - 'yield': function(value) { - return this.then(function() { - return value; - }); - }, - - /** - * Runs a side effect when this promise fulfills, without changing the - * fulfillment value. - * @param {function} onFulfilledSideEffect - * @returns {Promise} - */ - tap: function(onFulfilledSideEffect) { - return this.then(onFulfilledSideEffect)['yield'](this); - }, - - /** - * Assumes that this promise will fulfill with an array, and arranges - * for the onFulfilled to be called with the array as its argument list - * i.e. onFulfilled.apply(undefined, array). - * @param {function} onFulfilled function to receive spread arguments - * @return {Promise} - */ - spread: function(onFulfilled) { - return this.then(function(array) { - // array may contain promises, so resolve its contents. - return all(array, function(array) { - return onFulfilled.apply(undef, array); - }); - }); - }, - - /** - * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected) - * @deprecated - */ - always: function(onFulfilledOrRejected, onProgress) { - return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); - } - }; - - /** - * Returns a resolved promise. The returned promise will be - * - fulfilled with promiseOrValue if it is a value, or - * - if promiseOrValue is a promise - * - fulfilled with promiseOrValue's value after it is fulfilled - * - rejected with promiseOrValue's reason after it is rejected - * @param {*} value - * @return {Promise} - */ - function resolve(value) { - return promise(function(resolve) { - resolve(value); - }); - } - - /** - * Returns a rejected promise for the supplied promiseOrValue. The returned - * promise will be rejected with: - * - promiseOrValue, if it is a value, or - * - if promiseOrValue is a promise - * - promiseOrValue's value after it is fulfilled - * - promiseOrValue's reason after it is rejected - * @param {*} promiseOrValue the rejected value of the returned {@link Promise} - * @return {Promise} rejected {@link Promise} - */ - function reject(promiseOrValue) { - return when(promiseOrValue, rejected); - } - - /** - * Creates a {promise, resolver} pair, either or both of which - * may be given out safely to consumers. - * The resolver has resolve, reject, and progress. The promise - * has then plus extended promise API. - * - * @return {{ - * promise: Promise, - * resolve: function:Promise, - * reject: function:Promise, - * notify: function:Promise - * resolver: { - * resolve: function:Promise, - * reject: function:Promise, - * notify: function:Promise - * }}} - */ - function defer() { - var deferred, pending, resolved; - - // Optimize object shape - deferred = { - promise: undef, resolve: undef, reject: undef, notify: undef, - resolver: { resolve: undef, reject: undef, notify: undef } - }; - - deferred.promise = pending = promise(makeDeferred); - - return deferred; - - function makeDeferred(resolvePending, rejectPending, notifyPending) { - deferred.resolve = deferred.resolver.resolve = function(value) { - if(resolved) { - return resolve(value); - } - resolved = true; - resolvePending(value); - return pending; - }; - - deferred.reject = deferred.resolver.reject = function(reason) { - if(resolved) { - return resolve(rejected(reason)); - } - resolved = true; - rejectPending(reason); - return pending; - }; - - deferred.notify = deferred.resolver.notify = function(update) { - notifyPending(update); - return update; - }; - } - } - - /** - * Creates a new promise whose fate is determined by resolver. - * @param {function} resolver function(resolve, reject, notify) - * @returns {Promise} promise whose fate is determine by resolver - */ - function promise(resolver) { - return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); - } - - /** - * Creates a new promise, linked to parent, whose fate is determined - * by resolver. - * @param {function} resolver function(resolve, reject, notify) - * @param {Promise?} status promise from which the new promise is begotten - * @returns {Promise} promise whose fate is determine by resolver - * @private - */ - function _promise(resolver, status) { - var self, value, consumers = []; - - self = new Promise(_message, inspect); - self._status = status; - - // Call the provider resolver to seal the promise's fate - try { - resolver(promiseResolve, promiseReject, promiseNotify); - } catch(e) { - promiseReject(e); - } - - // Return the promise - return self; - - /** - * Private message delivery. Queues and delivers messages to - * the promise's ultimate fulfillment value or rejection reason. - * @private - * @param {String} type - * @param {Array} args - * @param {Function} resolve - * @param {Function} notify - */ - function _message(type, args, resolve, notify) { - consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); - - function deliver(p) { - p._message(type, args, resolve, notify); - } - } - - /** - * Returns a snapshot of the promise's state at the instant inspect() - * is called. The returned object is not live and will not update as - * the promise's state changes. - * @returns {{ state:String, value?:*, reason?:* }} status snapshot - * of the promise. - */ - function inspect() { - return value ? value.inspect() : toPendingState(); - } - - /** - * Transition from pre-resolution state to post-resolution state, notifying - * all listeners of the ultimate fulfillment or rejection - * @param {*|Promise} val resolution value - */ - function promiseResolve(val) { - if(!consumers) { - return; - } - - var queue = consumers; - consumers = undef; - - enqueue(function () { - value = coerce(self, val); - if(status) { - updateStatus(value, status); - } - runHandlers(queue, value); - }); - - } - - /** - * Reject this promise with the supplied reason, which will be used verbatim. - * @param {*} reason reason for the rejection - */ - function promiseReject(reason) { - promiseResolve(rejected(reason)); - } - - /** - * Issue a progress event, notifying all progress listeners - * @param {*} update progress event payload to pass to all listeners - */ - function promiseNotify(update) { - if(consumers) { - var queue = consumers; - enqueue(function () { - runHandlers(queue, progressed(update)); - }); - } - } - } - - /** - * Run a queue of functions as quickly as possible, passing - * value to each. - */ - function runHandlers(queue, value) { - for (var i = 0; i < queue.length; i++) { - queue[i](value); - } - } - - /** - * Creates a fulfilled, local promise as a proxy for a value - * NOTE: must never be exposed - * @param {*} value fulfillment value - * @returns {Promise} - */ - function fulfilled(value) { - return near( - new NearFulfilledProxy(value), - function() { return toFulfilledState(value); } - ); - } - - /** - * Creates a rejected, local promise with the supplied reason - * NOTE: must never be exposed - * @param {*} reason rejection reason - * @returns {Promise} - */ - function rejected(reason) { - return near( - new NearRejectedProxy(reason), - function() { return toRejectedState(reason); } - ); - } - - /** - * Creates a near promise using the provided proxy - * NOTE: must never be exposed - * @param {object} proxy proxy for the promise's ultimate value or reason - * @param {function} inspect function that returns a snapshot of the - * returned near promise's state - * @returns {Promise} - */ - function near(proxy, inspect) { - return new Promise(function (type, args, resolve) { - try { - resolve(proxy[type].apply(proxy, args)); - } catch(e) { - resolve(rejected(e)); - } - }, inspect); - } - - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressed(update) { - return new Promise(function (type, args, _, notify) { - var onProgress = args[2]; - try { - notify(typeof onProgress === 'function' ? onProgress(update) : update); - } catch(e) { - notify(e); - } - }); - } - - /** - * Coerces x to a trusted Promise - * @param {*} x thing to coerce - * @returns {*} Guaranteed to return a trusted Promise. If x - * is trusted, returns x, otherwise, returns a new, trusted, already-resolved - * Promise whose resolution value is: - * * the resolution value of x if it's a foreign promise, or - * * x if it's a value - */ - function coerce(self, x) { - if (x === self) { - return rejected(new TypeError()); - } - - if (x instanceof Promise) { - return x; - } - - try { - var untrustedThen = x === Object(x) && x.then; - - return typeof untrustedThen === 'function' - ? assimilate(untrustedThen, x) - : fulfilled(x); - } catch(e) { - return rejected(e); - } - } - - /** - * Safely assimilates a foreign thenable by wrapping it in a trusted promise - * @param {function} untrustedThen x's then() method - * @param {object|function} x thenable - * @returns {Promise} - */ - function assimilate(untrustedThen, x) { - return promise(function (resolve, reject) { - fcall(untrustedThen, x, resolve, reject); - }); - } - - /** - * Proxy for a near, fulfilled value - * @param {*} value - * @constructor - */ - function NearFulfilledProxy(value) { - this.value = value; - } - - NearFulfilledProxy.prototype.when = function(onResult) { - return typeof onResult === 'function' ? onResult(this.value) : this.value; - }; - - /** - * Proxy for a near rejection - * @param {*} reason - * @constructor - */ - function NearRejectedProxy(reason) { - this.reason = reason; - } - - NearRejectedProxy.prototype.when = function(_, onError) { - if(typeof onError === 'function') { - return onError(this.reason); - } else { - throw this.reason; - } - }; - - function updateStatus(value, status) { - value.then(statusFulfilled, statusRejected); - - function statusFulfilled() { status.fulfilled(); } - function statusRejected(r) { status.rejected(r); } - } - - /** - * Determines if x is promise-like, i.e. a thenable object - * NOTE: Will return true for *any thenable object*, and isn't truly - * safe, since it may attempt to access the `then` property of x (i.e. - * clever/malicious getters may do weird things) - * @param {*} x anything - * @returns {boolean} true if x is promise-like - */ - function isPromiseLike(x) { - return x && typeof x.then === 'function'; - } - - /** - * Initiates a competitive race, returning a promise that will resolve when - * howMany of the supplied promisesOrValues have resolved, or will reject when - * it becomes impossible for howMany to resolve, for example, when - * (promisesOrValues.length - howMany) + 1 input promises reject. - * - * @param {Array} promisesOrValues array of anything, may contain a mix - * of promises and values - * @param howMany {number} number of promisesOrValues to resolve - * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() - * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() - * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() - * @returns {Promise} promise that will resolve to an array of howMany values that - * resolved first, or will reject with an array of - * (promisesOrValues.length - howMany) + 1 rejection reasons. - */ - function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { - - return when(promisesOrValues, function(promisesOrValues) { - - return promise(resolveSome).then(onFulfilled, onRejected, onProgress); - - function resolveSome(resolve, reject, notify) { - var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i; - - len = promisesOrValues.length >>> 0; - - toResolve = Math.max(0, Math.min(howMany, len)); - values = []; - - toReject = (len - toResolve) + 1; - reasons = []; - - // No items in the input, resolve immediately - if (!toResolve) { - resolve(values); - - } else { - rejectOne = function(reason) { - reasons.push(reason); - if(!--toReject) { - fulfillOne = rejectOne = identity; - reject(reasons); - } - }; - - fulfillOne = function(val) { - // This orders the values based on promise resolution order - values.push(val); - if (!--toResolve) { - fulfillOne = rejectOne = identity; - resolve(values); - } - }; - - for(i = 0; i < len; ++i) { - if(i in promisesOrValues) { - when(promisesOrValues[i], fulfiller, rejecter, notify); - } - } - } - - function rejecter(reason) { - rejectOne(reason); - } - - function fulfiller(val) { - fulfillOne(val); - } - } - }); - } - - /** - * Initiates a competitive race, returning a promise that will resolve when - * any one of the supplied promisesOrValues has resolved or will reject when - * *all* promisesOrValues have rejected. - * - * @param {Array|Promise} promisesOrValues array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() - * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() - * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() - * @returns {Promise} promise that will resolve to the value that resolved first, or - * will reject with an array of all rejected inputs. - */ - function any(promisesOrValues, onFulfilled, onRejected, onProgress) { - - function unwrapSingleResult(val) { - return onFulfilled ? onFulfilled(val[0]) : val[0]; - } - - return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress); - } - - /** - * Return a promise that will resolve only once all the supplied promisesOrValues - * have resolved. The resolution value of the returned promise will be an array - * containing the resolution values of each of the promisesOrValues. - * @memberOf when - * - * @param {Array|Promise} promisesOrValues array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() - * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() - * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() - * @returns {Promise} - */ - function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); - } - - /** - * Joins multiple promises into a single returned promise. - * @return {Promise} a promise that will fulfill when *all* the input promises - * have fulfilled, or will reject when *any one* of the input promises rejects. - */ - function join(/* ...promises */) { - return _map(arguments, identity); - } - - /** - * Settles all input promises such that they are guaranteed not to - * be pending once the returned promise fulfills. The returned promise - * will always fulfill, except in the case where `array` is a promise - * that rejects. - * @param {Array|Promise} array or promise for array of promises to settle - * @returns {Promise} promise that always fulfills with an array of - * outcome snapshots for each input promise. - */ - function settle(array) { - return _map(array, toFulfilledState, toRejectedState); - } - - /** - * Promise-aware array map function, similar to `Array.prototype.map()`, - * but input array may contain promises or values. - * @param {Array|Promise} array array of anything, may contain promises and values - * @param {function} mapFunc map function which may return a promise or value - * @returns {Promise} promise that will fulfill with an array of mapped values - * or reject if any input promise rejects. - */ - function map(array, mapFunc) { - return _map(array, mapFunc); - } - - /** - * Internal map that allows a fallback to handle rejections - * @param {Array|Promise} array array of anything, may contain promises and values - * @param {function} mapFunc map function which may return a promise or value - * @param {function?} fallback function to handle rejected promises - * @returns {Promise} promise that will fulfill with an array of mapped values - * or reject if any input promise rejects. - */ - function _map(array, mapFunc, fallback) { - return when(array, function(array) { - - return _promise(resolveMap); - - function resolveMap(resolve, reject, notify) { - var results, len, toResolve, i; - - // Since we know the resulting length, we can preallocate the results - // array to avoid array expansions. - toResolve = len = array.length >>> 0; - results = []; - - if(!toResolve) { - resolve(results); - return; - } - - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } - } - - function resolveOne(item, i) { - when(item, mapFunc, fallback).then(function(mapped) { - results[i] = mapped; - - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - } - } - }); - } - - /** - * Traditional reduce function, similar to `Array.prototype.reduce()`, but - * input may contain promises and/or values, and reduceFunc - * may return either a value or a promise, *and* initialValue may - * be a promise for the starting value. - * - * @param {Array|Promise} promise array or promise for an array of anything, - * may contain a mix of promises and values. - * @param {function} reduceFunc reduce function reduce(currentValue, nextValue, index, total), - * where total is the total number of items being reduced, and will be the same - * in each call to reduceFunc. - * @returns {Promise} that will resolve to the final reduced value - */ - function reduce(promise, reduceFunc /*, initialValue */) { - var args = fcall(slice, arguments, 1); - - return when(promise, function(array) { - var total; - - total = array.length; - - // Wrap the supplied reduceFunc with one that handles promises and then - // delegates to the supplied. - args[0] = function (current, val, i) { - return when(current, function (c) { - return when(val, function (value) { - return reduceFunc(c, value, i, total); - }); - }); - }; - - return reduceArray.apply(array, args); - }); - } - - // Snapshot states - - /** - * Creates a fulfilled state snapshot - * @private - * @param {*} x any value - * @returns {{state:'fulfilled',value:*}} - */ - function toFulfilledState(x) { - return { state: 'fulfilled', value: x }; - } - - /** - * Creates a rejected state snapshot - * @private - * @param {*} x any reason - * @returns {{state:'rejected',reason:*}} - */ - function toRejectedState(x) { - return { state: 'rejected', reason: x }; - } - - /** - * Creates a pending state snapshot - * @private - * @returns {{state:'pending'}} - */ - function toPendingState() { - return { state: 'pending' }; - } - - // - // Internals, utilities, etc. - // - - var reduceArray, slice, fcall, nextTick, handlerQueue, - setTimeout, funcProto, call, arrayProto, monitorApi, - cjsRequire, MutationObserver, undef; - - cjsRequire = require; - - // - // Shared handler queue processing - // - // Credit to Twisol (https://github.com/Twisol) for suggesting - // this type of extensible queue + trampoline approach for - // next-tick conflation. - - handlerQueue = []; - - /** - * Enqueue a task. If the queue is not currently scheduled to be - * drained, schedule it. - * @param {function} task - */ - function enqueue(task) { - if(handlerQueue.push(task) === 1) { - nextTick(drainQueue); - } - } - - /** - * Drain the handler queue entirely, being careful to allow the - * queue to be extended while it is being processed, and to continue - * processing until it is truly empty. - */ - function drainQueue() { - runHandlers(handlerQueue); - handlerQueue = []; - } - - // capture setTimeout to avoid being caught by fake timers - // used in time based tests - setTimeout = global.setTimeout; - - // Allow attaching the monitor to when() if env has no console - monitorApi = typeof console != 'undefined' ? console : when; - - // Sniff "best" async scheduling option - // Prefer process.nextTick or MutationObserver, then check for - // vertx and finally fall back to setTimeout - /*global process*/ - if (typeof process === 'object' && process.nextTick) { - nextTick = process.nextTick; - } else if(MutationObserver = global.MutationObserver || global.WebKitMutationObserver) { - nextTick = (function(document, MutationObserver, drainQueue) { - var el = document.createElement('div'); - new MutationObserver(drainQueue).observe(el, { attributes: true }); - - return function() { - el.setAttribute('x', 'x'); - }; - }(document, MutationObserver, drainQueue)); - } else { - try { - // vert.x 1.x || 2.x - nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; - } catch(ignore) { - nextTick = function(t) { setTimeout(t, 0); }; - } - } - - // - // Capture/polyfill function and array utils - // - - // Safe function calls - funcProto = Function.prototype; - call = funcProto.call; - fcall = funcProto.bind - ? call.bind(call) - : function(f, context) { - return f.apply(context, slice.call(arguments, 2)); - }; - - // Safe array ops - arrayProto = []; - slice = arrayProto.slice; - - // ES5 reduce implementation if native not available - // See: http://es5.github.com/#x15.4.4.21 as there are many - // specifics and edge cases. ES5 dictates that reduce.length === 1 - // This implementation deviates from ES5 spec in the following ways: - // 1. It does not check if reduceFunc is a Callable - reduceArray = arrayProto.reduce || - function(reduceFunc /*, initialValue */) { - /*jshint maxcomplexity: 7*/ - var arr, args, reduced, len, i; - - i = 0; - arr = Object(this); - len = arr.length >>> 0; - args = arguments; - - // If no initialValue, use first item of array (we know length !== 0 here) - // and adjust i to start at second item - if(args.length <= 1) { - // Skip to the first real element in the array - for(;;) { - if(i in arr) { - reduced = arr[i++]; - break; - } - - // If we reached the end of the array without finding any real - // elements, it's a TypeError - if(++i >= len) { - throw new TypeError(); - } - } - } else { - // If initialValue provided, use it - reduced = args[1]; - } - - // Do the actual reduce - for(;i < len; ++i) { - if(i in arr) { - reduced = reduceFunc(reduced, arr[i], i, arr); - } - } - - return reduced; - }; - - function identity(x) { - return x; - } - - return when; -})(this); diff --git a/package.json b/package.json index e4a68f1..7003706 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "A stateful router library for single page applications", "keywords": ["routes", "routing", "router", "hierarchical", "stateful", "pushState"], "homepage": "https://github.com/AlexGalays/abyssa-js/", - "version": "1.2.4", + "version": "1.3.0", "author": { "name": "Alexandre Galays", "url": "https://github.com/AlexGalays/" @@ -22,10 +22,17 @@ "url": "http://www.opensource.org/licenses/mit-license.php" } ], + "dependencies": { + "when": "~2.5.0", + "signals": "~1.0.0", + "crossroads": "~0.12.0", + "html5-history-api": "devote/HTML5-History-API" + }, "devDependencies": { "grunt": "~0.4.0", "grunt-contrib-uglify": "~0.2", "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-watch": "~0.5.0" + "grunt-contrib-watch": "~0.5.0", + "grunt-browserify": "~1.2.0" } } diff --git a/src/Router.js b/src/Router.js index c7caa77..6a49705 100644 --- a/src/Router.js +++ b/src/Router.js @@ -1,4 +1,12 @@ +var Signal = require('signals').Signal, + crossroads = require('crossroads'), + + interceptAnchorClicks = require('./anchorClicks'), + StateWithParams = require('./StateWithParams'), + Transition = require('./Transition'), + util = require('./util'); + /* * Create a new Router instance, passing any state defined declaratively. * More states can be added using addState() before the router is initialized. @@ -8,8 +16,7 @@ */ function Router(declarativeStates) { var router = {}, - states = copyObject(declarativeStates), - // Abyssa internally depends on the lower-level crossroads.js router. + states = util.copyObject(declarativeStates), roads = crossroads.create(), firstTransition = true, initOptions = { @@ -123,7 +130,7 @@ function Router(declarativeStates) { diff = paramDiff(params, newParams); - return (newState == state) && (objectSize(diff) == 0); + return (newState == state) && (util.objectSize(diff) == 0); } /* @@ -157,7 +164,7 @@ function Router(declarativeStates) { * Configure the router before its initialization. */ function configure(options) { - mergeObjects(initOptions, options); + util.mergeObjects(initOptions, options); return router; } @@ -232,7 +239,7 @@ function Router(declarativeStates) { }); } - callbackIfLeaf(objectToArray(states)); + callbackIfLeaf(util.objectToArray(states)); } /* @@ -318,11 +325,11 @@ function Router(declarativeStates) { return ''; }); - if (query) mergeObjects(params, query); + if (query) util.mergeObjects(params, query); // Decode all params for (var i in params) { - if (isString(params[i])) params[i] = decodeURIComponent(params[i]); + if (util.isString(params[i])) params[i] = decodeURIComponent(params[i]); } return params; @@ -341,7 +348,7 @@ function Router(declarativeStates) { if (!state) throw new Error('Cannot find state ' + stateName); [state].concat(state.parents).forEach(function(s) { - mergeObjects(allQueryParams, s.queryParams); + util.mergeObjects(allQueryParams, s.queryParams); }); // The passed params are path and query params lumped together, @@ -414,7 +421,7 @@ function Router(declarativeStates) { // Logging -var log = logError = noop; +var log = logError = util.noop; Router.enableLogs = function() { log = function() { @@ -437,4 +444,4 @@ Router.enableLogs = function() { }; -Abyssa.Router = Router; \ No newline at end of file +module.exports = Router; \ No newline at end of file diff --git a/src/State.js b/src/State.js index 4de6e2e..d332266 100644 --- a/src/State.js +++ b/src/State.js @@ -1,4 +1,7 @@ +var util = require('./util'); +var async = require('./Transition').asyncPromises.register; + /* * Create a new State instance. * @@ -33,8 +36,8 @@ function State() { state.queryParams = args.queryParams; state.states = states; - state.enter = options.enter || noop; - state.exit = options.exit || noop; + state.enter = options.enter || util.noop; + state.exit = options.exit || util.noop; state.enterPrereqs = options.enterPrereqs; state.exitPrereqs = options.exitPrereqs; @@ -50,7 +53,7 @@ function State() { state.children = getChildren(); state.fullName = getFullName(); state.root = state.parents[state.parents.length - 1]; - state.async = Abyssa.Async; + state.async = async; eachChildState(function(name, childState) { childState.init(name, state); @@ -209,7 +212,7 @@ function getArgs(args) { param; if (args.length == 1) { - if (isString(arg1)) result.path = arg1; + if (util.isString(arg1)) result.path = arg1; else result.options = arg1; } else if (args.length == 2) { @@ -222,7 +225,7 @@ function getArgs(args) { if (queryIndex != -1) { result.queryParams = result.path.slice(queryIndex + 1); result.path = result.path.slice(0, queryIndex); - result.queryParams = arrayToObject(result.queryParams.split('&')); + result.queryParams = util.arrayToObject(result.queryParams.split('&')); } // Replace dynamic params like :id with {id}, which is what crossroads uses, @@ -237,4 +240,4 @@ function getArgs(args) { } -Abyssa.State = State; \ No newline at end of file +module.exports = State; \ No newline at end of file diff --git a/src/StateWithParams.js b/src/StateWithParams.js index 165d946..6bd7845 100644 --- a/src/StateWithParams.js +++ b/src/StateWithParams.js @@ -1,49 +1,45 @@ -var StateWithParams = (function() { - - /* - * Creates a new StateWithParams instance. - * - * StateWithParams is the merge between a State object (created and added to the router before init) - * and params (both path and query params, extracted from the URL after init) - */ - function StateWithParams(state, params) { - return { - _state: state, - name: state && state.name, - fullName: state && state.fullName, - data: state && state.data, - params: params, - is: is, - isIn: isIn, - toString: toString - }; - } - - /* - * Returns whether this state has the given fullName. - */ - function is(fullStateName) { - return this.fullName == fullStateName; - } - - /* - * Returns whether this state or any of its parents has the given fullName. - */ - function isIn(fullStateName) { - var current = this._state; - while (current) { - if (current.fullName == fullStateName) return true; - current = current.parent; - } - return false; - } - - function toString() { - return this.fullName + ':' + JSON.stringify(this.params) +/* +* Creates a new StateWithParams instance. +* +* StateWithParams is the merge between a State object (created and added to the router before init) +* and params (both path and query params, extracted from the URL after init) +*/ +function StateWithParams(state, params) { + return { + _state: state, + name: state && state.name, + fullName: state && state.fullName, + data: state && state.data, + params: params, + is: is, + isIn: isIn, + toString: toString + }; +} + +/* +* Returns whether this state has the given fullName. +*/ +function is(fullStateName) { + return this.fullName == fullStateName; +} + +/* +* Returns whether this state or any of its parents has the given fullName. +*/ +function isIn(fullStateName) { + var current = this._state; + while (current) { + if (current.fullName == fullStateName) return true; + current = current.parent; } + return false; +} +function toString() { + return this.fullName + ':' + JSON.stringify(this.params) +} - return StateWithParams; -})(); \ No newline at end of file +module.exports = StateWithParams; \ No newline at end of file diff --git a/src/Transition.js b/src/Transition.js index 355c066..e701d06 100644 --- a/src/Transition.js +++ b/src/Transition.js @@ -1,4 +1,6 @@ +var when = require('when'); + /* * Create a new Transition instance. */ @@ -153,7 +155,7 @@ function transitionStates(state, root, paramOnlyChange) { } -var asyncPromises = (function () { +var asyncPromises = Transition.asyncPromises = (function () { var that; var activeDeferreds = []; @@ -199,4 +201,4 @@ var asyncPromises = (function () { })(); -Abyssa.Async = asyncPromises.register; \ No newline at end of file +module.exports = Transition; \ No newline at end of file diff --git a/src/anchorClicks.js b/src/anchorClicks.js index 6a19555..94fce80 100644 --- a/src/anchorClicks.js +++ b/src/anchorClicks.js @@ -1,81 +1,76 @@ -var interceptAnchorClicks = (function() { +var ieButton; - var ieButton; +function anchorClickHandler(evt) { + evt = evt || window.event; - function anchorClickHandler(evt) { - evt = evt || window.event; + var defaultPrevented = ('defaultPrevented' in evt) + ? evt.defaultPrevented + : (evt.returnValue === false); - var defaultPrevented = ('defaultPrevented' in evt) - ? evt.defaultPrevented - : (evt.returnValue === false); + if (defaultPrevented || evt.metaKey || evt.ctrlKey || !isLeftButtonClick(evt)) return; - if (defaultPrevented || evt.metaKey || evt.ctrlKey || !isLeftButtonClick(evt)) return; + var target = evt.target || evt.srcElement; + var anchor = anchorTarget(target); + if (!anchor) return; - var target = evt.target || evt.srcElement; - var anchor = anchorTarget(target); - if (!anchor) return; + var href = anchor.getAttribute('href'); - var href = anchor.getAttribute('href'); + if (href.charAt(0) == '#') return; + if (anchor.getAttribute('target') == '_blank') return; + if (!isLocalLink(anchor)) return; - if (href.charAt(0) == '#') return; - if (anchor.getAttribute('target') == '_blank') return; - if (!isLocalLink(anchor)) return; + if (evt.preventDefault) + evt.preventDefault(); + else + evt.returnValue = false; - if (evt.preventDefault) - evt.preventDefault(); - else - evt.returnValue = false; + router.state(href); +} - router.state(href); - } - - function isLeftButtonClick(evt) { - evt = evt || window.event; - var button = (evt.which !== undefined) ? evt.which : ieButton; - return button == 1; - } +function isLeftButtonClick(evt) { + evt = evt || window.event; + var button = (evt.which !== undefined) ? evt.which : ieButton; + return button == 1; +} - function anchorTarget(target) { - while (target) { - if (target.nodeName == 'A') return target; - target = target.parentNode; - } +function anchorTarget(target) { + while (target) { + if (target.nodeName == 'A') return target; + target = target.parentNode; } - - // IE does not provide the correct event.button information on 'onclick' handlers - // but it does on mousedown/mouseup handlers. - function rememberIeButton(evt) { - ieButton = (evt || window.event).button; +} + +// IE does not provide the correct event.button information on 'onclick' handlers +// but it does on mousedown/mouseup handlers. +function rememberIeButton(evt) { + ieButton = (evt || window.event).button; +} + +function isLocalLink(anchor) { + var hostname = anchor.hostname; + var port = anchor.port; + + // IE10 and below can lose the hostname/port property when setting a relative href from JS + if (!hostname) { + var tempAnchor = document.createElement("a"); + tempAnchor.href = anchor.href; + hostname = tempAnchor.hostname; + port = tempAnchor.port; } - function isLocalLink(anchor) { - var hostname = anchor.hostname; - var port = anchor.port; + var sameHostname = (hostname == location.hostname); + var samePort = (port || '80') == (location.port || '80'); - // IE10 and below can lose the hostname/port property when setting a relative href from JS - if (!hostname) { - var tempAnchor = document.createElement("a"); - tempAnchor.href = anchor.href; - hostname = tempAnchor.hostname; - port = tempAnchor.port; - } + return sameHostname && samePort; +} - var sameHostname = (hostname == location.hostname); - var samePort = (port || '80') == (location.port || '80'); - return sameHostname && samePort; +module.exports = function interceptAnchorClicks(router) { + if (document.addEventListener) + document.addEventListener('click', anchorClickHandler); + else { + document.attachEvent('onmousedown', rememberIeButton); + document.attachEvent('onclick', anchorClickHandler); } - - - return function (router) { - if (document.addEventListener) - document.addEventListener('click', anchorClickHandler); - else { - document.attachEvent('onmousedown', rememberIeButton); - document.attachEvent('onclick', anchorClickHandler); - } - }; - - -})(); \ No newline at end of file +}; \ No newline at end of file diff --git a/src/footer.js b/src/footer.js deleted file mode 100644 index b7dc02a..0000000 --- a/src/footer.js +++ /dev/null @@ -1,4 +0,0 @@ - -return Abyssa; - -}); \ No newline at end of file diff --git a/src/header.js b/src/header.js deleted file mode 100644 index 33c5fe2..0000000 --- a/src/header.js +++ /dev/null @@ -1,19 +0,0 @@ -(function (factory) { - - // No AMD/CommonJS dependencies for now, just use the local (slighty modified) lib/ files. - // html5-history-api is not in npm yet. To be continued. - - if (typeof define === 'function' && define.amd) { - define([], factory); - } - else if (typeof exports === "object") { - module.exports = factory(); - } - else { - window.Abyssa = factory(); - } - -})(function() { - var Abyssa = {}; - var Signal = signals.Signal; - diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..ab0ed3b --- /dev/null +++ b/src/main.js @@ -0,0 +1,10 @@ + +require('html5-history-api/history.iegte8'); + +var Abyssa = { + Router: require('./Router'), + State: require('./State'), + Async: require('./Transition').asyncPromises.register +}; + +module.exports = Abyssa; \ No newline at end of file diff --git a/src/util.js b/src/util.js index 1f36164..1b3e639 100644 --- a/src/util.js +++ b/src/util.js @@ -32,4 +32,14 @@ function objectSize(obj) { var size = 0; for (var key in obj) size++; return size; -} \ No newline at end of file +} + +module.exports = { + isString: isString, + noop: noop, + arrayToObject: arrayToObject, + objectToArray: objectToArray, + copyObject: copyObject, + mergeObjects: mergeObjects, + objectSize: objectSize +}; \ No newline at end of file diff --git a/target/abyssa-nodeps.js b/target/abyssa-nodeps.js deleted file mode 100644 index 604c03d..0000000 --- a/target/abyssa-nodeps.js +++ /dev/null @@ -1,1073 +0,0 @@ -/* abyssa 1.2.4 - A stateful router library for single page applications */ - -(function (factory) { - - // No AMD/CommonJS dependencies for now, just use the local (slighty modified) lib/ files. - // html5-history-api is not in npm yet. To be continued. - - if (typeof define === 'function' && define.amd) { - define([], factory); - } - else if (typeof exports === "object") { - module.exports = factory(); - } - else { - window.Abyssa = factory(); - } - -})(function() { - var Abyssa = {}; - var Signal = signals.Signal; - - - -function isString(instance) { - return Object.prototype.toString.call(instance) == '[object String]'; -} - -function noop() {} - -function arrayToObject(array) { - return array.reduce(function(obj, item) { - obj[item] = 1; - return obj; - }, {}); -} - -function objectToArray(obj) { - var array = []; - for (var key in obj) array.push(obj[key]); - return array; -} - -function copyObject(obj) { - var copy = {}; - for (var key in obj) copy[key] = obj[key]; - return copy; -} - -function mergeObjects(to, from) { - for (var key in from) to[key] = from[key]; -} - -function objectSize(obj) { - var size = 0; - for (var key in obj) size++; - return size; -} - -var StateWithParams = (function() { - - /* - * Creates a new StateWithParams instance. - * - * StateWithParams is the merge between a State object (created and added to the router before init) - * and params (both path and query params, extracted from the URL after init) - */ - function StateWithParams(state, params) { - return { - _state: state, - name: state && state.name, - fullName: state && state.fullName, - data: state && state.data, - params: params, - is: is, - isIn: isIn, - toString: toString - }; - } - - /* - * Returns whether this state has the given fullName. - */ - function is(fullStateName) { - return this.fullName == fullStateName; - } - - /* - * Returns whether this state or any of its parents has the given fullName. - */ - function isIn(fullStateName) { - var current = this._state; - while (current) { - if (current.fullName == fullStateName) return true; - current = current.parent; - } - return false; - } - - function toString() { - return this.fullName + ':' + JSON.stringify(this.params) - } - - - return StateWithParams; - -})(); - -/* -* Create a new Transition instance. -*/ -function Transition(fromStateWithParams, toStateWithParams, paramDiff) { - var root, - cancelled, - enters, - transitionPromise, - error, - exits = []; - - var fromState = fromStateWithParams && fromStateWithParams._state; - var toState = toStateWithParams._state; - var params = toStateWithParams.params; - var paramOnlyChange = (fromState == toState); - - var transition = { - from: fromState, - to: toState, - toParams: params, - then: then, - cancel: cancel, - cancelled: cancelled, - currentState: fromState - }; - - // The first transition has no fromState. - if (fromState) { - root = transitionRoot(fromState, toState, paramOnlyChange, paramDiff); - exits = transitionStates(fromState, root, paramOnlyChange); - } - - enters = transitionStates(toState, root, paramOnlyChange).reverse(); - - transitionPromise = prereqs(enters, exits, params).then(function() { - if (!cancelled) doTransition(enters, exits, params, transition); - }); - - asyncPromises.newTransitionStarted(); - - function then(completed, failed) { - return transitionPromise.then( - function success() { if (!cancelled) completed(); }, - function fail(error) { if (!cancelled) failed(error); } - ); - } - - function cancel() { - cancelled = transition.cancelled = true; - } - - return transition; -} - -/* -* Return the promise of the prerequisites for all the states involved in the transition. -*/ -function prereqs(enters, exits, params) { - - exits.forEach(function(state) { - if (!state.exitPrereqs) return; - - var prereqs = state._exitPrereqs = when(state.exitPrereqs()).then( - function success(value) { - if (state._exitPrereqs == prereqs) state._exitPrereqs.value = value; - }, - function fail(cause) { - throw new Error('Failed to resolve EXIT prereqs of ' + state.fullName); - } - ); - }); - - enters.forEach(function(state) { - if (!state.enterPrereqs) return; - - var prereqs = state._enterPrereqs = when(state.enterPrereqs(params)).then( - function success(value) { - if (state._enterPrereqs == prereqs) state._enterPrereqs.value = value; - }, - function fail(cause) { - throw new Error('Failed to resolve ENTER prereqs of ' + state.fullName); - } - ); - }); - - return when.all(enters.concat(exits).map(function(state) { - return state._enterPrereqs || state._exitPrereqs; - })); -} - -function doTransition(enters, exits, params, transition) { - exits.forEach(function(state) { - state.exit(state._exitPrereqs && state._exitPrereqs.value); - }); - - // Async promises are only allowed in 'enter' hooks. - // Make it explicit to prevent programming errors. - asyncPromises.allowed = true; - - enters.forEach(function(state) { - if (!transition.cancelled) { - transition.currentState = state; - state.enter(params, state._enterPrereqs && state._enterPrereqs.value); - } - }); - - asyncPromises.allowed = false; -} - -/* -* The top-most current state's parent that must be exited. -*/ -function transitionRoot(fromState, toState, paramOnlyChange, paramDiff) { - var root, - parent, - param; - - // For a param-only change, the root is the top-most state owning the param(s), - if (paramOnlyChange) { - fromState.parents.slice().reverse().forEach(function(parent) { - for (param in paramDiff) { - if (parent.params[param] || parent.queryParams[param]) { - root = parent; - break; - } - } - }); - } - // Else, the root is the closest common parent of the two states. - else { - for (var i = 0; i < fromState.parents.length; i++) { - parent = fromState.parents[i]; - if (toState.parents.indexOf(parent) > -1) { - root = parent; - break; - } - } - } - - return root; -} - -function withParents(state, upTo, inclusive) { - var p = state.parents, - end = Math.min(p.length, p.indexOf(upTo) + (inclusive ? 1 : 0)); - return [state].concat(p.slice(0, end)); -} - -function transitionStates(state, root, paramOnlyChange) { - var inclusive = !root || paramOnlyChange; - return withParents(state, root || state.root, inclusive); -} - - -var asyncPromises = (function () { - - var that; - var activeDeferreds = []; - - /* - * Returns a promise that will not be fullfilled if the navigation context - * changes before the wrapped promise is fullfilled. - */ - function register(promise) { - if (!that.allowed) - throw new Error('Async can only be called from within state.enter()'); - - var defer = when.defer(); - - activeDeferreds.push(defer); - - when(promise).then( - function(value) { - if (activeDeferreds.indexOf(defer) > -1) - defer.resolve(value); - }, - function(error) { - if (activeDeferreds.indexOf(defer) > -1) - defer.reject(error); - } - ); - - return defer.promise; - } - - function newTransitionStarted() { - activeDeferreds.length = 0; - } - - that = { - register: register, - newTransitionStarted: newTransitionStarted, - allowed: false - }; - - return that; - -})(); - - -Abyssa.Async = asyncPromises.register; - -/* -* Create a new State instance. -* -* State() // A state without options and an empty path. -* State('path', {options}) // A state with a static named path and options -* State(':path', {options}) // A state with a dynamic named path and options -* State('path?query', {options}) // Same as above with an optional query string param named 'query' -* State({options}) // Its path is the empty string. -* -* options is an object with the following optional properties: -* enter, exit, enterPrereqs, exitPrereqs. -* -* Child states can also be specified in the options: -* State({ myChildStateName: State() }) -* This is the declarative equivalent to the addState method. -* -* Finally, options can contain any arbitrary data value -* that will get stored in the state and made available via the data() method: -* State({myData: 55}) -* This is the declarative equivalent to the data(key, value) method. -*/ -function State() { - var state = { _isState: true }, - args = getArgs(arguments), - options = args.options, - states = getStates(args.options), - initialized; - - - state.path = args.path; - state.params = args.params; - state.queryParams = args.queryParams; - state.states = states; - - state.enter = options.enter || noop; - state.exit = options.exit || noop; - state.enterPrereqs = options.enterPrereqs; - state.exitPrereqs = options.exitPrereqs; - - state.ownData = getOwnData(options); - - /* - * Initialize and freeze this state. - */ - function init(name, parent) { - state.name = name; - state.parent = parent; - state.parents = getParents(); - state.children = getChildren(); - state.fullName = getFullName(); - state.root = state.parents[state.parents.length - 1]; - state.async = Abyssa.Async; - - eachChildState(function(name, childState) { - childState.init(name, state); - }); - - initialized = true; - } - - /* - * The full path, composed of all the individual paths of this state and its parents. - */ - function fullPath() { - var result = state.path, - stateParent = state.parent; - - while (stateParent) { - if (stateParent.path) result = stateParent.path + '/' + result; - stateParent = stateParent.parent; - } - - return result; - } - - /* - * The list of all parents, starting from the closest ones. - */ - function getParents() { - var parents = [], - parent = state.parent; - - while (parent) { - parents.push(parent); - parent = parent.parent; - } - - return parents; - } - - /* - * The list of child states as an Array. - */ - function getChildren() { - var children = []; - - for (var name in states) { - children.push(states[name]); - } - - return children; - } - - /* - * The map of initial child states by name. - */ - function getStates(options) { - var states = {}; - - for (var key in options) { - if (options[key]._isState) states[key] = options[key]; - } - - return states; - } - - /* - * The fully qualified name of this state. - * e.g granparentName.parentName.name - */ - function getFullName() { - return state.parents.reduceRight(function(acc, parent) { - return acc + parent.name + '.'; - }, '') + state.name; - } - - function getOwnData(options) { - var reservedKeys = {'enter': 1, 'exit': 1, 'enterPrereqs': 1, 'exitPrereqs': 1}, - result = {}; - - for (var key in options) { - if (reservedKeys[key] || options[key]._isState) continue; - result[key] = options[key]; - } - - return result; - } - - /* - * Get or Set some arbitrary data by key on this state. - * child states have access to their parents' data. - * - * This can be useful when using external models/services - * as a mean to communicate between states is not desired. - */ - function data(key, value) { - if (value !== undefined) { - if (state.ownData[key] !== undefined) - throw new Error('State ' + state.fullName + ' already has data with the key ' + key); - state.ownData[key] = value; - return state; - } - - var currentState = state; - - while (currentState.ownData[key] === undefined && currentState.parent) - currentState = currentState.parent; - - return currentState.ownData[key]; - } - - function eachChildState(callback) { - for (var name in states) callback(name, states[name]); - } - - /* - * Add a child state. - */ - function addState(name, childState) { - if (initialized) - throw new Error('States can only be added before the Router is initialized'); - - if (states[name]) - throw new Error('The state {0} already has a child state named {1}' - .replace('{0}', state.name) - .replace('{1}', name) - ); - - states[name] = childState; - - return state; - }; - - function toString() { - return state.fullName; - } - - - state.init = init; - state.fullPath = fullPath; - - // Public methods - - state.data = data; - state.addState = addState; - state.toString = toString; - - return state; -} - - -// Extract the arguments of the overloaded State "constructor" function. -function getArgs(args) { - var result = { path: '', options: {}, params: {}, queryParams: {} }, - arg1 = args[0], - arg2 = args[1], - queryIndex, - param; - - if (args.length == 1) { - if (isString(arg1)) result.path = arg1; - else result.options = arg1; - } - else if (args.length == 2) { - result.path = arg1; - result.options = (typeof arg2 == 'object') ? arg2 : {enter: arg2}; - } - - // Extract the query string - queryIndex = result.path.indexOf('?'); - if (queryIndex != -1) { - result.queryParams = result.path.slice(queryIndex + 1); - result.path = result.path.slice(0, queryIndex); - result.queryParams = arrayToObject(result.queryParams.split('&')); - } - - // Replace dynamic params like :id with {id}, which is what crossroads uses, - // and store them for later lookup. - result.path = result.path.replace(/:\w*/g, function(match) { - param = match.substring(1); - result.params[param] = 1; - return '{' + param + '}'; - }); - - return result; -} - - -Abyssa.State = State; - -/* -* Create a new Router instance, passing any state defined declaratively. -* More states can be added using addState() before the router is initialized. -* -* Because a router manages global state (the URL), only one instance of Router -* should be used inside an application. -*/ -function Router(declarativeStates) { - var router = {}, - states = copyObject(declarativeStates), - // Abyssa internally depends on the lower-level crossroads.js router. - roads = crossroads.create(), - firstTransition = true, - initOptions = { - enableLogs: false, - interceptAnchorClicks: true - }, - currentPathQuery, - currentState, - transition, - leafStates, - stateFound, - poppedState, - initialized; - - // Routes params should be type casted. e.g the dynamic path items/:id when id is 33 - // will end up passing the integer 33 as an argument, not the string "33". - roads.shouldTypecast = true; - // Nil transitions are prevented from our side. - roads.ignoreState = true; - - /* - * Setting a new state will start a transition from the current state to the target state. - * A successful transition will result in the URL being changed. - * A failed transition will leave the router in its current state. - */ - function setState(state, params) { - if (isSameState(state, params)) return; - - var fromState; - var toState = StateWithParams(state, params); - - if (transition) { - cancelTransition(); - fromState = StateWithParams(transition.currentState, transition.toParams); - } - else { - fromState = currentState; - } - - // While the transition is running, any code asking the router about the current state should - // get the end result state. The currentState is rollbacked if the transition fails. - currentState = toState; - - startingTransition(fromState, toState); - - transition = Transition( - fromState, - toState, - paramDiff(fromState && fromState.params, params)); - - transition.then( - function success() { - var historyState; - - transition = null; - - if (!poppedState && !firstTransition) { - historyState = ('/' + currentPathQuery).replace('//', '/'); - log('Pushing state: {0}', historyState); - history.pushState(historyState, document.title, historyState); - } - - transitionCompleted(fromState, toState); - }, - function fail(error) { - transition = null; - currentState = fromState; - - logError('Transition from {0} to {1} failed: {2}', fromState, toState, error); - router.transition.failed.dispatch(fromState, toState); - }); - } - - function cancelTransition() { - log('Cancelling existing transition from {0} to {1}', - transition.from, transition.to); - - transition.cancel(); - - router.transition.cancelled.dispatch(transition.from, transition.to); - } - - function startingTransition(fromState, toState) { - log('Starting transition from {0} to {1}', fromState, toState); - - router.transition.started.dispatch(fromState, toState); - } - - function transitionCompleted(fromState, toState) { - log('Transition from {0} to {1} completed', fromState, toState); - - router.transition.completed.dispatch(fromState, toState); - firstTransition = false; - } - - /* - * Return whether the passed state is the same as the current one; - * in which case the router can ignore the change. - */ - function isSameState(newState, newParams) { - var state, params, diff; - - if (transition) { - state = transition.to; - params = transition.toParams; - } - else if (currentState) { - state = currentState._state; - params = currentState.params; - } - - diff = paramDiff(params, newParams); - - return (newState == state) && (objectSize(diff) == 0); - } - - /* - * Return the set of all the params that changed (Either added, removed or changed). - */ - function paramDiff(oldParams, newParams) { - var diff = {}, - oldParams = oldParams || {}; - - for (var name in oldParams) - if (oldParams[name] != newParams[name]) diff[name] = 1; - - for (var name in newParams) - if (oldParams[name] != newParams[name]) diff[name] = 1; - - return diff; - } - - /* - * The state wasn't found; - * Transition to the 'notFound' state if the developer specified it or else throw an error. - */ - function notFound(state) { - log('State not found: {0}', state); - - if (states.notFound) setState(states.notFound); - else throw new Error ('State "' + state + '" could not be found'); - } - - /* - * Configure the router before its initialization. - */ - function configure(options) { - mergeObjects(initOptions, options); - return router; - } - - /* - * Initialize and freeze the router (states can not be added afterwards). - * The router will immediately initiate a transition to, in order of priority: - * 1) The state captured by the current URL - * 2) The init state passed as an argument - * 3) The default state (pathless and queryless) - */ - function init(initState) { - if (initOptions.enableLogs) - Router.enableLogs(); - - if (initOptions.interceptAnchorClicks) - interceptAnchorClicks(router); - - log('Router init'); - initStates(); - - var initialState = (!Router.ignoreInitialURL && urlPathQuery()) || initState || ''; - - log('Initializing to state {0}', initialState || '""'); - state(initialState); - - window.onpopstate = function(evt) { - // history.js will dispatch fake popstate events on HTML4 browsers' hash changes; - // in these cases, evt.state is null. - var newState = evt.state || urlPathQuery(); - - log('Popped state: {0}', newState); - poppedState = true; - setStateForPathQuery(newState); - }; - - initialized = true; - return router; - } - - function initStates() { - eachRootState(function(name, state) { - state.init(name); - }); - - leafStates = {}; - - // Only leaf states can be transitioned to. - eachLeafState(function(state) { - leafStates[state.fullName] = state; - - state.route = roads.addRoute(state.fullPath() + ":?query:"); - state.route.matched.add(function() { - stateFound = true; - setState(state, toParams(state, arguments)); - }); - }); - } - - function eachRootState(callback) { - for (var name in states) callback(name, states[name]); - } - - function eachLeafState(callback) { - var name, state; - - function callbackIfLeaf(states) { - states.forEach(function(state) { - if (state.children.length) - callbackIfLeaf(state.children); - else - callback(state); - }); - } - - callbackIfLeaf(objectToArray(states)); - } - - /* - * Request a programmatic state change. - * - * Two notations are supported: - * state('my.target.state', {id: 33, filter: 'desc'}) - * state('target/33?filter=desc') - */ - function state(pathQueryOrName, params) { - var isName = (pathQueryOrName.indexOf('.') > -1 || leafStates[pathQueryOrName]); - - log('Changing state to {0}', pathQueryOrName || '""'); - - poppedState = false; - if (isName) setStateByName(pathQueryOrName, params || {}); - else setStateForPathQuery(pathQueryOrName); - } - - /* - * An alias of 'state'. You can use 'redirect' when it makes more sense semantically. - */ - function redirect(pathQueryOrName, params) { - log('Redirecting...'); - state(pathQueryOrName, params); - } - - function setStateForPathQuery(pathQuery) { - currentPathQuery = pathQuery; - stateFound = false; - roads.parse(pathQuery); - - if (!stateFound) notFound(pathQuery); - } - - function setStateByName(name, params) { - var state = leafStates[name]; - - if (!state) return notFound(name); - - var pathQuery = state.route.interpolate(params); - setStateForPathQuery(pathQuery); - } - - /* - * Add a new root state to the router. - * The name must be unique among root states. - */ - function addState(name, state) { - if (initialized) - throw new Error('States can only be added before the Router is initialized'); - - if (states[name]) - throw new Error('A state already exist in the router with the name ' + name); - - log('Adding state {0}', name); - - states[name] = state; - - return router; - } - - function urlPathQuery() { - var hashSlash = location.href.indexOf('#/'); - return hashSlash > -1 - ? location.href.slice(hashSlash + 2) - : (location.pathname + location.search).slice(1); - } - - /* - * Translate the crossroads argument format to what we want to use. - * We want to keep the path and query names and merge them all in one object for convenience. - */ - function toParams(state, crossroadsArgs) { - var args = Array.prototype.slice.apply(crossroadsArgs), - query = args.pop(), - params = {}, - pathName; - - state.fullPath().replace(/\{\w*\}/g, function(match) { - pathName = match.slice(1, -1); - params[pathName] = args.shift(); - return ''; - }); - - if (query) mergeObjects(params, query); - - // Decode all params - for (var i in params) { - if (isString(params[i])) params[i] = decodeURIComponent(params[i]); - } - - return params; - } - - /* - * Compute a link that can be used in anchors' href attributes - * from a state name and a list of params, a.k.a reverse routing. - */ - function link(stateName, params) { - var query = {}, - allQueryParams = {}, - hasQuery = false, - state = leafStates[stateName]; - - if (!state) throw new Error('Cannot find state ' + stateName); - - [state].concat(state.parents).forEach(function(s) { - mergeObjects(allQueryParams, s.queryParams); - }); - - // The passed params are path and query params lumped together, - // Separate them for crossroads' to compute its interpolation. - for (var key in params) { - if (allQueryParams[key]) { - query[key] = params[key]; - delete params[key]; - hasQuery = true; - } - } - - if (hasQuery) params.query = query; - - return '/' + state.route.interpolate(params).replace('/?', '?'); - } - - /* - * Returns a StateWithParams object representing the current state of the router. - */ - function getCurrentState() { - return currentState; - } - - - // Public methods - - router.configure = configure; - router.init = init; - router.state = state; - router.redirect = redirect; - router.addState = addState; - router.link = link; - router.currentState = getCurrentState; - - - // Signals - - router.transition = { - // Dispatched when a transition started. - started: new Signal(), - // Dispatched when a transition either completed, failed or got cancelled. - ended: new Signal(), - // Dispatched when a transition successfuly completed - completed: new Signal(), - // Dispatched when a transition failed to complete - failed: new Signal(), - // Dispatched when a transition got cancelled - cancelled: new Signal() - }; - - // Dispatched once after the router successfully reached its initial state. - router.initialized = new Signal(); - - router.transition.completed.addOnce(function() { - router.initialized.dispatch(); - }); - - router.transition.completed.add(transitionEnded); - router.transition.failed.add(transitionEnded); - router.transition.cancelled.add(transitionEnded); - - function transitionEnded(oldState, newState) { - router.transition.ended.dispatch(oldState, newState); - } - - return router; -} - - -// Logging - -var log = logError = noop; - -Router.enableLogs = function() { - log = function() { - console.log(getLogMessage(arguments)); - }; - - logError = function() { - console.error(getLogMessage(arguments)); - }; - - function getLogMessage(args) { - var message = args[0], - tokens = Array.prototype.slice.call(args, 1); - - for (var i = 0, l = tokens.length; i < l; i++) - message = message.replace('{' + i + '}', tokens[i]); - - return message; - } -}; - - -Abyssa.Router = Router; - -var interceptAnchorClicks = (function() { - - var ieButton; - - function anchorClickHandler(evt) { - evt = evt || window.event; - - var defaultPrevented = ('defaultPrevented' in evt) - ? evt.defaultPrevented - : (evt.returnValue === false); - - if (defaultPrevented || evt.metaKey || evt.ctrlKey || !isLeftButtonClick(evt)) return; - - var target = evt.target || evt.srcElement; - var anchor = anchorTarget(target); - if (!anchor) return; - - var href = anchor.getAttribute('href'); - - if (href.charAt(0) == '#') return; - if (anchor.getAttribute('target') == '_blank') return; - if (!isLocalLink(anchor)) return; - - if (evt.preventDefault) - evt.preventDefault(); - else - evt.returnValue = false; - - router.state(href); - } - - function isLeftButtonClick(evt) { - evt = evt || window.event; - var button = (evt.which !== undefined) ? evt.which : ieButton; - return button == 1; - } - - function anchorTarget(target) { - while (target) { - if (target.nodeName == 'A') return target; - target = target.parentNode; - } - } - - // IE does not provide the correct event.button information on 'onclick' handlers - // but it does on mousedown/mouseup handlers. - function rememberIeButton(evt) { - ieButton = (evt || window.event).button; - } - - function isLocalLink(anchor) { - var hostname = anchor.hostname; - var port = anchor.port; - - // IE10 and below can lose the hostname/port property when setting a relative href from JS - if (!hostname) { - var tempAnchor = document.createElement("a"); - tempAnchor.href = anchor.href; - hostname = tempAnchor.hostname; - port = tempAnchor.port; - } - - var sameHostname = (hostname == location.hostname); - var samePort = (port || '80') == (location.port || '80'); - - return sameHostname && samePort; - } - - - return function (router) { - if (document.addEventListener) - document.addEventListener('click', anchorClickHandler); - else { - document.attachEvent('onmousedown', rememberIeButton); - document.attachEvent('onclick', anchorClickHandler); - } - }; - - -})(); - -return Abyssa; - -}); \ No newline at end of file diff --git a/target/abyssa-nodeps.min.js b/target/abyssa-nodeps.min.js deleted file mode 100644 index 5de6fb3..0000000 --- a/target/abyssa-nodeps.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/* abyssa 1.2.4 - A stateful router library for single page applications */ - -!function(a){"function"==typeof define&&define.amd?define([],a):"object"==typeof exports?module.exports=a():window.Abyssa=a()}(function(){function a(a){return"[object String]"==Object.prototype.toString.call(a)}function b(){}function c(a){return a.reduce(function(a,b){return a[b]=1,a},{})}function d(a){var b=[];for(var c in a)b.push(a[c]);return b}function e(a){var b={};for(var c in a)b[c]=a[c];return b}function f(a,b){for(var c in b)a[c]=b[c]}function g(a){var b=0;for(var c in a)b++;return b}function h(a,b,c){function d(a,b){return l.then(function(){g||a()},function(a){g||b(a)})}function e(){g=s.cancelled=!0}var f,g,h,l,n=[],o=a&&a._state,p=b._state,q=b.params,r=o==p,s={from:o,to:p,toParams:q,then:d,cancel:e,cancelled:g,currentState:o};return o&&(f=k(o,p,r,c),n=m(o,f,r)),h=m(p,f,r).reverse(),l=i(h,n,q).then(function(){g||j(h,n,q,s)}),t.newTransitionStarted(),s}function i(a,b,c){return b.forEach(function(a){if(a.exitPrereqs)var b=a._exitPrereqs=when(a.exitPrereqs()).then(function(c){a._exitPrereqs==b&&(a._exitPrereqs.value=c)},function(){throw new Error("Failed to resolve EXIT prereqs of "+a.fullName)})}),a.forEach(function(a){if(a.enterPrereqs)var b=a._enterPrereqs=when(a.enterPrereqs(c)).then(function(c){a._enterPrereqs==b&&(a._enterPrereqs.value=c)},function(){throw new Error("Failed to resolve ENTER prereqs of "+a.fullName)})}),when.all(a.concat(b).map(function(a){return a._enterPrereqs||a._exitPrereqs}))}function j(a,b,c,d){b.forEach(function(a){a.exit(a._exitPrereqs&&a._exitPrereqs.value)}),t.allowed=!0,a.forEach(function(a){d.cancelled||(d.currentState=a,a.enter(c,a._enterPrereqs&&a._enterPrereqs.value))}),t.allowed=!1}function k(a,b,c,d){var e,f,g;if(c)a.parents.slice().reverse().forEach(function(a){for(g in d)if(a.params[g]||a.queryParams[g]){e=a;break}});else for(var h=0;h-1){e=f;break}return e}function l(a,b,c){var d=a.parents,e=Math.min(d.length,d.indexOf(b)+(c?1:0));return[a].concat(d.slice(0,e))}function m(a,b,c){var d=!b||c;return l(a,b||a.root,d)}function n(){function a(a,b){n.name=a,n.parent=b,n.parents=d(),n.children=e(),n.fullName=g(),n.root=n.parents[n.parents.length-1],n.async=q.Async,j(function(a,b){b.init(a,n)}),m=!0}function c(){for(var a=n.path,b=n.parent;b;)b.path&&(a=b.path+"/"+a),b=b.parent;return a}function d(){for(var a=[],b=n.parent;b;)a.push(b),b=b.parent;return a}function e(){var a=[];for(var b in s)a.push(s[b]);return a}function f(a){var b={};for(var c in a)a[c]._isState&&(b[c]=a[c]);return b}function g(){return n.parents.reduceRight(function(a,b){return a+b.name+"."},"")+n.name}function h(a){var b={enter:1,exit:1,enterPrereqs:1,exitPrereqs:1},c={};for(var d in a)b[d]||a[d]._isState||(c[d]=a[d]);return c}function i(a,b){if(void 0!==b){if(void 0!==n.ownData[a])throw new Error("State "+n.fullName+" already has data with the key "+a);return n.ownData[a]=b,n}for(var c=n;void 0===c.ownData[a]&&c.parent;)c=c.parent;return c.ownData[a]}function j(a){for(var b in s)a(b,s[b])}function k(a,b){if(m)throw new Error("States can only be added before the Router is initialized");if(s[a])throw new Error("The state {0} already has a child state named {1}".replace("{0}",n.name).replace("{1}",a));return s[a]=b,n}function l(){return n.fullName}var m,n={_isState:!0},p=o(arguments),r=p.options,s=f(p.options);return n.path=p.path,n.params=p.params,n.queryParams=p.queryParams,n.states=s,n.enter=r.enter||b,n.exit=r.exit||b,n.enterPrereqs=r.enterPrereqs,n.exitPrereqs=r.exitPrereqs,n.ownData=h(r),n.init=a,n.fullPath=c,n.data=i,n.addState=k,n.toString=l,n}function o(b){var d,e,f={path:"",options:{},params:{},queryParams:{}},g=b[0],h=b[1];return 1==b.length?a(g)?f.path=g:f.options=g:2==b.length&&(f.path=g,f.options="object"==typeof h?h:{enter:h}),d=f.path.indexOf("?"),-1!=d&&(f.queryParams=f.path.slice(d+1),f.path=f.path.slice(0,d),f.queryParams=c(f.queryParams.split("&"))),f.path=f.path.replace(/:\w*/g,function(a){return e=a.substring(1),f.params[e]=1,"{"+e+"}"}),f}function p(b){function c(a,b){if(!l(a,b)){var c,d=s(a,b);K?(i(),c=s(K.currentState,K.toParams)):c=J,J=d,j(c,d),K=h(c,d,m(c&&c.params,b)),K.then(function(){var a;K=null,N||S||(a=("/"+I).replace("//","/"),u("Pushing state: {0}",a),history.pushState(a,document.title,a)),k(c,d)},function(a){K=null,J=c,logError("Transition from {0} to {1} failed: {2}",c,d,a),P.transition.failed.dispatch(c,d)})}}function i(){u("Cancelling existing transition from {0} to {1}",K.from,K.to),K.cancel(),P.transition.cancelled.dispatch(K.from,K.to)}function j(a,b){u("Starting transition from {0} to {1}",a,b),P.transition.started.dispatch(a,b)}function k(a,b){u("Transition from {0} to {1} completed",a,b),P.transition.completed.dispatch(a,b),S=!1}function l(a,b){var c,d,e;return K?(c=K.to,d=K.toParams):J&&(c=J._state,d=J.params),e=m(d,b),a==c&&0==g(e)}function m(a,b){var c={},a=a||{};for(var d in a)a[d]!=b[d]&&(c[d]=1);for(var d in b)a[d]!=b[d]&&(c[d]=1);return c}function n(a){if(u("State not found: {0}",a),!Q.notFound)throw new Error('State "'+a+'" could not be found');c(Q.notFound)}function o(a){return f(T,a),P}function q(a){T.enableLogs&&p.enableLogs(),T.interceptAnchorClicks&&v(P),u("Router init"),t();var b=!p.ignoreInitialURL&&D()||a||"";return u("Initializing to state {0}",b||'""'),y(b),window.onpopstate=function(a){var b=a.state||D();u("Popped state: {0}",b),N=!0,A(b)},O=!0,P}function t(){w(function(a,b){b.init(a)}),L={},x(function(a){L[a.fullName]=a,a.route=R.addRoute(a.fullPath()+":?query:"),a.route.matched.add(function(){M=!0,c(a,E(a,arguments))})})}function w(a){for(var b in Q)a(b,Q[b])}function x(a){function b(c){c.forEach(function(c){c.children.length?b(c.children):a(c)})}b(d(Q))}function y(a,b){var c=a.indexOf(".")>-1||L[a];u("Changing state to {0}",a||'""'),N=!1,c?B(a,b||{}):A(a)}function z(a,b){u("Redirecting..."),y(a,b)}function A(a){I=a,M=!1,R.parse(a),M||n(a)}function B(a,b){var c=L[a];if(!c)return n(a);var d=c.route.interpolate(b);A(d)}function C(a,b){if(O)throw new Error("States can only be added before the Router is initialized");if(Q[a])throw new Error("A state already exist in the router with the name "+a);return u("Adding state {0}",a),Q[a]=b,P}function D(){var a=location.href.indexOf("#/");return a>-1?location.href.slice(a+2):(location.pathname+location.search).slice(1)}function E(b,c){var d,e=Array.prototype.slice.apply(c),g=e.pop(),h={};b.fullPath().replace(/\{\w*\}/g,function(a){return d=a.slice(1,-1),h[d]=e.shift(),""}),g&&f(h,g);for(var i in h)a(h[i])&&(h[i]=decodeURIComponent(h[i]));return h}function F(a,b){var c={},d={},e=!1,g=L[a];if(!g)throw new Error("Cannot find state "+a);[g].concat(g.parents).forEach(function(a){f(d,a.queryParams)});for(var h in b)d[h]&&(c[h]=b[h],delete b[h],e=!0);return e&&(b.query=c),"/"+g.route.interpolate(b).replace("/?","?")}function G(){return J}function H(a,b){P.transition.ended.dispatch(a,b)}var I,J,K,L,M,N,O,P={},Q=e(b),R=crossroads.create(),S=!0,T={enableLogs:!1,interceptAnchorClicks:!0};return R.shouldTypecast=!0,R.ignoreState=!0,P.configure=o,P.init=q,P.state=y,P.redirect=z,P.addState=C,P.link=F,P.currentState=G,P.transition={started:new r,ended:new r,completed:new r,failed:new r,cancelled:new r},P.initialized=new r,P.transition.completed.addOnce(function(){P.initialized.dispatch()}),P.transition.completed.add(H),P.transition.failed.add(H),P.transition.cancelled.add(H),P}var q={},r=signals.Signal,s=function(){function a(a,e){return{_state:a,name:a&&a.name,fullName:a&&a.fullName,data:a&&a.data,params:e,is:b,isIn:c,toString:d}}function b(a){return this.fullName==a}function c(a){for(var b=this._state;b;){if(b.fullName==a)return!0;b=b.parent}return!1}function d(){return this.fullName+":"+JSON.stringify(this.params)}return a}(),t=function(){function a(a){if(!c.allowed)throw new Error("Async can only be called from within state.enter()");var b=when.defer();return d.push(b),when(a).then(function(a){d.indexOf(b)>-1&&b.resolve(a)},function(a){d.indexOf(b)>-1&&b.reject(a)}),b.promise}function b(){d.length=0}var c,d=[];return c={register:a,newTransitionStarted:b,allowed:!1}}();q.Async=t.register,q.State=n;var u=logError=b;p.enableLogs=function(){function a(a){for(var b=a[0],c=Array.prototype.slice.call(a,1),d=0,e=c.length;e>d;d++)b=b.replace("{"+d+"}",c[d]);return b}u=function(){console.log(a(arguments))},logError=function(){console.error(a(arguments))}},q.Router=p;var v=function(){function a(a){a=a||window.event;var d="defaultPrevented"in a?a.defaultPrevented:a.returnValue===!1;if(!(d||a.metaKey||a.ctrlKey)&&b(a)){var f=a.target||a.srcElement,g=c(f);if(g){var h=g.getAttribute("href");"#"!=h.charAt(0)&&"_blank"!=g.getAttribute("target")&&e(g)&&(a.preventDefault?a.preventDefault():a.returnValue=!1,router.state(h))}}}function b(a){a=a||window.event;var b=void 0!==a.which?a.which:f;return 1==b}function c(a){for(;a;){if("A"==a.nodeName)return a;a=a.parentNode}}function d(a){f=(a||window.event).button}function e(a){var b=a.hostname,c=a.port;if(!b){var d=document.createElement("a");d.href=a.href,b=d.hostname,c=d.port}var e=b==location.hostname,f=(c||"80")==(location.port||"80");return e&&f}var f;return function(){document.addEventListener?document.addEventListener("click",a):(document.attachEvent("onmousedown",d),document.attachEvent("onclick",a))}}();return q}); \ No newline at end of file diff --git a/target/abyssa-with-deps.js b/target/abyssa-with-deps.js new file mode 100644 index 0000000..9b085a5 --- /dev/null +++ b/target/abyssa-with-deps.js @@ -0,0 +1,4186 @@ +/* abyssa 1.3.0 - A stateful router library for single page applications */ + +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.Abyssa=e():"undefined"!=typeof global?global.Abyssa=e():"undefined"!=typeof self&&(self.Abyssa=e())}(function(){var define,module,exports; +return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o + * Author: Miller Medeiros | MIT License + * v0.12.0 (2013/01/21 13:47) + */ + +(function () { +var factory = function (signals) { + + var crossroads, + _hasOptionalGroupBug, + UNDEF; + + // Helpers ----------- + //==================== + + // IE 7-8 capture optional groups as empty strings while other browsers + // capture as `undefined` + _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === ''; + + function arrayIndexOf(arr, val) { + if (arr.indexOf) { + return arr.indexOf(val); + } else { + //Array.indexOf doesn't work on IE 6-7 + var n = arr.length; + while (n--) { + if (arr[n] === val) { + return n; + } + } + return -1; + } + } + + function arrayRemove(arr, item) { + var i = arrayIndexOf(arr, item); + if (i !== -1) { + arr.splice(i, 1); + } + } + + function isKind(val, kind) { + return '[object '+ kind +']' === Object.prototype.toString.call(val); + } + + function isRegExp(val) { + return isKind(val, 'RegExp'); + } + + function isArray(val) { + return isKind(val, 'Array'); + } + + function isFunction(val) { + return typeof val === 'function'; + } + + //borrowed from AMD-utils + function typecastValue(val) { + var r; + if (val === null || val === 'null') { + r = null; + } else if (val === 'true') { + r = true; + } else if (val === 'false') { + r = false; + } else if (val === UNDEF || val === 'undefined') { + r = UNDEF; + } else if (val === '' || isNaN(val)) { + //isNaN('') returns false + r = val; + } else { + //parseFloat(null || '') returns NaN + r = parseFloat(val); + } + return r; + } + + function typecastArrayValues(values) { + var n = values.length, + result = []; + while (n--) { + result[n] = typecastValue(values[n]); + } + return result; + } + + //borrowed from AMD-Utils + function decodeQueryString(str, shouldTypecast) { + var queryArr = (str || '').replace('?', '').split('&'), + n = queryArr.length, + obj = {}, + item, val; + while (n--) { + item = queryArr[n].split('='); + val = shouldTypecast ? typecastValue(item[1]) : item[1]; + obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val; + } + return obj; + } + + + // Crossroads -------- + //==================== + + /** + * @constructor + */ + function Crossroads() { + this.bypassed = new signals.Signal(); + this.routed = new signals.Signal(); + this._routes = []; + this._prevRoutes = []; + this._piped = []; + this.resetState(); + } + + Crossroads.prototype = { + + greedy : false, + + greedyEnabled : true, + + ignoreCase : true, + + ignoreState : false, + + shouldTypecast : false, + + normalizeFn : null, + + resetState : function(){ + this._prevRoutes.length = 0; + this._prevMatchedRequest = null; + this._prevBypassedRequest = null; + }, + + create : function () { + return new Crossroads(); + }, + + addRoute : function (pattern, callback, priority) { + var route = new Route(pattern, callback, priority, this); + this._sortedInsert(route); + return route; + }, + + removeRoute : function (route) { + arrayRemove(this._routes, route); + route._destroy(); + }, + + removeAllRoutes : function () { + var n = this.getNumRoutes(); + while (n--) { + this._routes[n]._destroy(); + } + this._routes.length = 0; + }, + + parse : function (request, defaultArgs) { + request = request || ''; + defaultArgs = defaultArgs || []; + + // should only care about different requests if ignoreState isn't true + if ( !this.ignoreState && + (request === this._prevMatchedRequest || + request === this._prevBypassedRequest) ) { + return; + } + + var routes = this._getMatchedRoutes(request), + i = 0, + n = routes.length, + cur; + + if (n) { + this._prevMatchedRequest = request; + + this._notifyPrevRoutes(routes, request); + this._prevRoutes = routes; + //should be incremental loop, execute routes in order + while (i < n) { + cur = routes[i]; + cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params)); + cur.isFirst = !i; + this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur])); + i += 1; + } + } else { + this._prevBypassedRequest = request; + this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request])); + } + + this._pipeParse(request, defaultArgs); + }, + + _notifyPrevRoutes : function(matchedRoutes, request) { + var i = 0, prev; + while (prev = this._prevRoutes[i++]) { + //check if switched exist since route may be disposed + if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) { + prev.route.switched.dispatch(request); + } + } + }, + + _didSwitch : function (route, matchedRoutes){ + var matched, + i = 0; + while (matched = matchedRoutes[i++]) { + // only dispatch switched if it is going to a different route + if (matched.route === route) { + return false; + } + } + return true; + }, + + _pipeParse : function(request, defaultArgs) { + var i = 0, route; + while (route = this._piped[i++]) { + route.parse(request, defaultArgs); + } + }, + + getNumRoutes : function () { + return this._routes.length; + }, + + _sortedInsert : function (route) { + //simplified insertion sort + var routes = this._routes, + n = routes.length; + do { --n; } while (routes[n] && route._priority <= routes[n]._priority); + routes.splice(n+1, 0, route); + }, + + _getMatchedRoutes : function (request) { + var res = [], + routes = this._routes, + n = routes.length, + route; + //should be decrement loop since higher priorities are added at the end of array + while (route = routes[--n]) { + if ((!res.length || this.greedy || route.greedy) && route.match(request)) { + res.push({ + route : route, + params : route._getParamsArray(request) + }); + } + if (!this.greedyEnabled && res.length) { + break; + } + } + return res; + }, + + pipe : function (otherRouter) { + this._piped.push(otherRouter); + }, + + unpipe : function (otherRouter) { + arrayRemove(this._piped, otherRouter); + }, + + toString : function () { + return '[crossroads numRoutes:'+ this.getNumRoutes() +']'; + } + }; + + //"static" instance + crossroads = new Crossroads(); + crossroads.VERSION = '0.12.0'; + + crossroads.NORM_AS_ARRAY = function (req, vals) { + return [vals.vals_]; + }; + + crossroads.NORM_AS_OBJECT = function (req, vals) { + return [vals]; + }; + + + // Route -------------- + //===================== + + /** + * @constructor + */ + function Route(pattern, callback, priority, router) { + var isRegexPattern = isRegExp(pattern), + patternLexer = router.patternLexer; + this._router = router; + this._pattern = pattern; + this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern); + this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern); + this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase); + this.matched = new signals.Signal(); + this.switched = new signals.Signal(); + if (callback) { + this.matched.add(callback); + } + this._priority = priority || 0; + } + + Route.prototype = { + + greedy : false, + + rules : void(0), + + match : function (request) { + request = request || ''; + return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule. + }, + + _validateParams : function (request) { + var rules = this.rules, + values = this._getParamsObject(request), + key; + for (key in rules) { + // normalize_ isn't a validation rule... (#39) + if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){ + return false; + } + } + return true; + }, + + _isValidParam : function (request, prop, values) { + var validationRule = this.rules[prop], + val = values[prop], + isValid = false, + isQuery = (prop.indexOf('?') === 0); + + if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) { + isValid = true; + } + else if (isRegExp(validationRule)) { + if (isQuery) { + val = values[prop +'_']; //use raw string + } + isValid = validationRule.test(val); + } + else if (isArray(validationRule)) { + if (isQuery) { + val = values[prop +'_']; //use raw string + } + isValid = this._isValidArrayRule(validationRule, val); + } + else if (isFunction(validationRule)) { + isValid = validationRule(val, request, values); + } + + return isValid; //fail silently if validationRule is from an unsupported type + }, + + _isValidArrayRule : function (arr, val) { + if (! this._router.ignoreCase) { + return arrayIndexOf(arr, val) !== -1; + } + + if (typeof val === 'string') { + val = val.toLowerCase(); + } + + var n = arr.length, + item, + compareVal; + + while (n--) { + item = arr[n]; + compareVal = (typeof item === 'string')? item.toLowerCase() : item; + if (compareVal === val) { + return true; + } + } + return false; + }, + + _getParamsObject : function (request) { + var shouldTypecast = this._router.shouldTypecast, + values = this._router.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast), + o = {}, + n = values.length, + param, val; + while (n--) { + val = values[n]; + if (this._paramsIds) { + param = this._paramsIds[n]; + if (param.indexOf('?') === 0 && val) { + //make a copy of the original string so array and + //RegExp validation can be applied properly + o[param +'_'] = val; + //update vals_ array as well since it will be used + //during dispatch + val = decodeQueryString(val, shouldTypecast); + values[n] = val; + } + // IE will capture optional groups as empty strings while other + // browsers will capture `undefined` so normalize behavior. + // see: #gh-58, #gh-59, #gh-60 + if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) { + val = void(0); + values[n] = val; + } + o[param] = val; + } + //alias to paths and for RegExp pattern + o[n] = val; + } + o.request_ = shouldTypecast? typecastValue(request) : request; + o.vals_ = values; + return o; + }, + + _getParamsArray : function (request) { + var norm = this.rules? this.rules.normalize_ : null, + params; + norm = norm || this._router.normalizeFn; // default normalize + if (norm && isFunction(norm)) { + params = norm(request, this._getParamsObject(request)); + } else { + params = this._getParamsObject(request).vals_; + } + return params; + }, + + interpolate : function(replacements) { + var str = this._router.patternLexer.interpolate(this._pattern, replacements); + if (! this._validateParams(str) ) { + throw new Error('Generated string doesn\'t validate against `Route.rules`.'); + } + return str; + }, + + dispose : function () { + this._router.removeRoute(this); + }, + + _destroy : function () { + this.matched.dispose(); + this.switched.dispose(); + this.matched = this.switched = this._pattern = this._matchRegexp = null; + }, + + toString : function () { + return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']'; + } + + }; + + + + // Pattern Lexer ------ + //===================== + + Crossroads.prototype.patternLexer = (function () { + + var + //match chars that should be escaped on string regexp + ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, + + //trailing slashes (begin/end of string) + LOOSE_SLASHES_REGEXP = /^\/|\/$/g, + LEGACY_SLASHES_REGEXP = /\/$/g, + + //params - everything between `{ }` or `: :` + PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, + + //used to save params during compile (avoid escaping things that + //shouldn't be escaped). + TOKENS = { + 'OS' : { + //optional slashes + //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?` + rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g, + save : '$1{{id}}$2', + res : '\\/?' + }, + 'RS' : { + //required slashes + //used to insert slash between `:{` and `}{` + rgx : /([:}])\/?(\{)/g, + save : '$1{{id}}$2', + res : '\\/' + }, + 'RQ' : { + //required query string - everything in between `{? }` + rgx : /\{\?([^}]+)\}/g, + //everything from `?` till `#` or end of string + res : '\\?([^#]+)' + }, + 'OQ' : { + //optional query string - everything in between `:? :` + rgx : /:\?([^:]+):/g, + //everything from `?` till `#` or end of string + res : '(?:\\?([^#]*))?' + }, + 'OR' : { + //optional rest - everything in between `: *:` + rgx : /:([^:]+)\*:/g, + res : '(.*)?' // optional group to avoid passing empty string as captured + }, + 'RR' : { + //rest param - everything in between `{ *}` + rgx : /\{([^}]+)\*\}/g, + res : '(.+)' + }, + // required/optional params should come after rest segments + 'RP' : { + //required params - everything between `{ }` + rgx : /\{([^}]+)\}/g, + res : '([^\\/?]+)' + }, + 'OP' : { + //optional params - everything between `: :` + rgx : /:([^:]+):/g, + res : '([^\\/?]+)?\/?' + } + }, + + LOOSE_SLASH = 1, + STRICT_SLASH = 2, + LEGACY_SLASH = 3, + + _slashMode = LOOSE_SLASH; + + + function precompileTokens(){ + var key, cur; + for (key in TOKENS) { + if (TOKENS.hasOwnProperty(key)) { + cur = TOKENS[key]; + cur.id = '__CR_'+ key +'__'; + cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id; + cur.rRestore = new RegExp(cur.id, 'g'); + } + } + } + precompileTokens(); + + + function captureVals(regex, pattern) { + var vals = [], match; + // very important to reset lastIndex since RegExp can have "g" flag + // and multiple runs might affect the result, specially if matching + // same string multiple times on IE 7-8 + regex.lastIndex = 0; + while (match = regex.exec(pattern)) { + vals.push(match[1]); + } + return vals; + } + + function getParamIds(pattern) { + return captureVals(PARAMS_REGEXP, pattern); + } + + function getOptionalParamsIds(pattern) { + return captureVals(TOKENS.OP.rgx, pattern); + } + + function compilePattern(pattern, ignoreCase) { + pattern = pattern || ''; + + if(pattern){ + if (_slashMode === LOOSE_SLASH) { + pattern = pattern.replace(LOOSE_SLASHES_REGEXP, ''); + } + else if (_slashMode === LEGACY_SLASH) { + pattern = pattern.replace(LEGACY_SLASHES_REGEXP, ''); + } + + //save tokens + pattern = replaceTokens(pattern, 'rgx', 'save'); + //regexp escape + pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&'); + //restore tokens + pattern = replaceTokens(pattern, 'rRestore', 'res'); + + if (_slashMode === LOOSE_SLASH) { + pattern = '\\/?'+ pattern; + } + } + + if (_slashMode !== STRICT_SLASH) { + //single slash is treated as empty and end slash is optional + pattern += '\\/?'; + } + return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : ''); + } + + function replaceTokens(pattern, regexpName, replaceName) { + var cur, key; + for (key in TOKENS) { + if (TOKENS.hasOwnProperty(key)) { + cur = TOKENS[key]; + pattern = pattern.replace(cur[regexpName], cur[replaceName]); + } + } + return pattern; + } + + function getParamValues(request, regexp, shouldTypecast) { + var vals = regexp.exec(request); + if (vals) { + vals.shift(); + if (shouldTypecast) { + vals = typecastArrayValues(vals); + } + } + return vals; + } + + function interpolate(pattern, replacements) { + if (typeof pattern !== 'string') { + throw new Error('Route pattern should be a string.'); + } + + var replaceFn = function(match, prop){ + var val; + prop = (prop.substr(0, 1) === '?')? prop.substr(1) : prop; + if (replacements[prop] != null) { + if (typeof replacements[prop] === 'object') { + var queryParts = []; + for(var key in replacements[prop]) { + queryParts.push(encodeURI(key + '=' + replacements[prop][key])); + } + val = '?' + queryParts.join('&'); + } else { + // make sure value is a string see #gh-54 + val = String(replacements[prop]); + } + + if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) { + throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".'); + } + } + else if (match.indexOf('{') !== -1) { + throw new Error('The segment '+ match +' is required.'); + } + else { + val = ''; + } + return val; + }; + + if (! TOKENS.OS.trail) { + TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$'); + } + + return pattern + .replace(TOKENS.OS.rgx, TOKENS.OS.save) + .replace(PARAMS_REGEXP, replaceFn) + .replace(TOKENS.OS.trail, '') // remove trailing + .replace(TOKENS.OS.rRestore, '/'); // add slash between segments + } + + //API + return { + strict : function(){ + _slashMode = STRICT_SLASH; + }, + loose : function(){ + _slashMode = LOOSE_SLASH; + }, + legacy : function(){ + _slashMode = LEGACY_SLASH; + }, + getParamIds : getParamIds, + getOptionalParamsIds : getOptionalParamsIds, + getParamValues : getParamValues, + compilePattern : compilePattern, + interpolate : interpolate + }; + + }()); + + + return crossroads; +}; + +if (typeof define === 'function' && define.amd) { + define(['signals'], factory); +} else if (typeof module !== 'undefined' && module.exports) { //Node + module.exports = factory(require('signals')); +} else { + /*jshint sub:true */ + window['crossroads'] = factory(window['signals']); +} + +}()); + + +},{"signals":4}],2:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; + +process.nextTick = (function () { + var canSetImmediate = typeof window !== 'undefined' + && window.setImmediate; + var canPost = typeof window !== 'undefined' + && window.postMessage && window.addEventListener + ; + + if (canSetImmediate) { + return function (f) { return window.setImmediate(f) }; + } + + if (canPost) { + var queue = []; + window.addEventListener('message', function (ev) { + if (ev.source === window && ev.data === 'process-tick') { + ev.stopPropagation(); + if (queue.length > 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); + + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; + } + + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; + +},{}],3:[function(require,module,exports){ +/*! + * History API JavaScript Library v4.0.8 + * + * Support: IE8+, FF3+, Opera 9+, Safari, Chrome and other + * + * Copyright 2011-2013, Dmitrii Pakhtinov ( spb.piksel@gmail.com ) + * + * http://spb-piksel.ru/ + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Update: 2013-10-31 16:06 + */ +(function(window) { + // Prevent the code from running if there is no window.history object + if (!window.history) return; + // symlink to document + var document = window.document; + // HTML element + var documentElement = document.documentElement; + // symlink to sessionStorage + var sessionStorage = null; + // symlink to constructor of Object + var Object = window['Object']; + // symlink to JSON Object + var JSON = window['JSON']; + // symlink to instance object of 'Location' + var windowLocation = window.location; + // symlink to instance object of 'History' + var windowHistory = window.history; + // new instance of 'History'. The default is a reference to the original object instance + var historyObject = windowHistory; + // symlink to method 'history.pushState' + var historyPushState = windowHistory.pushState; + // symlink to method 'history.replaceState' + var historyReplaceState = windowHistory.replaceState; + // if the browser supports HTML5-History-API + var isSupportHistoryAPI = !!historyPushState; + // verifies the presence of an object 'state' in interface 'History' + var isSupportStateObjectInHistory = 'state' in windowHistory; + // symlink to method 'Object.defineProperty' + var defineProperty = Object.defineProperty; + // new instance of 'Location', for IE8 will use the element HTMLAnchorElement, instead of pure object + var locationObject = redefineProperty({}, 't') ? {} : document.createElement('a'); + // prefix for the names of events + var eventNamePrefix = ''; + // String that will contain the name of the method + var addEventListenerName = window.addEventListener ? 'addEventListener' : (eventNamePrefix = 'on') && 'attachEvent'; + // String that will contain the name of the method + var removeEventListenerName = window.removeEventListener ? 'removeEventListener' : 'detachEvent'; + // String that will contain the name of the method + var dispatchEventName = window.dispatchEvent ? 'dispatchEvent' : 'fireEvent'; + // reference native methods for the events + var addEvent = window[addEventListenerName]; + var removeEvent = window[removeEventListenerName]; + var dispatch = window[dispatchEventName]; + // default settings + var settings = {"basepath": '/', "redirect": 0, "type": '/'}; + // key for the sessionStorage + var sessionStorageKey = '__historyAPI__'; + // Anchor Element for parseURL function + var anchorElement = document.createElement('a'); + // last URL before change to new URL + var lastURL = windowLocation.href; + // Control URL, need to fix the bug in Opera + var checkUrlForPopState = ''; + // trigger event 'onpopstate' on page load + var isFireInitialState = false; + // store a list of 'state' objects in the current session + var stateStorage = {}; + // in this object will be stored custom handlers + var eventsList = {}; + // stored last title + var lastTitle = document.title; + + /** + * Properties that will be replaced in the global + * object 'window', to prevent conflicts + * + * @type {Object} + */ + var eventsDescriptors = { + "onhashchange": null, + "onpopstate": null + }; + + /** + * Fix for Chrome in iOS + * See https://github.com/devote/HTML5-History-API/issues/29 + */ + var fastFixChrome = function(method, args) { + var isNeedFix = window.history !== windowHistory; + if (isNeedFix) { + window.history = windowHistory; + } + method.apply(windowHistory, args); + if (isNeedFix) { + window.history = historyObject; + } + }; + + /** + * Properties that will be replaced/added to object + * 'window.history', includes the object 'history.location', + * for a complete the work with the URL address + * + * @type {Object} + */ + var historyDescriptors = { + /** + * @namespace history + * @param {String} [type] + * @param {String} [basepath] + */ + "redirect": function(type, basepath) { + settings["basepath"] = basepath = basepath == null ? settings["basepath"] : basepath; + settings["type"] = type = type == null ? settings["type"] : type; + if (window.top == window.self) { + var relative = parseURL(null, false, true)._relative; + var path = windowLocation.pathname + windowLocation.search; + if (isSupportHistoryAPI) { + path = path.replace(/([^\/])$/, '$1/'); + if (relative != basepath && (new RegExp("^" + basepath + "$", "i")).test(path)) { + windowLocation.replace(relative); + } + } else if (path != basepath) { + path = path.replace(/([^\/])\?/, '$1/?'); + if ((new RegExp("^" + basepath, "i")).test(path)) { + windowLocation.replace(basepath + '#' + path. + replace(new RegExp("^" + basepath, "i"), type) + windowLocation.hash); + } + } + } + }, + /** + * The method adds a state object entry + * to the history. + * + * @namespace history + * @param {Object} state + * @param {string} title + * @param {string} [url] + */ + pushState: function(state, title, url) { + var t = document.title; + if (lastTitle != null) { + document.title = lastTitle; + } + historyPushState && fastFixChrome(historyPushState, arguments); + changeState(state, url); + document.title = t; + lastTitle = title; + }, + /** + * The method updates the state object, + * title, and optionally the URL of the + * current entry in the history. + * + * @namespace history + * @param {Object} state + * @param {string} title + * @param {string} [url] + */ + replaceState: function(state, title, url) { + var t = document.title; + if (lastTitle != null) { + document.title = lastTitle; + } + delete stateStorage[windowLocation.href]; + historyReplaceState && fastFixChrome(historyReplaceState, arguments); + changeState(state, url, true); + document.title = t; + lastTitle = title; + }, + /** + * Object 'history.location' is similar to the + * object 'window.location', except that in + * HTML4 browsers it will behave a bit differently + * + * @namespace history + */ + "location": { + set: function(value) { + window.location = value; + }, + get: function() { + return isSupportHistoryAPI ? windowLocation : locationObject; + } + }, + /** + * A state object is an object representing + * a user interface state. + * + * @namespace history + */ + "state": { + get: function() { + return stateStorage[windowLocation.href] || null; + } + } + }; + + /** + * Properties for object 'history.location'. + * Object 'history.location' is similar to the + * object 'window.location', except that in + * HTML4 browsers it will behave a bit differently + * + * @type {Object} + */ + var locationDescriptors = { + /** + * Navigates to the given page. + * + * @namespace history.location + */ + assign: function(url) { + if (('' + url).indexOf('#') === 0) { + changeState(null, url); + } else { + windowLocation.assign(url); + } + }, + /** + * Reloads the current page. + * + * @namespace history.location + */ + reload: function() { + windowLocation.reload(); + }, + /** + * Removes the current page from + * the session history and navigates + * to the given page. + * + * @namespace history.location + */ + replace: function(url) { + if (('' + url).indexOf('#') === 0) { + changeState(null, url, true); + } else { + windowLocation.replace(url); + } + }, + /** + * Returns the current page's location. + * + * @namespace history.location + */ + toString: function() { + return this.href; + }, + /** + * Returns the current page's location. + * Can be set, to navigate to another page. + * + * @namespace history.location + */ + "href": { + get: function() { + return parseURL()._href; + } + }, + /** + * Returns the current page's protocol. + * + * @namespace history.location + */ + "protocol": null, + /** + * Returns the current page's host and port number. + * + * @namespace history.location + */ + "host": null, + /** + * Returns the current page's host. + * + * @namespace history.location + */ + "hostname": null, + /** + * Returns the current page's port number. + * + * @namespace history.location + */ + "port": null, + /** + * Returns the current page's path only. + * + * @namespace history.location + */ + "pathname": { + get: function() { + return parseURL()._pathname; + } + }, + /** + * Returns the current page's search + * string, beginning with the character + * '?' and to the symbol '#' + * + * @namespace history.location + */ + "search": { + get: function() { + return parseURL()._search; + } + }, + /** + * Returns the current page's hash + * string, beginning with the character + * '#' and to the end line + * + * @namespace history.location + */ + "hash": { + set: function(value) { + changeState(null, ('' + value).replace(/^(#|)/, '#'), false, lastURL); + }, + get: function() { + return parseURL()._hash; + } + } + }; + + /** + * Just empty function + * + * @return void + */ + function emptyFunction() { + // dummy + } + + /** + * Prepares a parts of the current or specified reference for later use in the library + * + * @param {string} [href] + * @param {boolean} [isWindowLocation] + * @param {boolean} [isNotAPI] + * @return {Object} + */ + function parseURL(href, isWindowLocation, isNotAPI) { + var re = /(?:([\w0-9]+:))?(?:\/\/(?:[^@]*@)?([^\/:\?#]+)(?::([0-9]+))?)?([^\?#]*)(?:(\?[^#]+)|\?)?(?:(#.*))?/; + if (href != null && href !== '' && !isWindowLocation) { + var current = parseURL(), _pathname = current._pathname, _protocol = current._protocol; + // convert to type of string + href = '' + href; + // convert relative link to the absolute + href = /^(?:[\w0-9]+\:)?\/\//.test(href) ? href.indexOf("/") === 0 + ? _protocol + href : href : _protocol + "//" + current._host + ( + href.indexOf("/") === 0 ? href : href.indexOf("?") === 0 + ? _pathname + href : href.indexOf("#") === 0 + ? _pathname + current._search + href : _pathname.replace(/[^\/]+$/g, '') + href + ); + } else { + href = isWindowLocation ? href : windowLocation.href; + // if current browser not support History-API + if (!isSupportHistoryAPI || isNotAPI) { + // get hash fragment + href = href.replace(/^[^#]*/, '') || "#"; + // form the absolute link from the hash + href = windowLocation.protocol + '//' + windowLocation.host + settings['basepath'] + + href.replace(new RegExp("^#[\/]?(?:" + settings["type"] + ")?"), ""); + } + } + // that would get rid of the links of the form: /../../ + anchorElement.href = href; + // decompose the link in parts + var result = re.exec(anchorElement.href); + // host name with the port number + var host = result[2] + (result[3] ? ':' + result[3] : ''); + // folder + var pathname = result[4] || '/'; + // the query string + var search = result[5] || ''; + // hash + var hash = result[6] === '#' ? '' : (result[6] || ''); + // relative link, no protocol, no host + var relative = pathname + search + hash; + // special links for set to hash-link, if browser not support History API + var nohash = pathname.replace(new RegExp("^" + settings["basepath"], "i"), settings["type"]) + search; + // result + return { + _href: result[1] + '//' + host + relative, + _protocol: result[1], + _host: host, + _hostname: result[2], + _port: result[3] || '', + _pathname: pathname, + _search: search, + _hash: hash, + _relative: relative, + _nohash: nohash, + _special: nohash + hash + } + } + + /** + * Initializing storage for the custom state's object + */ + function storageInitialize() { + var storage = ''; + if (sessionStorage) { + // get cache from the storage in browser + storage += sessionStorage.getItem(sessionStorageKey); + } else { + var cookie = document.cookie.split(sessionStorageKey + "="); + if (cookie.length > 1) { + storage += (cookie.pop().split(";").shift() || 'null'); + } + } + try { + stateStorage = JSON.parse(storage) || {}; + } catch(_e_) { + stateStorage = {}; + } + // hang up the event handler to event unload page + addEvent(eventNamePrefix + 'unload', function() { + if (sessionStorage) { + // save current state's object + sessionStorage.setItem(sessionStorageKey, JSON.stringify(stateStorage)); + } else { + // save the current 'state' in the cookie + var state = {}; + if (state[windowLocation.href] = historyObject.state) { + document.cookie = sessionStorageKey + '=' + JSON.stringify(state); + } + } + }, false); + } + + /** + * This method is implemented to override the built-in(native) + * properties in the browser, unfortunately some browsers are + * not allowed to override all the properties and even add. + * For this reason, this was written by a method that tries to + * do everything necessary to get the desired result. + * + * @param {Object} object The object in which will be overridden/added property + * @param {String} prop The property name to be overridden/added + * @param {Object} [descriptor] An object containing properties set/get + * @param {Function} [onWrapped] The function to be called when the wrapper is created + * @return {Object|Boolean} Returns an object on success, otherwise returns false + */ + function redefineProperty(object, prop, descriptor, onWrapped) { + // test only if descriptor is undefined + descriptor = descriptor || {set: emptyFunction}; + // variable will have a value of true the success of attempts to set descriptors + var isDefinedSetter = !descriptor.set; + var isDefinedGetter = !descriptor.get; + // for tests of attempts to set descriptors + var test = {configurable: true, set: function() { + isDefinedSetter = 1; + }, get: function() { + isDefinedGetter = 1; + }}; + + try { + // testing for the possibility of overriding/adding properties + defineProperty(object, prop, test); + // running the test + object[prop] = object[prop]; + // attempt to override property using the standard method + defineProperty(object, prop, descriptor); + } catch(_e_) { + } + + // If the variable 'isDefined' has a false value, it means that need to try other methods + if (!isDefinedSetter || !isDefinedGetter) { + // try to override/add the property, using deprecated functions + if (object.__defineGetter__) { + // testing for the possibility of overriding/adding properties + object.__defineGetter__(prop, test.get); + object.__defineSetter__(prop, test.set); + // running the test + object[prop] = object[prop]; + // attempt to override property using the deprecated functions + descriptor.get && object.__defineGetter__(prop, descriptor.get); + descriptor.set && object.__defineSetter__(prop, descriptor.set); + } + + // Browser refused to override the property, using the standard and deprecated methods + if ((!isDefinedSetter || !isDefinedGetter) && object === window) { + try { + // save original value from this property + var originalValue = object[prop]; + // set null to built-in(native) property + object[prop] = null; + } catch(_e_) { + } + // This rule for Internet Explorer 8 + if ('execScript' in window) { + /** + * to IE8 override the global properties using + * VBScript, declaring it in global scope with + * the same names. + */ + window['execScript']('Public ' + prop, 'VBScript'); + } else { + try { + /** + * This hack allows to override a property + * with the set 'configurable: false', working + * in the hack 'Safari' to 'Mac' + */ + defineProperty(object, prop, {value: emptyFunction}); + } catch(_e_) { + } + } + // set old value to new variable + object[prop] = originalValue; + + } else if (!isDefinedSetter || !isDefinedGetter) { + // the last stage of trying to override the property + try { + try { + // wrap the object in a new empty object + var temp = Object.create(object); + defineProperty(Object.getPrototypeOf(temp) === object ? temp : object, prop, descriptor); + for(var key in object) { + // need to bind a function to the original object + if (typeof object[key] === 'function') { + temp[key] = object[key].bind(object); + } + } + try { + // to run a function that will inform about what the object was to wrapped + onWrapped.call(temp, temp, object); + } catch(_e_) { + } + object = temp; + } catch(_e_) { + // sometimes works override simply by assigning the prototype property of the constructor + defineProperty(object.constructor.prototype, prop, descriptor); + } + } catch(_e_) { + // all methods have failed + return false; + } + } + } + + return object; + } + + /** + * Adds the missing property in descriptor + * + * @param {Object} object An object that stores values + * @param {String} prop Name of the property in the object + * @param {Object|null} descriptor Descriptor + * @return {Object} Returns the generated descriptor + */ + function prepareDescriptorsForObject(object, prop, descriptor) { + descriptor = descriptor || {}; + // the default for the object 'location' is the standard object 'window.location' + object = object === locationDescriptors ? windowLocation : object; + // setter for object properties + descriptor.set = (descriptor.set || function(value) { + object[prop] = value; + }); + // getter for object properties + descriptor.get = (descriptor.get || function() { + return object[prop]; + }); + return descriptor; + } + + /** + * Wrapper for the methods 'addEventListener/attachEvent' in the context of the 'window' + * + * @param {String} event The event type for which the user is registering + * @param {Function} listener The method to be called when the event occurs. + * @param {Boolean} capture If true, capture indicates that the user wishes to initiate capture. + * @return void + */ + function addEventListener(event, listener, capture) { + if (event in eventsList) { + // here stored the event listeners 'popstate/hashchange' + eventsList[event].push(listener); + } else { + // FireFox support non-standart four argument aWantsUntrusted + // https://github.com/devote/HTML5-History-API/issues/13 + if (arguments.length > 3) { + addEvent(event, listener, capture, arguments[3]); + } else { + addEvent(event, listener, capture); + } + } + } + + /** + * Wrapper for the methods 'removeEventListener/detachEvent' in the context of the 'window' + * + * @param {String} event The event type for which the user is registered + * @param {Function} listener The parameter indicates the Listener to be removed. + * @param {Boolean} capture Was registered as a capturing listener or not. + * @return void + */ + function removeEventListener(event, listener, capture) { + var list = eventsList[event]; + if (list) { + for(var i = list.length; --i;) { + if (list[i] === listener) { + list.splice(i, 1); + break; + } + } + } else { + removeEvent(event, listener, capture); + } + } + + /** + * Wrapper for the methods 'dispatchEvent/fireEvent' in the context of the 'window' + * + * @param {Event|String} event Instance of Event or event type string if 'eventObject' used + * @param {*} [eventObject] For Internet Explorer 8 required event object on this argument + * @return {Boolean} If 'preventDefault' was called the value is false, else the value is true. + */ + function dispatchEvent(event, eventObject) { + var eventType = ('' + (typeof event === "string" ? event : event.type)).replace(/^on/, ''); + var list = eventsList[eventType]; + if (list) { + // need to understand that there is one object of Event + eventObject = typeof event === "string" ? eventObject : event; + if (eventObject.target == null) { + // need to override some of the properties of the Event object + for(var props = ['target', 'currentTarget', 'srcElement', 'type']; event = props.pop();) { + // use 'redefineProperty' to override the properties + eventObject = redefineProperty(eventObject, event, { + get: event === 'type' ? function() { + return eventType; + } : function() { + return window; + } + }); + } + } + // run function defined in the attributes 'onpopstate/onhashchange' in the 'window' context + ((eventType === 'popstate' ? window.onpopstate : window.onhashchange) + || emptyFunction).call(window, eventObject); + // run other functions that are in the list of handlers + for(var i = 0, len = list.length; i < len; i++) { + list[i].call(window, eventObject); + } + return true; + } else { + return dispatch(event, eventObject); + } + } + + /** + * dispatch current state event + */ + function firePopState() { + var o = document.createEvent ? document.createEvent('Event') : document.createEventObject(); + if (o.initEvent) { + o.initEvent('popstate', false, false); + } else { + o.type = 'popstate'; + } + o.state = historyObject.state; + // send a newly created events to be processed + dispatchEvent(o); + } + + /** + * fire initial state for non-HTML5 browsers + */ + function fireInitialState() { + if (isFireInitialState) { + isFireInitialState = false; + firePopState(); + } + } + + /** + * Change the data of the current history for HTML4 browsers + * + * @param {Object} state + * @param {string} [url] + * @param {Boolean} [replace] + * @param {string} [lastURLValue] + * @return void + */ + function changeState(state, url, replace, lastURLValue) { + if (!isSupportHistoryAPI) { + // normalization url + var urlObject = parseURL(url); + // if current url not equal new url + if (urlObject._relative !== parseURL()._relative) { + // if empty lastURLValue to skip hash change event + lastURL = lastURLValue; + if (replace) { + // only replace hash, not store to history + windowLocation.replace("#" + urlObject._special); + } else { + // change hash and add new record to history + windowLocation.hash = urlObject._special; + } + } + } + if (!isSupportStateObjectInHistory && state) { + stateStorage[windowLocation.href] = state; + } + isFireInitialState = false; + } + + /** + * Event handler function changes the hash in the address bar + * + * @param {Event} event + * @return void + */ + function onHashChange(event) { + // if not empty lastURL, otherwise skipped the current handler event + if (lastURL) { + // if checkUrlForPopState equal current url, this means that the event was raised popstate browser + if (checkUrlForPopState !== windowLocation.href) { + // otherwise, + // the browser does not support popstate event or just does not run the event by changing the hash. + firePopState(); + } + // current event object + event = event || window.event; + + var oldURLObject = parseURL(lastURL, true); + var newURLObject = parseURL(); + // HTML4 browser not support properties oldURL/newURL + if (!event.oldURL) { + event.oldURL = oldURLObject._href; + event.newURL = newURLObject._href; + } + if (oldURLObject._hash !== newURLObject._hash) { + // if current hash not equal previous hash + dispatchEvent(event); + } + } + // new value to lastURL + lastURL = windowLocation.href; + } + + /** + * The event handler is fully loaded document + * + * @param {*} [noScroll] + * @return void + */ + function onLoad(noScroll) { + // Get rid of the events popstate when the first loading a document in the webkit browsers + setTimeout(function() { + // hang up the event handler for the built-in popstate event in the browser + addEvent('popstate', function(e) { + // set the current url, that suppress the creation of the popstate event by changing the hash + checkUrlForPopState = windowLocation.href; + // for Safari browser in OS Windows not implemented 'state' object in 'History' interface + // and not implemented in old HTML4 browsers + if (!isSupportStateObjectInHistory) { + e = redefineProperty(e, 'state', {get: function() { + return historyObject.state; + }}); + } + // send events to be processed + dispatchEvent(e); + }, false); + }, 0); + // for non-HTML5 browsers + if (!isSupportHistoryAPI && noScroll !== true && historyObject.location) { + // scroll window to anchor element + scrollToAnchorId(historyObject.location.hash); + // fire initial state for non-HTML5 browser after load page + fireInitialState(); + } + } + + /** + * Finds the closest ancestor anchor element (including the target itself). + * + * @param {HTMLElement} target The element to start scanning from. + * @return {HTMLElement} An element which is the closest ancestor anchor. + */ + function anchorTarget(target) { + while (target) { + if (target.nodeName === 'A') return target; + target = target.parentNode; + } + } + + /** + * Handles anchor elements with a hash fragment for non-HTML5 browsers + * + * @param {Event} e + */ + function onAnchorClick(e) { + var event = e || window.event; + var target = anchorTarget(event.target || event.srcElement); + var defaultPrevented = "defaultPrevented" in event ? event['defaultPrevented'] : event.returnValue === false; + if (target && target.nodeName === "A" && !defaultPrevented) { + var current = parseURL(); + var expect = parseURL(target.getAttribute("href", 2)); + var isEqualBaseURL = current._href.split('#').shift() === expect._href.split('#').shift(); + if (isEqualBaseURL && expect._hash) { + if (current._hash !== expect._hash) { + historyObject.location.hash = expect._hash; + } + scrollToAnchorId(expect._hash); + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + } + } + + /** + * Scroll page to current anchor in url-hash + * + * @param hash + */ + function scrollToAnchorId(hash) { + var target = document.getElementById(hash = (hash || '').replace(/^#/, '')); + if (target && target.id === hash && target.nodeName === "A") { + var rect = target.getBoundingClientRect(); + window.scrollTo((documentElement.scrollLeft || 0), rect.top + (documentElement.scrollTop || 0) + - (documentElement.clientTop || 0)); + } + } + + /** + * Library initialization + * + * @return {Boolean} return true if all is well, otherwise return false value + */ + function initialize() { + /** + * Get custom settings from the query string + */ + var scripts = document.getElementsByTagName('script'); + var src = (scripts[scripts.length - 1] || {}).src || ''; + var arg = src.indexOf('?') !== -1 ? src.split('?').pop() : ''; + arg.replace(/(\w+)(?:=([^&]*))?/g, function(a, key, value) { + settings[key] = (value || (key === 'basepath' ? '/' : '')).replace(/^(0|false)$/, ''); + }); + + /** + * sessionStorage throws error when cookies are disabled + * Chrome content settings when running the site in a Facebook IFrame. + * see: https://github.com/devote/HTML5-History-API/issues/34 + */ + try { + sessionStorage = window['sessionStorage']; + } catch(_e_) {} + + /** + * hang up the event handler to listen to the events hashchange + */ + addEvent(eventNamePrefix + 'hashchange', onHashChange, false); + + // a list of objects with pairs of descriptors/object + var data = [locationDescriptors, locationObject, eventsDescriptors, window, historyDescriptors, historyObject]; + + // if browser support object 'state' in interface 'History' + if (isSupportStateObjectInHistory) { + // remove state property from descriptor + delete historyDescriptors['state']; + } + + // initializing descriptors + for(var i = 0; i < data.length; i += 2) { + for(var prop in data[i]) { + if (data[i].hasOwnProperty(prop)) { + if (typeof data[i][prop] === 'function') { + // If the descriptor is a simple function, simply just assign it an object + data[i + 1][prop] = data[i][prop]; + } else { + // prepare the descriptor the required format + var descriptor = prepareDescriptorsForObject(data[i], prop, data[i][prop]); + // try to set the descriptor object + if (!redefineProperty(data[i + 1], prop, descriptor, function(n, o) { + // is satisfied if the failed override property + if (o === historyObject) { + // the problem occurs in Safari on the Mac + window.history = historyObject = data[i + 1] = n; + } + })) { + // if there is no possibility override. + // This browser does not support descriptors, such as IE7 + + // remove previously hung event handlers + removeEvent(eventNamePrefix + 'hashchange', onHashChange, false); + + // fail to initialize :( + return false; + } + + // create a repository for custom handlers onpopstate/onhashchange + if (data[i + 1] === window) { + eventsList[prop] = eventsList[prop.substr(2)] = []; + } + } + } + } + } + + // redirect if necessary + if (settings['redirect']) { + historyObject['redirect'](); + } + + // If browser does not support object 'state' in interface 'History' + if (!isSupportStateObjectInHistory && JSON) { + storageInitialize(); + } + + // track clicks on anchors + if (!isSupportHistoryAPI) { + document[addEventListenerName](eventNamePrefix + "click", onAnchorClick, false); + } + + if (document.readyState === 'complete') { + onLoad(true); + } else { + if (!isSupportHistoryAPI && parseURL()._relative !== settings["basepath"]) { + isFireInitialState = true; + } + /** + * Need to avoid triggering events popstate the initial page load. + * Hang handler popstate as will be fully loaded document that + * would prevent triggering event onpopstate + */ + addEvent(eventNamePrefix + 'load', onLoad, false); + } + + // everything went well + return true; + } + + /** + * Starting the library + */ + if (!initialize()) { + // if unable to initialize descriptors + // therefore quite old browser and there + // is no sense to continue to perform + return; + } + + /** + * If the property history.emulate will be true, + * this will be talking about what's going on + * emulation capabilities HTML5-History-API. + * Otherwise there is no emulation, ie the + * built-in browser capabilities. + * + * @type {boolean} + * @const + */ + historyObject['emulate'] = !isSupportHistoryAPI; + + /** + * Replace the original methods on the wrapper + */ + window[addEventListenerName] = addEventListener; + window[removeEventListenerName] = removeEventListener; + window[dispatchEventName] = dispatchEvent; + +})(window); +},{}],4:[function(require,module,exports){ +/*jslint onevar:true, undef:true, newcap:true, regexp:true, bitwise:true, maxerr:50, indent:4, white:false, nomen:false, plusplus:false */ +/*global define:false, require:false, exports:false, module:false, signals:false */ + +/** @license + * JS Signals + * Released under the MIT license + * Author: Miller Medeiros + * Version: 1.0.0 - Build: 268 (2012/11/29 05:48 PM) + */ + +(function(global){ + + // SignalBinding ------------------------------------------------- + //================================================================ + + /** + * Object that represents a binding between a Signal and a listener function. + *
- This is an internal constructor and shouldn't be called by regular users. + *
- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes. + * @author Miller Medeiros + * @constructor + * @internal + * @name SignalBinding + * @param {Signal} signal Reference to Signal object that listener is currently bound to. + * @param {Function} listener Handler function bound to the signal. + * @param {boolean} isOnce If binding should be executed just once. + * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). + * @param {Number} [priority] The priority level of the event listener. (default = 0). + */ + function SignalBinding(signal, listener, isOnce, listenerContext, priority) { + + /** + * Handler function bound to the signal. + * @type Function + * @private + */ + this._listener = listener; + + /** + * If binding should be executed just once. + * @type boolean + * @private + */ + this._isOnce = isOnce; + + /** + * Context on which listener will be executed (object that should represent the `this` variable inside listener function). + * @memberOf SignalBinding.prototype + * @name context + * @type Object|undefined|null + */ + this.context = listenerContext; + + /** + * Reference to Signal object that listener is currently bound to. + * @type Signal + * @private + */ + this._signal = signal; + + /** + * Listener priority + * @type Number + * @private + */ + this._priority = priority || 0; + } + + SignalBinding.prototype = { + + /** + * If binding is active and should be executed. + * @type boolean + */ + active : true, + + /** + * Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute`. (curried parameters) + * @type Array|null + */ + params : null, + + /** + * Call listener passing arbitrary parameters. + *

If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch.

+ * @param {Array} [paramsArr] Array of parameters that should be passed to the listener + * @return {*} Value returned by the listener. + */ + execute : function (paramsArr) { + var handlerReturn, params; + if (this.active && !!this._listener) { + params = this.params? this.params.concat(paramsArr) : paramsArr; + handlerReturn = this._listener.apply(this.context, params); + if (this._isOnce) { + this.detach(); + } + } + return handlerReturn; + }, + + /** + * Detach binding from signal. + * - alias to: mySignal.remove(myBinding.getListener()); + * @return {Function|null} Handler function bound to the signal or `null` if binding was previously detached. + */ + detach : function () { + return this.isBound()? this._signal.remove(this._listener, this.context) : null; + }, + + /** + * @return {Boolean} `true` if binding is still bound to the signal and have a listener. + */ + isBound : function () { + return (!!this._signal && !!this._listener); + }, + + /** + * @return {boolean} If SignalBinding will only be executed once. + */ + isOnce : function () { + return this._isOnce; + }, + + /** + * @return {Function} Handler function bound to the signal. + */ + getListener : function () { + return this._listener; + }, + + /** + * @return {Signal} Signal that listener is currently bound to. + */ + getSignal : function () { + return this._signal; + }, + + /** + * Delete instance properties + * @private + */ + _destroy : function () { + delete this._signal; + delete this._listener; + delete this.context; + }, + + /** + * @return {string} String representation of the object. + */ + toString : function () { + return '[SignalBinding isOnce:' + this._isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']'; + } + + }; + + +/*global SignalBinding:false*/ + + // Signal -------------------------------------------------------- + //================================================================ + + function validateListener(listener, fnName) { + if (typeof listener !== 'function') { + throw new Error( 'listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName) ); + } + } + + /** + * Custom event broadcaster + *
- inspired by Robert Penner's AS3 Signals. + * @name Signal + * @author Miller Medeiros + * @constructor + */ + function Signal() { + /** + * @type Array. + * @private + */ + this._bindings = []; + this._prevParams = null; + + // enforce dispatch to aways work on same context (#47) + var self = this; + this.dispatch = function(){ + Signal.prototype.dispatch.apply(self, arguments); + }; + } + + Signal.prototype = { + + /** + * Signals Version Number + * @type String + * @const + */ + VERSION : '1.0.0', + + /** + * If Signal should keep record of previously dispatched parameters and + * automatically execute listener during `add()`/`addOnce()` if Signal was + * already dispatched before. + * @type boolean + */ + memorize : false, + + /** + * @type boolean + * @private + */ + _shouldPropagate : true, + + /** + * If Signal is active and should broadcast events. + *

IMPORTANT: Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead.

+ * @type boolean + */ + active : true, + + /** + * @param {Function} listener + * @param {boolean} isOnce + * @param {Object} [listenerContext] + * @param {Number} [priority] + * @return {SignalBinding} + * @private + */ + _registerListener : function (listener, isOnce, listenerContext, priority) { + + var prevIndex = this._indexOfListener(listener, listenerContext), + binding; + + if (prevIndex !== -1) { + binding = this._bindings[prevIndex]; + if (binding.isOnce() !== isOnce) { + throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.'); + } + } else { + binding = new SignalBinding(this, listener, isOnce, listenerContext, priority); + this._addBinding(binding); + } + + if(this.memorize && this._prevParams){ + binding.execute(this._prevParams); + } + + return binding; + }, + + /** + * @param {SignalBinding} binding + * @private + */ + _addBinding : function (binding) { + //simplified insertion sort + var n = this._bindings.length; + do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); + this._bindings.splice(n + 1, 0, binding); + }, + + /** + * @param {Function} listener + * @return {number} + * @private + */ + _indexOfListener : function (listener, context) { + var n = this._bindings.length, + cur; + while (n--) { + cur = this._bindings[n]; + if (cur._listener === listener && cur.context === context) { + return n; + } + } + return -1; + }, + + /** + * Check if listener was attached to Signal. + * @param {Function} listener + * @param {Object} [context] + * @return {boolean} if Signal has the specified listener. + */ + has : function (listener, context) { + return this._indexOfListener(listener, context) !== -1; + }, + + /** + * Add a listener to the signal. + * @param {Function} listener Signal handler function. + * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). + * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) + * @return {SignalBinding} An Object representing the binding between the Signal and listener. + */ + add : function (listener, listenerContext, priority) { + validateListener(listener, 'add'); + return this._registerListener(listener, false, listenerContext, priority); + }, + + /** + * Add listener to the signal that should be removed after first execution (will be executed only once). + * @param {Function} listener Signal handler function. + * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). + * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) + * @return {SignalBinding} An Object representing the binding between the Signal and listener. + */ + addOnce : function (listener, listenerContext, priority) { + validateListener(listener, 'addOnce'); + return this._registerListener(listener, true, listenerContext, priority); + }, + + /** + * Remove a single listener from the dispatch queue. + * @param {Function} listener Handler function that should be removed. + * @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context). + * @return {Function} Listener handler function. + */ + remove : function (listener, context) { + validateListener(listener, 'remove'); + + var i = this._indexOfListener(listener, context); + if (i !== -1) { + this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal + this._bindings.splice(i, 1); + } + return listener; + }, + + /** + * Remove all listeners from the Signal. + */ + removeAll : function () { + var n = this._bindings.length; + while (n--) { + this._bindings[n]._destroy(); + } + this._bindings.length = 0; + }, + + /** + * @return {number} Number of listeners attached to the Signal. + */ + getNumListeners : function () { + return this._bindings.length; + }, + + /** + * Stop propagation of the event, blocking the dispatch to next listeners on the queue. + *

IMPORTANT: should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.

+ * @see Signal.prototype.disable + */ + halt : function () { + this._shouldPropagate = false; + }, + + /** + * Dispatch/Broadcast Signal to all listeners added to the queue. + * @param {...*} [params] Parameters that should be passed to each handler. + */ + dispatch : function (params) { + if (! this.active) { + return; + } + + var paramsArr = Array.prototype.slice.call(arguments), + n = this._bindings.length, + bindings; + + if (this.memorize) { + this._prevParams = paramsArr; + } + + if (! n) { + //should come after memorize + return; + } + + bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch + this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. + + //execute all callbacks until end of the list or until a callback returns `false` or stops propagation + //reverse loop since listeners with higher priority will be added at the end of the list + do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); + }, + + /** + * Forget memorized arguments. + * @see Signal.memorize + */ + forget : function(){ + this._prevParams = null; + }, + + /** + * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). + *

IMPORTANT: calling any method on the signal instance after calling dispose will throw errors.

+ */ + dispose : function () { + this.removeAll(); + delete this._bindings; + delete this._prevParams; + }, + + /** + * @return {string} String representation of the object. + */ + toString : function () { + return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']'; + } + + }; + + + // Namespace ----------------------------------------------------- + //================================================================ + + /** + * Signals namespace + * @namespace + * @name signals + */ + var signals = Signal; + + /** + * Custom event broadcaster + * @see Signal + */ + // alias for backwards compatibility (see #gh-44) + signals.Signal = Signal; + + + + //exports to multiple environments + if(typeof define === 'function' && define.amd){ //AMD + define(function () { return signals; }); + } else if (typeof module !== 'undefined' && module.exports){ //node + module.exports = signals; + } else { //browser + //use string because of Google closure compiler ADVANCED_MODE + /*jslint sub:true */ + global['signals'] = signals; + } + +}(this)); + +},{}],5:[function(require,module,exports){ +var process=require("__browserify_process");/** @license MIT License (c) copyright 2011-2013 original author or authors */ + +/** + * A lightweight CommonJS Promises/A and when() implementation + * when is part of the cujo.js family of libraries (http://cujojs.com/) + * + * Licensed under the MIT License at: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Brian Cavalier + * @author John Hann + * @version 2.5.1 + */ +(function(define, global) { 'use strict'; +define(function (require) { + + // Public API + + when.promise = promise; // Create a pending promise + when.resolve = resolve; // Create a resolved promise + when.reject = reject; // Create a rejected promise + when.defer = defer; // Create a {promise, resolver} pair + + when.join = join; // Join 2 or more promises + + when.all = all; // Resolve a list of promises + when.map = map; // Array.map() for promises + when.reduce = reduce; // Array.reduce() for promises + when.settle = settle; // Settle a list of promises + + when.any = any; // One-winner race + when.some = some; // Multi-winner race + + when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike + when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable + + /** + * Register an observer for a promise or immediate value. + * + * @param {*} promiseOrValue + * @param {function?} [onFulfilled] callback to be called when promiseOrValue is + * successfully fulfilled. If promiseOrValue is an immediate value, callback + * will be invoked immediately. + * @param {function?} [onRejected] callback to be called when promiseOrValue is + * rejected. + * @param {function?} [onProgress] callback to be called when progress updates + * are issued for promiseOrValue. + * @returns {Promise} a new {@link Promise} that will complete with the return + * value of callback or errback or the completion value of promiseOrValue if + * callback and/or errback is not supplied. + */ + function when(promiseOrValue, onFulfilled, onRejected, onProgress) { + // Get a trusted promise for the input promiseOrValue, and then + // register promise handlers + return cast(promiseOrValue).then(onFulfilled, onRejected, onProgress); + } + + function cast(x) { + return x instanceof Promise ? x : resolve(x); + } + + /** + * Trusted Promise constructor. A Promise created from this constructor is + * a trusted when.js promise. Any other duck-typed promise is considered + * untrusted. + * @constructor + * @param {function} sendMessage function to deliver messages to the promise's handler + * @param {function?} inspect function that reports the promise's state + * @name Promise + */ + function Promise(sendMessage, inspect) { + this._message = sendMessage; + this.inspect = inspect; + } + + Promise.prototype = { + /** + * Register handlers for this promise. + * @param [onFulfilled] {Function} fulfillment handler + * @param [onRejected] {Function} rejection handler + * @param [onProgress] {Function} progress handler + * @return {Promise} new Promise + */ + then: function(onFulfilled, onRejected, onProgress) { + /*jshint unused:false*/ + var args, sendMessage; + + args = arguments; + sendMessage = this._message; + + return _promise(function(resolve, reject, notify) { + sendMessage('when', args, resolve, notify); + }, this._status && this._status.observed()); + }, + + /** + * Register a rejection handler. Shortcut for .then(undefined, onRejected) + * @param {function?} onRejected + * @return {Promise} + */ + otherwise: function(onRejected) { + return this.then(undef, onRejected); + }, + + /** + * Ensures that onFulfilledOrRejected will be called regardless of whether + * this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT + * receive the promises' value or reason. Any returned value will be disregarded. + * onFulfilledOrRejected may throw or return a rejected promise to signal + * an additional error. + * @param {function} onFulfilledOrRejected handler to be called regardless of + * fulfillment or rejection + * @returns {Promise} + */ + ensure: function(onFulfilledOrRejected) { + return typeof onFulfilledOrRejected === 'function' + ? this.then(injectHandler, injectHandler)['yield'](this) + : this; + + function injectHandler() { + return resolve(onFulfilledOrRejected()); + } + }, + + /** + * Shortcut for .then(function() { return value; }) + * @param {*} value + * @return {Promise} a promise that: + * - is fulfilled if value is not a promise, or + * - if value is a promise, will fulfill with its value, or reject + * with its reason. + */ + 'yield': function(value) { + return this.then(function() { + return value; + }); + }, + + /** + * Runs a side effect when this promise fulfills, without changing the + * fulfillment value. + * @param {function} onFulfilledSideEffect + * @returns {Promise} + */ + tap: function(onFulfilledSideEffect) { + return this.then(onFulfilledSideEffect)['yield'](this); + }, + + /** + * Assumes that this promise will fulfill with an array, and arranges + * for the onFulfilled to be called with the array as its argument list + * i.e. onFulfilled.apply(undefined, array). + * @param {function} onFulfilled function to receive spread arguments + * @return {Promise} + */ + spread: function(onFulfilled) { + return this.then(function(array) { + // array may contain promises, so resolve its contents. + return all(array, function(array) { + return onFulfilled.apply(undef, array); + }); + }); + }, + + /** + * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected) + * @deprecated + */ + always: function(onFulfilledOrRejected, onProgress) { + return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); + } + }; + + /** + * Returns a resolved promise. The returned promise will be + * - fulfilled with promiseOrValue if it is a value, or + * - if promiseOrValue is a promise + * - fulfilled with promiseOrValue's value after it is fulfilled + * - rejected with promiseOrValue's reason after it is rejected + * @param {*} value + * @return {Promise} + */ + function resolve(value) { + return promise(function(resolve) { + resolve(value); + }); + } + + /** + * Returns a rejected promise for the supplied promiseOrValue. The returned + * promise will be rejected with: + * - promiseOrValue, if it is a value, or + * - if promiseOrValue is a promise + * - promiseOrValue's value after it is fulfilled + * - promiseOrValue's reason after it is rejected + * @param {*} promiseOrValue the rejected value of the returned {@link Promise} + * @return {Promise} rejected {@link Promise} + */ + function reject(promiseOrValue) { + return when(promiseOrValue, rejected); + } + + /** + * Creates a {promise, resolver} pair, either or both of which + * may be given out safely to consumers. + * The resolver has resolve, reject, and progress. The promise + * has then plus extended promise API. + * + * @return {{ + * promise: Promise, + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise + * resolver: { + * resolve: function:Promise, + * reject: function:Promise, + * notify: function:Promise + * }}} + */ + function defer() { + var deferred, pending, resolved; + + // Optimize object shape + deferred = { + promise: undef, resolve: undef, reject: undef, notify: undef, + resolver: { resolve: undef, reject: undef, notify: undef } + }; + + deferred.promise = pending = promise(makeDeferred); + + return deferred; + + function makeDeferred(resolvePending, rejectPending, notifyPending) { + deferred.resolve = deferred.resolver.resolve = function(value) { + if(resolved) { + return resolve(value); + } + resolved = true; + resolvePending(value); + return pending; + }; + + deferred.reject = deferred.resolver.reject = function(reason) { + if(resolved) { + return resolve(rejected(reason)); + } + resolved = true; + rejectPending(reason); + return pending; + }; + + deferred.notify = deferred.resolver.notify = function(update) { + notifyPending(update); + return update; + }; + } + } + + /** + * Creates a new promise whose fate is determined by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @returns {Promise} promise whose fate is determine by resolver + */ + function promise(resolver) { + return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + } + + /** + * Creates a new promise, linked to parent, whose fate is determined + * by resolver. + * @param {function} resolver function(resolve, reject, notify) + * @param {Promise?} status promise from which the new promise is begotten + * @returns {Promise} promise whose fate is determine by resolver + * @private + */ + function _promise(resolver, status) { + var self, value, consumers = []; + + self = new Promise(_message, inspect); + self._status = status; + + // Call the provider resolver to seal the promise's fate + try { + resolver(promiseResolve, promiseReject, promiseNotify); + } catch(e) { + promiseReject(e); + } + + // Return the promise + return self; + + /** + * Private message delivery. Queues and delivers messages to + * the promise's ultimate fulfillment value or rejection reason. + * @private + * @param {String} type + * @param {Array} args + * @param {Function} resolve + * @param {Function} notify + */ + function _message(type, args, resolve, notify) { + consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); + + function deliver(p) { + p._message(type, args, resolve, notify); + } + } + + /** + * Returns a snapshot of the promise's state at the instant inspect() + * is called. The returned object is not live and will not update as + * the promise's state changes. + * @returns {{ state:String, value?:*, reason?:* }} status snapshot + * of the promise. + */ + function inspect() { + return value ? value.inspect() : toPendingState(); + } + + /** + * Transition from pre-resolution state to post-resolution state, notifying + * all listeners of the ultimate fulfillment or rejection + * @param {*|Promise} val resolution value + */ + function promiseResolve(val) { + if(!consumers) { + return; + } + + var queue = consumers; + consumers = undef; + + enqueue(function () { + value = coerce(self, val); + if(status) { + updateStatus(value, status); + } + runHandlers(queue, value); + }); + + } + + /** + * Reject this promise with the supplied reason, which will be used verbatim. + * @param {*} reason reason for the rejection + */ + function promiseReject(reason) { + promiseResolve(rejected(reason)); + } + + /** + * Issue a progress event, notifying all progress listeners + * @param {*} update progress event payload to pass to all listeners + */ + function promiseNotify(update) { + if(consumers) { + var queue = consumers; + enqueue(function () { + runHandlers(queue, progressed(update)); + }); + } + } + } + + /** + * Run a queue of functions as quickly as possible, passing + * value to each. + */ + function runHandlers(queue, value) { + for (var i = 0; i < queue.length; i++) { + queue[i](value); + } + } + + /** + * Creates a fulfilled, local promise as a proxy for a value + * NOTE: must never be exposed + * @param {*} value fulfillment value + * @returns {Promise} + */ + function fulfilled(value) { + return near( + new NearFulfilledProxy(value), + function() { return toFulfilledState(value); } + ); + } + + /** + * Creates a rejected, local promise with the supplied reason + * NOTE: must never be exposed + * @param {*} reason rejection reason + * @returns {Promise} + */ + function rejected(reason) { + return near( + new NearRejectedProxy(reason), + function() { return toRejectedState(reason); } + ); + } + + /** + * Creates a near promise using the provided proxy + * NOTE: must never be exposed + * @param {object} proxy proxy for the promise's ultimate value or reason + * @param {function} inspect function that returns a snapshot of the + * returned near promise's state + * @returns {Promise} + */ + function near(proxy, inspect) { + return new Promise(function (type, args, resolve) { + try { + resolve(proxy[type].apply(proxy, args)); + } catch(e) { + resolve(rejected(e)); + } + }, inspect); + } + + /** + * Create a progress promise with the supplied update. + * @private + * @param {*} update + * @return {Promise} progress promise + */ + function progressed(update) { + return new Promise(function (type, args, _, notify) { + var onProgress = args[2]; + try { + notify(typeof onProgress === 'function' ? onProgress(update) : update); + } catch(e) { + notify(e); + } + }); + } + + /** + * Coerces x to a trusted Promise + * @param {*} x thing to coerce + * @returns {*} Guaranteed to return a trusted Promise. If x + * is trusted, returns x, otherwise, returns a new, trusted, already-resolved + * Promise whose resolution value is: + * * the resolution value of x if it's a foreign promise, or + * * x if it's a value + */ + function coerce(self, x) { + if (x === self) { + return rejected(new TypeError()); + } + + if (x instanceof Promise) { + return x; + } + + try { + var untrustedThen = x === Object(x) && x.then; + + return typeof untrustedThen === 'function' + ? assimilate(untrustedThen, x) + : fulfilled(x); + } catch(e) { + return rejected(e); + } + } + + /** + * Safely assimilates a foreign thenable by wrapping it in a trusted promise + * @param {function} untrustedThen x's then() method + * @param {object|function} x thenable + * @returns {Promise} + */ + function assimilate(untrustedThen, x) { + return promise(function (resolve, reject) { + fcall(untrustedThen, x, resolve, reject); + }); + } + + /** + * Proxy for a near, fulfilled value + * @param {*} value + * @constructor + */ + function NearFulfilledProxy(value) { + this.value = value; + } + + NearFulfilledProxy.prototype.when = function(onResult) { + return typeof onResult === 'function' ? onResult(this.value) : this.value; + }; + + /** + * Proxy for a near rejection + * @param {*} reason + * @constructor + */ + function NearRejectedProxy(reason) { + this.reason = reason; + } + + NearRejectedProxy.prototype.when = function(_, onError) { + if(typeof onError === 'function') { + return onError(this.reason); + } else { + throw this.reason; + } + }; + + function updateStatus(value, status) { + value.then(statusFulfilled, statusRejected); + + function statusFulfilled() { status.fulfilled(); } + function statusRejected(r) { status.rejected(r); } + } + + /** + * Determines if x is promise-like, i.e. a thenable object + * NOTE: Will return true for *any thenable object*, and isn't truly + * safe, since it may attempt to access the `then` property of x (i.e. + * clever/malicious getters may do weird things) + * @param {*} x anything + * @returns {boolean} true if x is promise-like + */ + function isPromiseLike(x) { + return x && typeof x.then === 'function'; + } + + /** + * Initiates a competitive race, returning a promise that will resolve when + * howMany of the supplied promisesOrValues have resolved, or will reject when + * it becomes impossible for howMany to resolve, for example, when + * (promisesOrValues.length - howMany) + 1 input promises reject. + * + * @param {Array} promisesOrValues array of anything, may contain a mix + * of promises and values + * @param howMany {number} number of promisesOrValues to resolve + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() + * @returns {Promise} promise that will resolve to an array of howMany values that + * resolved first, or will reject with an array of + * (promisesOrValues.length - howMany) + 1 rejection reasons. + */ + function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { + + return when(promisesOrValues, function(promisesOrValues) { + + return promise(resolveSome).then(onFulfilled, onRejected, onProgress); + + function resolveSome(resolve, reject, notify) { + var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i; + + len = promisesOrValues.length >>> 0; + + toResolve = Math.max(0, Math.min(howMany, len)); + values = []; + + toReject = (len - toResolve) + 1; + reasons = []; + + // No items in the input, resolve immediately + if (!toResolve) { + resolve(values); + + } else { + rejectOne = function(reason) { + reasons.push(reason); + if(!--toReject) { + fulfillOne = rejectOne = identity; + reject(reasons); + } + }; + + fulfillOne = function(val) { + // This orders the values based on promise resolution order + values.push(val); + if (!--toResolve) { + fulfillOne = rejectOne = identity; + resolve(values); + } + }; + + for(i = 0; i < len; ++i) { + if(i in promisesOrValues) { + when(promisesOrValues[i], fulfiller, rejecter, notify); + } + } + } + + function rejecter(reason) { + rejectOne(reason); + } + + function fulfiller(val) { + fulfillOne(val); + } + } + }); + } + + /** + * Initiates a competitive race, returning a promise that will resolve when + * any one of the supplied promisesOrValues has resolved or will reject when + * *all* promisesOrValues have rejected. + * + * @param {Array|Promise} promisesOrValues array of anything, may contain a mix + * of {@link Promise}s and values + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() + * @returns {Promise} promise that will resolve to the value that resolved first, or + * will reject with an array of all rejected inputs. + */ + function any(promisesOrValues, onFulfilled, onRejected, onProgress) { + + function unwrapSingleResult(val) { + return onFulfilled ? onFulfilled(val[0]) : val[0]; + } + + return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress); + } + + /** + * Return a promise that will resolve only once all the supplied promisesOrValues + * have resolved. The resolution value of the returned promise will be an array + * containing the resolution values of each of the promisesOrValues. + * @memberOf when + * + * @param {Array|Promise} promisesOrValues array of anything, may contain a mix + * of {@link Promise}s and values + * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() + * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() + * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() + * @returns {Promise} + */ + function all(promisesOrValues, onFulfilled, onRejected, onProgress) { + return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); + } + + /** + * Joins multiple promises into a single returned promise. + * @return {Promise} a promise that will fulfill when *all* the input promises + * have fulfilled, or will reject when *any one* of the input promises rejects. + */ + function join(/* ...promises */) { + return _map(arguments, identity); + } + + /** + * Settles all input promises such that they are guaranteed not to + * be pending once the returned promise fulfills. The returned promise + * will always fulfill, except in the case where `array` is a promise + * that rejects. + * @param {Array|Promise} array or promise for array of promises to settle + * @returns {Promise} promise that always fulfills with an array of + * outcome snapshots for each input promise. + */ + function settle(array) { + return _map(array, toFulfilledState, toRejectedState); + } + + /** + * Promise-aware array map function, similar to `Array.prototype.map()`, + * but input array may contain promises or values. + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function map(array, mapFunc) { + return _map(array, mapFunc); + } + + /** + * Internal map that allows a fallback to handle rejections + * @param {Array|Promise} array array of anything, may contain promises and values + * @param {function} mapFunc map function which may return a promise or value + * @param {function?} fallback function to handle rejected promises + * @returns {Promise} promise that will fulfill with an array of mapped values + * or reject if any input promise rejects. + */ + function _map(array, mapFunc, fallback) { + return when(array, function(array) { + + return _promise(resolveMap); + + function resolveMap(resolve, reject, notify) { + var results, len, toResolve, i; + + // Since we know the resulting length, we can preallocate the results + // array to avoid array expansions. + toResolve = len = array.length >>> 0; + results = []; + + if(!toResolve) { + resolve(results); + return; + } + + // Since mapFunc may be async, get all invocations of it into flight + for(i = 0; i < len; i++) { + if(i in array) { + resolveOne(array[i], i); + } else { + --toResolve; + } + } + + function resolveOne(item, i) { + when(item, mapFunc, fallback).then(function(mapped) { + results[i] = mapped; + + if(!--toResolve) { + resolve(results); + } + }, reject, notify); + } + } + }); + } + + /** + * Traditional reduce function, similar to `Array.prototype.reduce()`, but + * input may contain promises and/or values, and reduceFunc + * may return either a value or a promise, *and* initialValue may + * be a promise for the starting value. + * + * @param {Array|Promise} promise array or promise for an array of anything, + * may contain a mix of promises and values. + * @param {function} reduceFunc reduce function reduce(currentValue, nextValue, index, total), + * where total is the total number of items being reduced, and will be the same + * in each call to reduceFunc. + * @returns {Promise} that will resolve to the final reduced value + */ + function reduce(promise, reduceFunc /*, initialValue */) { + var args = fcall(slice, arguments, 1); + + return when(promise, function(array) { + var total; + + total = array.length; + + // Wrap the supplied reduceFunc with one that handles promises and then + // delegates to the supplied. + args[0] = function (current, val, i) { + return when(current, function (c) { + return when(val, function (value) { + return reduceFunc(c, value, i, total); + }); + }); + }; + + return reduceArray.apply(array, args); + }); + } + + // Snapshot states + + /** + * Creates a fulfilled state snapshot + * @private + * @param {*} x any value + * @returns {{state:'fulfilled',value:*}} + */ + function toFulfilledState(x) { + return { state: 'fulfilled', value: x }; + } + + /** + * Creates a rejected state snapshot + * @private + * @param {*} x any reason + * @returns {{state:'rejected',reason:*}} + */ + function toRejectedState(x) { + return { state: 'rejected', reason: x }; + } + + /** + * Creates a pending state snapshot + * @private + * @returns {{state:'pending'}} + */ + function toPendingState() { + return { state: 'pending' }; + } + + // + // Internals, utilities, etc. + // + + var reduceArray, slice, fcall, nextTick, handlerQueue, + setTimeout, funcProto, call, arrayProto, monitorApi, + cjsRequire, MutationObserver, undef; + + cjsRequire = require; + + // + // Shared handler queue processing + // + // Credit to Twisol (https://github.com/Twisol) for suggesting + // this type of extensible queue + trampoline approach for + // next-tick conflation. + + handlerQueue = []; + + /** + * Enqueue a task. If the queue is not currently scheduled to be + * drained, schedule it. + * @param {function} task + */ + function enqueue(task) { + if(handlerQueue.push(task) === 1) { + nextTick(drainQueue); + } + } + + /** + * Drain the handler queue entirely, being careful to allow the + * queue to be extended while it is being processed, and to continue + * processing until it is truly empty. + */ + function drainQueue() { + runHandlers(handlerQueue); + handlerQueue = []; + } + + // capture setTimeout to avoid being caught by fake timers + // used in time based tests + setTimeout = global.setTimeout; + + // Allow attaching the monitor to when() if env has no console + monitorApi = typeof console != 'undefined' ? console : when; + + // Sniff "best" async scheduling option + // Prefer process.nextTick or MutationObserver, then check for + // vertx and finally fall back to setTimeout + /*global process*/ + if (typeof process === 'object' && process.nextTick) { + nextTick = process.nextTick; + } else if(MutationObserver = global.MutationObserver || global.WebKitMutationObserver) { + nextTick = (function(document, MutationObserver, drainQueue) { + var el = document.createElement('div'); + new MutationObserver(drainQueue).observe(el, { attributes: true }); + + return function() { + el.setAttribute('x', 'x'); + }; + }(document, MutationObserver, drainQueue)); + } else { + try { + // vert.x 1.x || 2.x + nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; + } catch(ignore) { + nextTick = function(t) { setTimeout(t, 0); }; + } + } + + // + // Capture/polyfill function and array utils + // + + // Safe function calls + funcProto = Function.prototype; + call = funcProto.call; + fcall = funcProto.bind + ? call.bind(call) + : function(f, context) { + return f.apply(context, slice.call(arguments, 2)); + }; + + // Safe array ops + arrayProto = []; + slice = arrayProto.slice; + + // ES5 reduce implementation if native not available + // See: http://es5.github.com/#x15.4.4.21 as there are many + // specifics and edge cases. ES5 dictates that reduce.length === 1 + // This implementation deviates from ES5 spec in the following ways: + // 1. It does not check if reduceFunc is a Callable + reduceArray = arrayProto.reduce || + function(reduceFunc /*, initialValue */) { + /*jshint maxcomplexity: 7*/ + var arr, args, reduced, len, i; + + i = 0; + arr = Object(this); + len = arr.length >>> 0; + args = arguments; + + // If no initialValue, use first item of array (we know length !== 0 here) + // and adjust i to start at second item + if(args.length <= 1) { + // Skip to the first real element in the array + for(;;) { + if(i in arr) { + reduced = arr[i++]; + break; + } + + // If we reached the end of the array without finding any real + // elements, it's a TypeError + if(++i >= len) { + throw new TypeError(); + } + } + } else { + // If initialValue provided, use it + reduced = args[1]; + } + + // Do the actual reduce + for(;i < len; ++i) { + if(i in arr) { + reduced = reduceFunc(reduced, arr[i], i, arr); + } + } + + return reduced; + }; + + function identity(x) { + return x; + } + + return when; +}); +})(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); + +},{"__browserify_process":2}],6:[function(require,module,exports){ + +var Signal = require('signals').Signal, + crossroads = require('crossroads'), + + interceptAnchorClicks = require('./anchorClicks'), + StateWithParams = require('./StateWithParams'), + Transition = require('./Transition'), + util = require('./util'); + +/* +* Create a new Router instance, passing any state defined declaratively. +* More states can be added using addState() before the router is initialized. +* +* Because a router manages global state (the URL), only one instance of Router +* should be used inside an application. +*/ +function Router(declarativeStates) { + var router = {}, + states = util.copyObject(declarativeStates), + roads = crossroads.create(), + firstTransition = true, + initOptions = { + enableLogs: false, + interceptAnchorClicks: true + }, + currentPathQuery, + currentState, + transition, + leafStates, + stateFound, + poppedState, + initialized; + + // Routes params should be type casted. e.g the dynamic path items/:id when id is 33 + // will end up passing the integer 33 as an argument, not the string "33". + roads.shouldTypecast = true; + // Nil transitions are prevented from our side. + roads.ignoreState = true; + + /* + * Setting a new state will start a transition from the current state to the target state. + * A successful transition will result in the URL being changed. + * A failed transition will leave the router in its current state. + */ + function setState(state, params) { + if (isSameState(state, params)) return; + + var fromState; + var toState = StateWithParams(state, params); + + if (transition) { + cancelTransition(); + fromState = StateWithParams(transition.currentState, transition.toParams); + } + else { + fromState = currentState; + } + + // While the transition is running, any code asking the router about the current state should + // get the end result state. The currentState is rollbacked if the transition fails. + currentState = toState; + + startingTransition(fromState, toState); + + transition = Transition( + fromState, + toState, + paramDiff(fromState && fromState.params, params)); + + transition.then( + function success() { + var historyState; + + transition = null; + + if (!poppedState && !firstTransition) { + historyState = ('/' + currentPathQuery).replace('//', '/'); + log('Pushing state: {0}', historyState); + history.pushState(historyState, document.title, historyState); + } + + transitionCompleted(fromState, toState); + }, + function fail(error) { + transition = null; + currentState = fromState; + + logError('Transition from {0} to {1} failed: {2}', fromState, toState, error); + router.transition.failed.dispatch(fromState, toState); + }); + } + + function cancelTransition() { + log('Cancelling existing transition from {0} to {1}', + transition.from, transition.to); + + transition.cancel(); + + router.transition.cancelled.dispatch(transition.from, transition.to); + } + + function startingTransition(fromState, toState) { + log('Starting transition from {0} to {1}', fromState, toState); + + router.transition.started.dispatch(fromState, toState); + } + + function transitionCompleted(fromState, toState) { + log('Transition from {0} to {1} completed', fromState, toState); + + router.transition.completed.dispatch(fromState, toState); + firstTransition = false; + } + + /* + * Return whether the passed state is the same as the current one; + * in which case the router can ignore the change. + */ + function isSameState(newState, newParams) { + var state, params, diff; + + if (transition) { + state = transition.to; + params = transition.toParams; + } + else if (currentState) { + state = currentState._state; + params = currentState.params; + } + + diff = paramDiff(params, newParams); + + return (newState == state) && (util.objectSize(diff) == 0); + } + + /* + * Return the set of all the params that changed (Either added, removed or changed). + */ + function paramDiff(oldParams, newParams) { + var diff = {}, + oldParams = oldParams || {}; + + for (var name in oldParams) + if (oldParams[name] != newParams[name]) diff[name] = 1; + + for (var name in newParams) + if (oldParams[name] != newParams[name]) diff[name] = 1; + + return diff; + } + + /* + * The state wasn't found; + * Transition to the 'notFound' state if the developer specified it or else throw an error. + */ + function notFound(state) { + log('State not found: {0}', state); + + if (states.notFound) setState(states.notFound); + else throw new Error ('State "' + state + '" could not be found'); + } + + /* + * Configure the router before its initialization. + */ + function configure(options) { + util.mergeObjects(initOptions, options); + return router; + } + + /* + * Initialize and freeze the router (states can not be added afterwards). + * The router will immediately initiate a transition to, in order of priority: + * 1) The state captured by the current URL + * 2) The init state passed as an argument + * 3) The default state (pathless and queryless) + */ + function init(initState) { + if (initOptions.enableLogs) + Router.enableLogs(); + + if (initOptions.interceptAnchorClicks) + interceptAnchorClicks(router); + + log('Router init'); + initStates(); + + var initialState = (!Router.ignoreInitialURL && urlPathQuery()) || initState || ''; + + log('Initializing to state {0}', initialState || '""'); + state(initialState); + + window.onpopstate = function(evt) { + // history.js will dispatch fake popstate events on HTML4 browsers' hash changes; + // in these cases, evt.state is null. + var newState = evt.state || urlPathQuery(); + + log('Popped state: {0}', newState); + poppedState = true; + setStateForPathQuery(newState); + }; + + initialized = true; + return router; + } + + function initStates() { + eachRootState(function(name, state) { + state.init(name); + }); + + leafStates = {}; + + // Only leaf states can be transitioned to. + eachLeafState(function(state) { + leafStates[state.fullName] = state; + + state.route = roads.addRoute(state.fullPath() + ":?query:"); + state.route.matched.add(function() { + stateFound = true; + setState(state, toParams(state, arguments)); + }); + }); + } + + function eachRootState(callback) { + for (var name in states) callback(name, states[name]); + } + + function eachLeafState(callback) { + var name, state; + + function callbackIfLeaf(states) { + states.forEach(function(state) { + if (state.children.length) + callbackIfLeaf(state.children); + else + callback(state); + }); + } + + callbackIfLeaf(util.objectToArray(states)); + } + + /* + * Request a programmatic state change. + * + * Two notations are supported: + * state('my.target.state', {id: 33, filter: 'desc'}) + * state('target/33?filter=desc') + */ + function state(pathQueryOrName, params) { + var isName = (pathQueryOrName.indexOf('.') > -1 || leafStates[pathQueryOrName]); + + log('Changing state to {0}', pathQueryOrName || '""'); + + poppedState = false; + if (isName) setStateByName(pathQueryOrName, params || {}); + else setStateForPathQuery(pathQueryOrName); + } + + /* + * An alias of 'state'. You can use 'redirect' when it makes more sense semantically. + */ + function redirect(pathQueryOrName, params) { + log('Redirecting...'); + state(pathQueryOrName, params); + } + + function setStateForPathQuery(pathQuery) { + currentPathQuery = pathQuery; + stateFound = false; + roads.parse(pathQuery); + + if (!stateFound) notFound(pathQuery); + } + + function setStateByName(name, params) { + var state = leafStates[name]; + + if (!state) return notFound(name); + + var pathQuery = state.route.interpolate(params); + setStateForPathQuery(pathQuery); + } + + /* + * Add a new root state to the router. + * The name must be unique among root states. + */ + function addState(name, state) { + if (initialized) + throw new Error('States can only be added before the Router is initialized'); + + if (states[name]) + throw new Error('A state already exist in the router with the name ' + name); + + log('Adding state {0}', name); + + states[name] = state; + + return router; + } + + function urlPathQuery() { + var hashSlash = location.href.indexOf('#/'); + return hashSlash > -1 + ? location.href.slice(hashSlash + 2) + : (location.pathname + location.search).slice(1); + } + + /* + * Translate the crossroads argument format to what we want to use. + * We want to keep the path and query names and merge them all in one object for convenience. + */ + function toParams(state, crossroadsArgs) { + var args = Array.prototype.slice.apply(crossroadsArgs), + query = args.pop(), + params = {}, + pathName; + + state.fullPath().replace(/\{\w*\}/g, function(match) { + pathName = match.slice(1, -1); + params[pathName] = args.shift(); + return ''; + }); + + if (query) util.mergeObjects(params, query); + + // Decode all params + for (var i in params) { + if (util.isString(params[i])) params[i] = decodeURIComponent(params[i]); + } + + return params; + } + + /* + * Compute a link that can be used in anchors' href attributes + * from a state name and a list of params, a.k.a reverse routing. + */ + function link(stateName, params) { + var query = {}, + allQueryParams = {}, + hasQuery = false, + state = leafStates[stateName]; + + if (!state) throw new Error('Cannot find state ' + stateName); + + [state].concat(state.parents).forEach(function(s) { + util.mergeObjects(allQueryParams, s.queryParams); + }); + + // The passed params are path and query params lumped together, + // Separate them for crossroads' to compute its interpolation. + for (var key in params) { + if (allQueryParams[key]) { + query[key] = params[key]; + delete params[key]; + hasQuery = true; + } + } + + if (hasQuery) params.query = query; + + return '/' + state.route.interpolate(params).replace('/?', '?'); + } + + /* + * Returns a StateWithParams object representing the current state of the router. + */ + function getCurrentState() { + return currentState; + } + + + // Public methods + + router.configure = configure; + router.init = init; + router.state = state; + router.redirect = redirect; + router.addState = addState; + router.link = link; + router.currentState = getCurrentState; + + + // Signals + + router.transition = { + // Dispatched when a transition started. + started: new Signal(), + // Dispatched when a transition either completed, failed or got cancelled. + ended: new Signal(), + // Dispatched when a transition successfuly completed + completed: new Signal(), + // Dispatched when a transition failed to complete + failed: new Signal(), + // Dispatched when a transition got cancelled + cancelled: new Signal() + }; + + // Dispatched once after the router successfully reached its initial state. + router.initialized = new Signal(); + + router.transition.completed.addOnce(function() { + router.initialized.dispatch(); + }); + + router.transition.completed.add(transitionEnded); + router.transition.failed.add(transitionEnded); + router.transition.cancelled.add(transitionEnded); + + function transitionEnded(oldState, newState) { + router.transition.ended.dispatch(oldState, newState); + } + + return router; +} + + +// Logging + +var log = logError = util.noop; + +Router.enableLogs = function() { + log = function() { + console.log(getLogMessage(arguments)); + }; + + logError = function() { + console.error(getLogMessage(arguments)); + }; + + function getLogMessage(args) { + var message = args[0], + tokens = Array.prototype.slice.call(args, 1); + + for (var i = 0, l = tokens.length; i < l; i++) + message = message.replace('{' + i + '}', tokens[i]); + + return message; + } +}; + + +module.exports = Router; +},{"./StateWithParams":8,"./Transition":9,"./anchorClicks":10,"./util":12,"crossroads":1,"signals":4}],7:[function(require,module,exports){ + +var util = require('./util'); +var async = require('./Transition').asyncPromises.register; + +/* +* Create a new State instance. +* +* State() // A state without options and an empty path. +* State('path', {options}) // A state with a static named path and options +* State(':path', {options}) // A state with a dynamic named path and options +* State('path?query', {options}) // Same as above with an optional query string param named 'query' +* State({options}) // Its path is the empty string. +* +* options is an object with the following optional properties: +* enter, exit, enterPrereqs, exitPrereqs. +* +* Child states can also be specified in the options: +* State({ myChildStateName: State() }) +* This is the declarative equivalent to the addState method. +* +* Finally, options can contain any arbitrary data value +* that will get stored in the state and made available via the data() method: +* State({myData: 55}) +* This is the declarative equivalent to the data(key, value) method. +*/ +function State() { + var state = { _isState: true }, + args = getArgs(arguments), + options = args.options, + states = getStates(args.options), + initialized; + + + state.path = args.path; + state.params = args.params; + state.queryParams = args.queryParams; + state.states = states; + + state.enter = options.enter || util.noop; + state.exit = options.exit || util.noop; + state.enterPrereqs = options.enterPrereqs; + state.exitPrereqs = options.exitPrereqs; + + state.ownData = getOwnData(options); + + /* + * Initialize and freeze this state. + */ + function init(name, parent) { + state.name = name; + state.parent = parent; + state.parents = getParents(); + state.children = getChildren(); + state.fullName = getFullName(); + state.root = state.parents[state.parents.length - 1]; + state.async = async; + + eachChildState(function(name, childState) { + childState.init(name, state); + }); + + initialized = true; + } + + /* + * The full path, composed of all the individual paths of this state and its parents. + */ + function fullPath() { + var result = state.path, + stateParent = state.parent; + + while (stateParent) { + if (stateParent.path) result = stateParent.path + '/' + result; + stateParent = stateParent.parent; + } + + return result; + } + + /* + * The list of all parents, starting from the closest ones. + */ + function getParents() { + var parents = [], + parent = state.parent; + + while (parent) { + parents.push(parent); + parent = parent.parent; + } + + return parents; + } + + /* + * The list of child states as an Array. + */ + function getChildren() { + var children = []; + + for (var name in states) { + children.push(states[name]); + } + + return children; + } + + /* + * The map of initial child states by name. + */ + function getStates(options) { + var states = {}; + + for (var key in options) { + if (options[key]._isState) states[key] = options[key]; + } + + return states; + } + + /* + * The fully qualified name of this state. + * e.g granparentName.parentName.name + */ + function getFullName() { + return state.parents.reduceRight(function(acc, parent) { + return acc + parent.name + '.'; + }, '') + state.name; + } + + function getOwnData(options) { + var reservedKeys = {'enter': 1, 'exit': 1, 'enterPrereqs': 1, 'exitPrereqs': 1}, + result = {}; + + for (var key in options) { + if (reservedKeys[key] || options[key]._isState) continue; + result[key] = options[key]; + } + + return result; + } + + /* + * Get or Set some arbitrary data by key on this state. + * child states have access to their parents' data. + * + * This can be useful when using external models/services + * as a mean to communicate between states is not desired. + */ + function data(key, value) { + if (value !== undefined) { + if (state.ownData[key] !== undefined) + throw new Error('State ' + state.fullName + ' already has data with the key ' + key); + state.ownData[key] = value; + return state; + } + + var currentState = state; + + while (currentState.ownData[key] === undefined && currentState.parent) + currentState = currentState.parent; + + return currentState.ownData[key]; + } + + function eachChildState(callback) { + for (var name in states) callback(name, states[name]); + } + + /* + * Add a child state. + */ + function addState(name, childState) { + if (initialized) + throw new Error('States can only be added before the Router is initialized'); + + if (states[name]) + throw new Error('The state {0} already has a child state named {1}' + .replace('{0}', state.name) + .replace('{1}', name) + ); + + states[name] = childState; + + return state; + }; + + function toString() { + return state.fullName; + } + + + state.init = init; + state.fullPath = fullPath; + + // Public methods + + state.data = data; + state.addState = addState; + state.toString = toString; + + return state; +} + + +// Extract the arguments of the overloaded State "constructor" function. +function getArgs(args) { + var result = { path: '', options: {}, params: {}, queryParams: {} }, + arg1 = args[0], + arg2 = args[1], + queryIndex, + param; + + if (args.length == 1) { + if (util.isString(arg1)) result.path = arg1; + else result.options = arg1; + } + else if (args.length == 2) { + result.path = arg1; + result.options = (typeof arg2 == 'object') ? arg2 : {enter: arg2}; + } + + // Extract the query string + queryIndex = result.path.indexOf('?'); + if (queryIndex != -1) { + result.queryParams = result.path.slice(queryIndex + 1); + result.path = result.path.slice(0, queryIndex); + result.queryParams = util.arrayToObject(result.queryParams.split('&')); + } + + // Replace dynamic params like :id with {id}, which is what crossroads uses, + // and store them for later lookup. + result.path = result.path.replace(/:\w*/g, function(match) { + param = match.substring(1); + result.params[param] = 1; + return '{' + param + '}'; + }); + + return result; +} + + +module.exports = State; +},{"./Transition":9,"./util":12}],8:[function(require,module,exports){ + +/* +* Creates a new StateWithParams instance. +* +* StateWithParams is the merge between a State object (created and added to the router before init) +* and params (both path and query params, extracted from the URL after init) +*/ +function StateWithParams(state, params) { + return { + _state: state, + name: state && state.name, + fullName: state && state.fullName, + data: state && state.data, + params: params, + is: is, + isIn: isIn, + toString: toString + }; +} + +/* +* Returns whether this state has the given fullName. +*/ +function is(fullStateName) { + return this.fullName == fullStateName; +} + +/* +* Returns whether this state or any of its parents has the given fullName. +*/ +function isIn(fullStateName) { + var current = this._state; + while (current) { + if (current.fullName == fullStateName) return true; + current = current.parent; + } + return false; +} + +function toString() { + return this.fullName + ':' + JSON.stringify(this.params) +} + + +module.exports = StateWithParams; +},{}],9:[function(require,module,exports){ + +var when = require('when'); + +/* +* Create a new Transition instance. +*/ +function Transition(fromStateWithParams, toStateWithParams, paramDiff) { + var root, + cancelled, + enters, + transitionPromise, + error, + exits = []; + + var fromState = fromStateWithParams && fromStateWithParams._state; + var toState = toStateWithParams._state; + var params = toStateWithParams.params; + var paramOnlyChange = (fromState == toState); + + var transition = { + from: fromState, + to: toState, + toParams: params, + then: then, + cancel: cancel, + cancelled: cancelled, + currentState: fromState + }; + + // The first transition has no fromState. + if (fromState) { + root = transitionRoot(fromState, toState, paramOnlyChange, paramDiff); + exits = transitionStates(fromState, root, paramOnlyChange); + } + + enters = transitionStates(toState, root, paramOnlyChange).reverse(); + + transitionPromise = prereqs(enters, exits, params).then(function() { + if (!cancelled) doTransition(enters, exits, params, transition); + }); + + asyncPromises.newTransitionStarted(); + + function then(completed, failed) { + return transitionPromise.then( + function success() { if (!cancelled) completed(); }, + function fail(error) { if (!cancelled) failed(error); } + ); + } + + function cancel() { + cancelled = transition.cancelled = true; + } + + return transition; +} + +/* +* Return the promise of the prerequisites for all the states involved in the transition. +*/ +function prereqs(enters, exits, params) { + + exits.forEach(function(state) { + if (!state.exitPrereqs) return; + + var prereqs = state._exitPrereqs = when(state.exitPrereqs()).then( + function success(value) { + if (state._exitPrereqs == prereqs) state._exitPrereqs.value = value; + }, + function fail(cause) { + throw new Error('Failed to resolve EXIT prereqs of ' + state.fullName); + } + ); + }); + + enters.forEach(function(state) { + if (!state.enterPrereqs) return; + + var prereqs = state._enterPrereqs = when(state.enterPrereqs(params)).then( + function success(value) { + if (state._enterPrereqs == prereqs) state._enterPrereqs.value = value; + }, + function fail(cause) { + throw new Error('Failed to resolve ENTER prereqs of ' + state.fullName); + } + ); + }); + + return when.all(enters.concat(exits).map(function(state) { + return state._enterPrereqs || state._exitPrereqs; + })); +} + +function doTransition(enters, exits, params, transition) { + exits.forEach(function(state) { + state.exit(state._exitPrereqs && state._exitPrereqs.value); + }); + + // Async promises are only allowed in 'enter' hooks. + // Make it explicit to prevent programming errors. + asyncPromises.allowed = true; + + enters.forEach(function(state) { + if (!transition.cancelled) { + transition.currentState = state; + state.enter(params, state._enterPrereqs && state._enterPrereqs.value); + } + }); + + asyncPromises.allowed = false; +} + +/* +* The top-most current state's parent that must be exited. +*/ +function transitionRoot(fromState, toState, paramOnlyChange, paramDiff) { + var root, + parent, + param; + + // For a param-only change, the root is the top-most state owning the param(s), + if (paramOnlyChange) { + fromState.parents.slice().reverse().forEach(function(parent) { + for (param in paramDiff) { + if (parent.params[param] || parent.queryParams[param]) { + root = parent; + break; + } + } + }); + } + // Else, the root is the closest common parent of the two states. + else { + for (var i = 0; i < fromState.parents.length; i++) { + parent = fromState.parents[i]; + if (toState.parents.indexOf(parent) > -1) { + root = parent; + break; + } + } + } + + return root; +} + +function withParents(state, upTo, inclusive) { + var p = state.parents, + end = Math.min(p.length, p.indexOf(upTo) + (inclusive ? 1 : 0)); + return [state].concat(p.slice(0, end)); +} + +function transitionStates(state, root, paramOnlyChange) { + var inclusive = !root || paramOnlyChange; + return withParents(state, root || state.root, inclusive); +} + + +var asyncPromises = Transition.asyncPromises = (function () { + + var that; + var activeDeferreds = []; + + /* + * Returns a promise that will not be fullfilled if the navigation context + * changes before the wrapped promise is fullfilled. + */ + function register(promise) { + if (!that.allowed) + throw new Error('Async can only be called from within state.enter()'); + + var defer = when.defer(); + + activeDeferreds.push(defer); + + when(promise).then( + function(value) { + if (activeDeferreds.indexOf(defer) > -1) + defer.resolve(value); + }, + function(error) { + if (activeDeferreds.indexOf(defer) > -1) + defer.reject(error); + } + ); + + return defer.promise; + } + + function newTransitionStarted() { + activeDeferreds.length = 0; + } + + that = { + register: register, + newTransitionStarted: newTransitionStarted, + allowed: false + }; + + return that; + +})(); + + +module.exports = Transition; +},{"when":5}],10:[function(require,module,exports){ + +var ieButton; + +function anchorClickHandler(evt) { + evt = evt || window.event; + + var defaultPrevented = ('defaultPrevented' in evt) + ? evt.defaultPrevented + : (evt.returnValue === false); + + if (defaultPrevented || evt.metaKey || evt.ctrlKey || !isLeftButtonClick(evt)) return; + + var target = evt.target || evt.srcElement; + var anchor = anchorTarget(target); + if (!anchor) return; + + var href = anchor.getAttribute('href'); + + if (href.charAt(0) == '#') return; + if (anchor.getAttribute('target') == '_blank') return; + if (!isLocalLink(anchor)) return; + + if (evt.preventDefault) + evt.preventDefault(); + else + evt.returnValue = false; + + router.state(href); +} + +function isLeftButtonClick(evt) { + evt = evt || window.event; + var button = (evt.which !== undefined) ? evt.which : ieButton; + return button == 1; +} + +function anchorTarget(target) { + while (target) { + if (target.nodeName == 'A') return target; + target = target.parentNode; + } +} + +// IE does not provide the correct event.button information on 'onclick' handlers +// but it does on mousedown/mouseup handlers. +function rememberIeButton(evt) { + ieButton = (evt || window.event).button; +} + +function isLocalLink(anchor) { + var hostname = anchor.hostname; + var port = anchor.port; + + // IE10 and below can lose the hostname/port property when setting a relative href from JS + if (!hostname) { + var tempAnchor = document.createElement("a"); + tempAnchor.href = anchor.href; + hostname = tempAnchor.hostname; + port = tempAnchor.port; + } + + var sameHostname = (hostname == location.hostname); + var samePort = (port || '80') == (location.port || '80'); + + return sameHostname && samePort; +} + + +module.exports = function interceptAnchorClicks(router) { + if (document.addEventListener) + document.addEventListener('click', anchorClickHandler); + else { + document.attachEvent('onmousedown', rememberIeButton); + document.attachEvent('onclick', anchorClickHandler); + } +}; +},{}],11:[function(require,module,exports){ + +require('html5-history-api/history.iegte8'); + +var Abyssa = { + Router: require('./Router'), + State: require('./State'), + Async: require('./Transition').asyncPromises.register +}; + +module.exports = Abyssa; +},{"./Router":6,"./State":7,"./Transition":9,"html5-history-api/history.iegte8":3}],12:[function(require,module,exports){ + +function isString(instance) { + return Object.prototype.toString.call(instance) == '[object String]'; +} + +function noop() {} + +function arrayToObject(array) { + return array.reduce(function(obj, item) { + obj[item] = 1; + return obj; + }, {}); +} + +function objectToArray(obj) { + var array = []; + for (var key in obj) array.push(obj[key]); + return array; +} + +function copyObject(obj) { + var copy = {}; + for (var key in obj) copy[key] = obj[key]; + return copy; +} + +function mergeObjects(to, from) { + for (var key in from) to[key] = from[key]; +} + +function objectSize(obj) { + var size = 0; + for (var key in obj) size++; + return size; +} + +module.exports = { + isString: isString, + noop: noop, + arrayToObject: arrayToObject, + objectToArray: objectToArray, + copyObject: copyObject, + mergeObjects: mergeObjects, + objectSize: objectSize +}; +},{}]},{},[11]) +(11) +}); +; \ No newline at end of file diff --git a/target/abyssa-with-deps.min.js b/target/abyssa-with-deps.min.js new file mode 100644 index 0000000..25b39ff --- /dev/null +++ b/target/abyssa-with-deps.min.js @@ -0,0 +1,4 @@ +/* abyssa 1.3.0 - A stateful router library for single page applications */ + +!function(a){"object"==typeof exports?module.exports=a():"function"==typeof define&&define.amd?define(a):"undefined"!=typeof window?window.Abyssa=a():"undefined"!=typeof global?global.Abyssa=a():"undefined"!=typeof self&&(self.Abyssa=a())}(function(){var a;return function b(a,c,d){function e(g,h){if(!c[g]){if(!a[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};a[g][0].call(j.exports,function(b){var c=a[g][1][b];return e(c?c:b)},j,j.exports,b,a,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;ge;)c=d[e],c.route.matched.dispatch.apply(c.route.matched,b.concat(c.params)),c.isFirst=!e,this.routed.dispatch.apply(this.routed,b.concat([a,c])),e+=1;else this._prevBypassedRequest=a,this.bypassed.dispatch.apply(this.bypassed,b.concat([a]));this._pipeParse(a,b)}},_notifyPrevRoutes:function(a,b){for(var c,d=0;c=this._prevRoutes[d++];)c.route.switched&&this._didSwitch(c.route,a)&&c.route.switched.dispatch(b)},_didSwitch:function(a,b){for(var c,d=0;c=b[d++];)if(c.route===a)return!1;return!0},_pipeParse:function(a,b){for(var c,d=0;c=this._piped[d++];)c.parse(a,b)},getNumRoutes:function(){return this._routes.length},_sortedInsert:function(a){var b=this._routes,c=b.length;do--c;while(b[c]&&a._priority<=b[c]._priority);b.splice(c+1,0,a)},_getMatchedRoutes:function(a){for(var b,c=[],d=this._routes,e=d.length;(b=d[--e])&&((!c.length||this.greedy||b.greedy)&&b.match(a)&&c.push({route:b,params:b._getParamsArray(a)}),this.greedyEnabled||!c.length););return c},pipe:function(a){this._piped.push(a)},unpipe:function(a){c(this._piped,a)},toString:function(){return"[crossroads numRoutes:"+this.getNumRoutes()+"]"}},m=new k,m.VERSION="0.12.0",m.NORM_AS_ARRAY=function(a,b){return[b.vals_]},m.NORM_AS_OBJECT=function(a,b){return[b]},l.prototype={greedy:!1,rules:void 0,match:function(a){return a=a||"",this._matchRegexp.test(a)&&this._validateParams(a)},_validateParams:function(a){var b,c=this.rules,d=this._getParamsObject(a);for(b in c)if("normalize_"!==b&&c.hasOwnProperty(b)&&!this._isValidParam(a,b,d))return!1;return!0},_isValidParam:function(a,c,d){var h=this.rules[c],i=d[c],j=!1,k=0===c.indexOf("?");return null==i&&this._optionalParamsIds&&-1!==b(this._optionalParamsIds,c)?j=!0:e(h)?(k&&(i=d[c+"_"]),j=h.test(i)):f(h)?(k&&(i=d[c+"_"]),j=this._isValidArrayRule(h,i)):g(h)&&(j=h(i,a,d)),j},_isValidArrayRule:function(a,c){if(!this._router.ignoreCase)return-1!==b(a,c);"string"==typeof c&&(c=c.toLowerCase());for(var d,e,f=a.length;f--;)if(d=a[f],e="string"==typeof d?d.toLowerCase():d,e===c)return!0;return!1},_getParamsObject:function(a){for(var c,d,e=this._router.shouldTypecast,f=this._router.patternLexer.getParamValues(a,this._matchRegexp,e),g={},i=f.length;i--;)d=f[i],this._paramsIds&&(c=this._paramsIds[i],0===c.indexOf("?")&&d&&(g[c+"_"]=d,d=j(d,e),f[i]=d),n&&""===d&&-1!==b(this._optionalParamsIds,c)&&(d=void 0,f[i]=d),g[c]=d),g[i]=d;return g.request_=e?h(a):a,g.vals_=f,g},_getParamsArray:function(a){var b,c=this.rules?this.rules.normalize_:null;return c=c||this._router.normalizeFn,b=c&&g(c)?c(a,this._getParamsObject(a)):this._getParamsObject(a).vals_},interpolate:function(a){var b=this._router.patternLexer.interpolate(this._pattern,a);if(!this._validateParams(b))throw new Error("Generated string doesn't validate against `Route.rules`.");return b},dispose:function(){this._router.removeRoute(this)},_destroy:function(){this.matched.dispose(),this.switched.dispose(),this.matched=this.switched=this._pattern=this._matchRegexp=null},toString:function(){return'[Route pattern:"'+this._pattern+'", numListeners:'+this.matched.getNumListeners()+"]"}},k.prototype.patternLexer=function(){function a(){var a,b;for(a in n)n.hasOwnProperty(a)&&(b=n[a],b.id="__CR_"+a+"__",b.save="save"in b?b.save.replace("{{id}}",b.id):b.id,b.rRestore=new RegExp(b.id,"g"))}function b(a,b){var c,d=[];for(a.lastIndex=0;c=a.exec(b);)d.push(c[1]);return d}function c(a){return b(m,a)}function d(a){return b(n.OP.rgx,a)}function e(a,b){return a=a||"",a&&(r===o?a=a.replace(k,""):r===q&&(a=a.replace(l,"")),a=f(a,"rgx","save"),a=a.replace(j,"\\$&"),a=f(a,"rRestore","res"),r===o&&(a="\\/?"+a)),r!==p&&(a+="\\/?"),new RegExp("^"+a+"$",b?"i":"")}function f(a,b,c){var d,e;for(e in n)n.hasOwnProperty(e)&&(d=n[e],a=a.replace(d[b],d[c]));return a}function g(a,b,c){var d=b.exec(a);return d&&(d.shift(),c&&(d=i(d))),d}function h(a,b){if("string"!=typeof a)throw new Error("Route pattern should be a string.");var c=function(a,c){var d;if(c="?"===c.substr(0,1)?c.substr(1):c,null!=b[c]){if("object"==typeof b[c]){var e=[];for(var f in b[c])e.push(encodeURI(f+"="+b[c][f]));d="?"+e.join("&")}else d=String(b[c]);if(-1===a.indexOf("*")&&-1!==d.indexOf("/"))throw new Error('Invalid value "'+d+'" for segment "'+a+'".')}else{if(-1!==a.indexOf("{"))throw new Error("The segment "+a+" is required.");d=""}return d};return n.OS.trail||(n.OS.trail=new RegExp("(?:"+n.OS.id+")+$")),a.replace(n.OS.rgx,n.OS.save).replace(m,c).replace(n.OS.trail,"").replace(n.OS.rRestore,"/")}var j=/[\\.+*?\^$\[\](){}\/'#]/g,k=/^\/|\/$/g,l=/\/$/g,m=/(?:\{|:)([^}:]+)(?:\}|:)/g,n={OS:{rgx:/([:}]|\w(?=\/))\/?(:|(?:\{\?))/g,save:"$1{{id}}$2",res:"\\/?"},RS:{rgx:/([:}])\/?(\{)/g,save:"$1{{id}}$2",res:"\\/"},RQ:{rgx:/\{\?([^}]+)\}/g,res:"\\?([^#]+)"},OQ:{rgx:/:\?([^:]+):/g,res:"(?:\\?([^#]*))?"},OR:{rgx:/:([^:]+)\*:/g,res:"(.*)?"},RR:{rgx:/\{([^}]+)\*\}/g,res:"(.+)"},RP:{rgx:/\{([^}]+)\}/g,res:"([^\\/?]+)"},OP:{rgx:/:([^:]+):/g,res:"([^\\/?]+)?/?"}},o=1,p=2,q=3,r=o;return a(),{strict:function(){r=p},loose:function(){r=o},legacy:function(){r=q},getParamIds:c,getOptionalParamsIds:d,getParamValues:g,compilePattern:e,interpolate:h}}(),m};"function"==typeof a&&a.amd?a(["signals"],d):"undefined"!=typeof c&&c.exports?c.exports=d(b("signals")):window.crossroads=d(window.signals)}()},{signals:4}],2:[function(a,b){var c=b.exports={};c.nextTick=function(){var a="undefined"!=typeof window&&window.setImmediate,b="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(a)return function(a){return window.setImmediate(a)};if(b){var c=[];return window.addEventListener("message",function(a){if(a.source===window&&"process-tick"===a.data&&(a.stopPropagation(),c.length>0)){var b=c.shift();b()}},!0),function(a){c.push(a),window.postMessage("process-tick","*")}}return function(a){setTimeout(a,0)}}(),c.title="browser",c.browser=!0,c.env={},c.argv=[],c.binding=function(){throw new Error("process.binding is not supported")},c.cwd=function(){return"/"},c.chdir=function(){throw new Error("process.chdir is not supported")}},{}],3:[function(){!function(a){function b(){}function c(a,b,d){var e=/(?:([\w0-9]+:))?(?:\/\/(?:[^@]*@)?([^\/:\?#]+)(?::([0-9]+))?)?([^\?#]*)(?:(\?[^#]+)|\?)?(?:(#.*))?/;if(null==a||""===a||b)a=b?a:x.href,(!C||d)&&(a=a.replace(/^[^#]*/,"")||"#",a=x.protocol+"//"+x.host+N.basepath+a.replace(new RegExp("^#[/]?(?:"+N.type+")?"),""));else{var f=c(),g=f._pathname,h=f._protocol;a=""+a,a=/^(?:[\w0-9]+\:)?\/\//.test(a)?0===a.indexOf("/")?h+a:a:h+"//"+f._host+(0===a.indexOf("/")?a:0===a.indexOf("?")?g+a:0===a.indexOf("#")?g+f._search+a:g.replace(/[^\/]+$/g,"")+a)}P.href=a;var i=e.exec(P.href),j=i[2]+(i[3]?":"+i[3]:""),k=i[4]||"/",l=i[5]||"",m="#"===i[6]?"":i[6]||"",n=k+l+m,o=k.replace(new RegExp("^"+N.basepath,"i"),N.type)+l;return{_href:i[1]+"//"+j+n,_protocol:i[1],_host:j,_hostname:i[2],_port:i[3]||"",_pathname:k,_search:l,_hash:m,_relative:n,_nohash:o,_special:o+m}}function d(){var a="";if(u)a+=u.getItem(O);else{var b=s.cookie.split(O+"=");b.length>1&&(a+=b.pop().split(";").shift()||"null")}try{T=w.parse(a)||{}}catch(c){T={}}K(G+"unload",function(){if(u)u.setItem(O,w.stringify(T));else{var a={};(a[x.href]=z.state)&&(s.cookie=O+"="+w.stringify(a))}},!1)}function e(c,d,e,f){e=e||{set:b};var g=!e.set,h=!e.get,i={configurable:!0,set:function(){g=1},get:function(){h=1}};try{E(c,d,i),c[d]=c[d],E(c,d,e)}catch(j){}if(!g||!h)if(c.__defineGetter__&&(c.__defineGetter__(d,i.get),c.__defineSetter__(d,i.set),c[d]=c[d],e.get&&c.__defineGetter__(d,e.get),e.set&&c.__defineSetter__(d,e.set)),g&&h||c!==a){if(!g||!h)try{try{var k=v.create(c);E(v.getPrototypeOf(k)===c?k:c,d,e);for(var l in c)"function"==typeof c[l]&&(k[l]=c[l].bind(c));try{f.call(k,k,c)}catch(j){}c=k}catch(j){E(c.constructor.prototype,d,e)}}catch(j){return!1}}else{try{var m=c[d];c[d]=null}catch(j){}if("execScript"in a)a.execScript("Public "+d,"VBScript");else try{E(c,d,{value:b})}catch(j){}c[d]=m}return c}function f(a,b,c){return c=c||{},a=a===Z?x:a,c.set=c.set||function(c){a[b]=c},c.get=c.get||function(){return a[b]},c}function g(a,b,c){a in U?U[a].push(b):arguments.length>3?K(a,b,c,arguments[3]):K(a,b,c)}function h(a,b,c){var d=U[a];if(d){for(var e=d.length;--e;)if(d[e]===b){d.splice(e,1);break}}else L(a,b,c)}function i(c,d){var f=(""+("string"==typeof c?c:c.type)).replace(/^on/,""),g=U[f];if(g){if(d="string"==typeof c?d:c,null==d.target)for(var h=["target","currentTarget","srcElement","type"];c=h.pop();)d=e(d,c,{get:"type"===c?function(){return f}:function(){return a}});(("popstate"===f?a.onpopstate:a.onhashchange)||b).call(a,d);for(var i=0,j=g.length;j>i;i++)g[i].call(a,d);return!0}return M(c,d)}function j(){var a=s.createEvent?s.createEvent("Event"):s.createEventObject();a.initEvent?a.initEvent("popstate",!1,!1):a.type="popstate",a.state=z.state,i(a)}function k(){S&&(S=!1,j())}function l(a,b,d,e){if(!C){var f=c(b);f._relative!==c()._relative&&(Q=e,d?x.replace("#"+f._special):x.hash=f._special)}!D&&a&&(T[x.href]=a),S=!1}function m(b){if(Q){R!==x.href&&j(),b=b||a.event;var d=c(Q,!0),e=c();b.oldURL||(b.oldURL=d._href,b.newURL=e._href),d._hash!==e._hash&&i(b)}Q=x.href}function n(a){setTimeout(function(){K("popstate",function(a){R=x.href,D||(a=e(a,"state",{get:function(){return z.state}})),i(a)},!1)},0),!C&&a!==!0&&z.location&&(q(z.location.hash),k())}function o(a){for(;a;){if("A"===a.nodeName)return a;a=a.parentNode}}function p(b){var d=b||a.event,e=o(d.target||d.srcElement),f="defaultPrevented"in d?d.defaultPrevented:d.returnValue===!1;if(e&&"A"===e.nodeName&&!f){var g=c(),h=c(e.getAttribute("href",2)),i=g._href.split("#").shift()===h._href.split("#").shift();i&&h._hash&&(g._hash!==h._hash&&(z.location.hash=h._hash),q(h._hash),d.preventDefault?d.preventDefault():d.returnValue=!1)}}function q(b){var c=s.getElementById(b=(b||"").replace(/^#/,""));if(c&&c.id===b&&"A"===c.nodeName){var d=c.getBoundingClientRect();a.scrollTo(t.scrollLeft||0,d.top+(t.scrollTop||0)-(t.clientTop||0))}}function r(){var b=s.getElementsByTagName("script"),g=(b[b.length-1]||{}).src||"",h=-1!==g.indexOf("?")?g.split("?").pop():"";h.replace(/(\w+)(?:=([^&]*))?/g,function(a,b,c){N[b]=(c||("basepath"===b?"/":"")).replace(/^(0|false)$/,"")});try{u=a.sessionStorage}catch(i){}K(G+"hashchange",m,!1);var j=[Z,F,W,a,Y,z];D&&delete Y.state;for(var k=0;k>>0,i=Math.max(0,Math.min(b,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=J,e(l))},m=function(a){k.push(a),--i||(m=n=J,d(k))},p=0;o>p;++p)p in a&&c(a[p],h,g,f);else d(k)}return j(g).then(d,e,f)})}function x(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return w(a,1,e,c,d)}function y(a,b,c,d){return C(a,J).then(b,c,d)}function z(){return C(arguments,J)}function A(a){return C(a,E,F)}function B(a,b){return C(a,b)}function C(a,b,d){return c(a,function(a){function e(e,f,g){function h(a,h){c(a,b,d).then(function(a){i[h]=a,--k||e(i)},f,g)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return k(e)})}function D(a,b){var d=M(L,arguments,1);return c(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return c(a,function(a){return c(d,function(c){return b(a,c,f,e)})})},K.apply(a,d)})}function E(a){return{state:"fulfilled",value:a}}function F(a){return{state:"rejected",reason:a}}function G(){return{state:"pending"}}function H(a){1===O.push(a)&&N(I)}function I(){l(O),O=[]}function J(a){return a}c.promise=j,c.resolve=g,c.reject=h,c.defer=i,c.join=z,c.all=y,c.map=B,c.reduce=D,c.settle=A,c.any=x,c.some=w,c.isPromise=v,c.isPromiseLike=v,f.prototype={then:function(){var a,b;return a=arguments,b=this._message,k(function(c,d,e){b("when",a,c,e)},this._status&&this._status.observed())},otherwise:function(a){return this.then(W,a)},ensure:function(a){function b(){return g(a())}return"function"==typeof a?this.then(b,b).yield(this):this},yield:function(a){return this.then(function(){return a})},tap:function(a){return this.then(a).yield(this)},spread:function(a){return this.then(function(b){return y(b,function(b){return a.apply(W,b)})})},always:function(a,b){return this.then(a,a,b)}},s.prototype.when=function(a){return"function"==typeof a?a(this.value):this.value},t.prototype.when=function(a,b){if("function"==typeof b)return b(this.reason);throw this.reason};var K,L,M,N,O,P,Q,R,S,T,U,V,W;if(U=a,O=[],P=b.setTimeout,T="undefined"!=typeof console?console:c,"object"==typeof d&&d.nextTick)N=d.nextTick;else if(V=b.MutationObserver||b.WebKitMutationObserver)N=function(a,b,c){var d=a.createElement("div");return new b(c).observe(d,{attributes:!0}),function(){d.setAttribute("x","x")}}(document,V,I);else try{N=U("vertx").runOnLoop||U("vertx").runOnContext}catch(X){N=function(a){P(a,0)}}return Q=Function.prototype,R=Q.call,M=Q.bind?R.bind(R):function(a,b){return a.apply(b,L.call(arguments,2))},S=[],L=S.slice,K=S.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},c})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)},this)},{__browserify_process:2}],6:[function(a,b){function c(a){function b(a,b){if(!n(a,b)){var c,d=g(a,b);H?(k(),c=g(H.currentState,H.toParams)):c=G,G=d,l(c,d),H=h(c,d,o(c&&c.params,b)),H.then(function(){var a;H=null,K||P||(a=("/"+F).replace("//","/"),j("Pushing state: {0}",a),history.pushState(a,document.title,a)),m(c,d)},function(a){H=null,G=c,logError("Transition from {0} to {1} failed: {2}",c,d,a),M.transition.failed.dispatch(c,d)})}}function k(){j("Cancelling existing transition from {0} to {1}",H.from,H.to),H.cancel(),M.transition.cancelled.dispatch(H.from,H.to)}function l(a,b){j("Starting transition from {0} to {1}",a,b),M.transition.started.dispatch(a,b)}function m(a,b){j("Transition from {0} to {1} completed",a,b),M.transition.completed.dispatch(a,b),P=!1}function n(a,b){var c,d,e;return H?(c=H.to,d=H.toParams):G&&(c=G._state,d=G.params),e=o(d,b),a==c&&0==i.objectSize(e)}function o(a,b){var c={},a=a||{};for(var d in a)a[d]!=b[d]&&(c[d]=1);for(var d in b)a[d]!=b[d]&&(c[d]=1);return c}function p(a){if(j("State not found: {0}",a),!N.notFound)throw new Error('State "'+a+'" could not be found');b(N.notFound)}function q(a){return i.mergeObjects(Q,a),M}function r(a){Q.enableLogs&&c.enableLogs(),Q.interceptAnchorClicks&&f(M),j("Router init"),s();var b=!c.ignoreInitialURL&&A()||a||"";return j("Initializing to state {0}",b||'""'),v(b),window.onpopstate=function(a){var b=a.state||A();j("Popped state: {0}",b),K=!0,x(b)},L=!0,M}function s(){t(function(a,b){b.init(a)}),I={},u(function(a){I[a.fullName]=a,a.route=O.addRoute(a.fullPath()+":?query:"),a.route.matched.add(function(){J=!0,b(a,B(a,arguments))})})}function t(a){for(var b in N)a(b,N[b])}function u(a){function b(c){c.forEach(function(c){c.children.length?b(c.children):a(c)})}b(i.objectToArray(N))}function v(a,b){var c=a.indexOf(".")>-1||I[a];j("Changing state to {0}",a||'""'),K=!1,c?y(a,b||{}):x(a)}function w(a,b){j("Redirecting..."),v(a,b)}function x(a){F=a,J=!1,O.parse(a),J||p(a)}function y(a,b){var c=I[a];if(!c)return p(a);var d=c.route.interpolate(b);x(d)}function z(a,b){if(L)throw new Error("States can only be added before the Router is initialized");if(N[a])throw new Error("A state already exist in the router with the name "+a);return j("Adding state {0}",a),N[a]=b,M}function A(){var a=location.href.indexOf("#/");return a>-1?location.href.slice(a+2):(location.pathname+location.search).slice(1)}function B(a,b){var c,d=Array.prototype.slice.apply(b),e=d.pop(),f={};a.fullPath().replace(/\{\w*\}/g,function(a){return c=a.slice(1,-1),f[c]=d.shift(),""}),e&&i.mergeObjects(f,e);for(var g in f)i.isString(f[g])&&(f[g]=decodeURIComponent(f[g]));return f}function C(a,b){var c={},d={},e=!1,f=I[a];if(!f)throw new Error("Cannot find state "+a);[f].concat(f.parents).forEach(function(a){i.mergeObjects(d,a.queryParams)});for(var g in b)d[g]&&(c[g]=b[g],delete b[g],e=!0);return e&&(b.query=c),"/"+f.route.interpolate(b).replace("/?","?")}function D(){return G}function E(a,b){M.transition.ended.dispatch(a,b)}var F,G,H,I,J,K,L,M={},N=i.copyObject(a),O=e.create(),P=!0,Q={enableLogs:!1,interceptAnchorClicks:!0};return O.shouldTypecast=!0,O.ignoreState=!0,M.configure=q,M.init=r,M.state=v,M.redirect=w,M.addState=z,M.link=C,M.currentState=D,M.transition={started:new d,ended:new d,completed:new d,failed:new d,cancelled:new d},M.initialized=new d,M.transition.completed.addOnce(function(){M.initialized.dispatch()}),M.transition.completed.add(E),M.transition.failed.add(E),M.transition.cancelled.add(E),M}var d=a("signals").Signal,e=a("crossroads"),f=a("./anchorClicks"),g=a("./StateWithParams"),h=a("./Transition"),i=a("./util"),j=logError=i.noop;c.enableLogs=function(){function a(a){for(var b=a[0],c=Array.prototype.slice.call(a,1),d=0,e=c.length;e>d;d++)b=b.replace("{"+d+"}",c[d]);return b}j=function(){console.log(a(arguments))},logError=function(){console.error(a(arguments))}},b.exports=c},{"./StateWithParams":8,"./Transition":9,"./anchorClicks":10,"./util":12,crossroads:1,signals:4}],7:[function(a,b){function c(){function a(a,b){p.name=a,p.parent=b,p.parents=c(),p.children=g(),p.fullName=i(),p.root=p.parents[p.parents.length-1],p.async=f,l(function(a,b){b.init(a,p)}),o=!0}function b(){for(var a=p.path,b=p.parent;b;)b.path&&(a=b.path+"/"+a),b=b.parent;return a}function c(){for(var a=[],b=p.parent;b;)a.push(b),b=b.parent;return a}function g(){var a=[];for(var b in s)a.push(s[b]);return a}function h(a){var b={};for(var c in a)a[c]._isState&&(b[c]=a[c]);return b}function i(){return p.parents.reduceRight(function(a,b){return a+b.name+"."},"")+p.name}function j(a){var b={enter:1,exit:1,enterPrereqs:1,exitPrereqs:1},c={};for(var d in a)b[d]||a[d]._isState||(c[d]=a[d]);return c}function k(a,b){if(void 0!==b){if(void 0!==p.ownData[a])throw new Error("State "+p.fullName+" already has data with the key "+a);return p.ownData[a]=b,p}for(var c=p;void 0===c.ownData[a]&&c.parent;)c=c.parent;return c.ownData[a]}function l(a){for(var b in s)a(b,s[b])}function m(a,b){if(o)throw new Error("States can only be added before the Router is initialized");if(s[a])throw new Error("The state {0} already has a child state named {1}".replace("{0}",p.name).replace("{1}",a));return s[a]=b,p}function n(){return p.fullName}var o,p={_isState:!0},q=d(arguments),r=q.options,s=h(q.options);return p.path=q.path,p.params=q.params,p.queryParams=q.queryParams,p.states=s,p.enter=r.enter||e.noop,p.exit=r.exit||e.noop,p.enterPrereqs=r.enterPrereqs,p.exitPrereqs=r.exitPrereqs,p.ownData=j(r),p.init=a,p.fullPath=b,p.data=k,p.addState=m,p.toString=n,p}function d(a){var b,c,d={path:"",options:{},params:{},queryParams:{}},f=a[0],g=a[1];return 1==a.length?e.isString(f)?d.path=f:d.options=f:2==a.length&&(d.path=f,d.options="object"==typeof g?g:{enter:g}),b=d.path.indexOf("?"),-1!=b&&(d.queryParams=d.path.slice(b+1),d.path=d.path.slice(0,b),d.queryParams=e.arrayToObject(d.queryParams.split("&"))),d.path=d.path.replace(/:\w*/g,function(a){return c=a.substring(1),d.params[c]=1,"{"+c+"}"}),d}var e=a("./util"),f=a("./Transition").asyncPromises.register;b.exports=c},{"./Transition":9,"./util":12}],8:[function(a,b){function c(a,b){return{_state:a,name:a&&a.name,fullName:a&&a.fullName,data:a&&a.data,params:b,is:d,isIn:e,toString:f}}function d(a){return this.fullName==a}function e(a){for(var b=this._state;b;){if(b.fullName==a)return!0;b=b.parent}return!1}function f(){return this.fullName+":"+JSON.stringify(this.params)}b.exports=c},{}],9:[function(a,b){function c(a,b,c){function g(a,b){return n.then(function(){l||a()},function(a){l||b(a)})}function i(){l=t.cancelled=!0}var k,l,m,n,o=[],p=a&&a._state,q=b._state,r=b.params,s=p==q,t={from:p,to:q,toParams:r,then:g,cancel:i,cancelled:l,currentState:p};return p&&(k=f(p,q,s,c),o=h(p,k,s)),m=h(q,k,s).reverse(),n=d(m,o,r).then(function(){l||e(m,o,r,t)}),j.newTransitionStarted(),t}function d(a,b,c){return b.forEach(function(a){if(a.exitPrereqs)var b=a._exitPrereqs=i(a.exitPrereqs()).then(function(c){a._exitPrereqs==b&&(a._exitPrereqs.value=c)},function(){throw new Error("Failed to resolve EXIT prereqs of "+a.fullName)})}),a.forEach(function(a){if(a.enterPrereqs)var b=a._enterPrereqs=i(a.enterPrereqs(c)).then(function(c){a._enterPrereqs==b&&(a._enterPrereqs.value=c)},function(){throw new Error("Failed to resolve ENTER prereqs of "+a.fullName)})}),i.all(a.concat(b).map(function(a){return a._enterPrereqs||a._exitPrereqs}))}function e(a,b,c,d){b.forEach(function(a){a.exit(a._exitPrereqs&&a._exitPrereqs.value)}),j.allowed=!0,a.forEach(function(a){d.cancelled||(d.currentState=a,a.enter(c,a._enterPrereqs&&a._enterPrereqs.value))}),j.allowed=!1}function f(a,b,c,d){var e,f,g;if(c)a.parents.slice().reverse().forEach(function(a){for(g in d)if(a.params[g]||a.queryParams[g]){e=a;break}});else for(var h=0;h-1){e=f;break}return e}function g(a,b,c){var d=a.parents,e=Math.min(d.length,d.indexOf(b)+(c?1:0));return[a].concat(d.slice(0,e))}function h(a,b,c){var d=!b||c;return g(a,b||a.root,d)}var i=a("when"),j=c.asyncPromises=function(){function a(a){if(!c.allowed)throw new Error("Async can only be called from within state.enter()");var b=i.defer();return d.push(b),i(a).then(function(a){d.indexOf(b)>-1&&b.resolve(a)},function(a){d.indexOf(b)>-1&&b.reject(a)}),b.promise}function b(){d.length=0}var c,d=[];return c={register:a,newTransitionStarted:b,allowed:!1}}();b.exports=c},{when:5}],10:[function(a,b){function c(a){a=a||window.event;var b="defaultPrevented"in a?a.defaultPrevented:a.returnValue===!1;if(!(b||a.metaKey||a.ctrlKey)&&d(a)){var c=a.target||a.srcElement,f=e(c);if(f){var h=f.getAttribute("href");"#"!=h.charAt(0)&&"_blank"!=f.getAttribute("target")&&g(f)&&(a.preventDefault?a.preventDefault():a.returnValue=!1,router.state(h))}}}function d(a){a=a||window.event;var b=void 0!==a.which?a.which:h;return 1==b}function e(a){for(;a;){if("A"==a.nodeName)return a;a=a.parentNode}}function f(a){h=(a||window.event).button}function g(a){var b=a.hostname,c=a.port; +if(!b){var d=document.createElement("a");d.href=a.href,b=d.hostname,c=d.port}var e=b==location.hostname,f=(c||"80")==(location.port||"80");return e&&f}var h;b.exports=function(){document.addEventListener?document.addEventListener("click",c):(document.attachEvent("onmousedown",f),document.attachEvent("onclick",c))}},{}],11:[function(a,b){a("html5-history-api/history.iegte8");var c={Router:a("./Router"),State:a("./State"),Async:a("./Transition").asyncPromises.register};b.exports=c},{"./Router":6,"./State":7,"./Transition":9,"html5-history-api/history.iegte8":3}],12:[function(a,b){function c(a){return"[object String]"==Object.prototype.toString.call(a)}function d(){}function e(a){return a.reduce(function(a,b){return a[b]=1,a},{})}function f(a){var b=[];for(var c in a)b.push(a[c]);return b}function g(a){var b={};for(var c in a)b[c]=a[c];return b}function h(a,b){for(var c in b)a[c]=b[c]}function i(a){var b=0;for(var c in a)b++;return b}b.exports={isString:c,noop:d,arrayToObject:e,objectToArray:f,copyObject:g,mergeObjects:h,objectSize:i}},{}]},{},[11])(11)}); \ No newline at end of file diff --git a/target/abyssa.js b/target/abyssa.js index 8339e47..42d9f5a 100644 --- a/target/abyssa.js +++ b/target/abyssa.js @@ -1,3126 +1,749 @@ -/* abyssa 1.2.4 - A stateful router library for single page applications */ - -/*jslint indent:4, white:true, nomen:true, plusplus:true */ -/*global define:false, require:false, exports:false, module:false, signals:false */ - -/** @license - * JS Signals - * Released under the MIT license - * Author: Miller Medeiros - * Version: 0.8.1 - Build: 266 (2012/07/31 03:33 PM) - */ - -var Signal = (function(global){ - - // SignalBinding ------------------------------------------------- - //================================================================ - - /** - * Object that represents a binding between a Signal and a listener function. - *
- This is an internal constructor and shouldn't be called by regular users. - *
- inspired by Joa Ebert AS3 SignalBinding and Robert Penner's Slot classes. - * @author Miller Medeiros - * @constructor - * @internal - * @name SignalBinding - * @param {Signal} signal Reference to Signal object that listener is currently bound to. - * @param {Function} listener Handler function bound to the signal. - * @param {boolean} isOnce If binding should be executed just once. - * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @param {Number} [priority] The priority level of the event listener. (default = 0). - */ - function SignalBinding(signal, listener, isOnce, listenerContext, priority) { - - /** - * Handler function bound to the signal. - * @type Function - * @private - */ - this._listener = listener; - - /** - * If binding should be executed just once. - * @type boolean - * @private - */ - this._isOnce = isOnce; - - /** - * Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @memberOf SignalBinding.prototype - * @name context - * @type Object|undefined|null - */ - this.context = listenerContext; - - /** - * Reference to Signal object that listener is currently bound to. - * @type Signal - * @private - */ - this._signal = signal; - - /** - * Listener priority - * @type Number - * @private - */ - this._priority = priority || 0; - } - - SignalBinding.prototype = { - - /** - * If binding is active and should be executed. - * @type boolean - */ - active : true, - - /** - * Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute`. (curried parameters) - * @type Array|null - */ - params : null, - - /** - * Call listener passing arbitrary parameters. - *

If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch.

- * @param {Array} [paramsArr] Array of parameters that should be passed to the listener - * @return {*} Value returned by the listener. - */ - execute : function (paramsArr) { - var handlerReturn, params; - if (this.active && !!this._listener) { - params = this.params? this.params.concat(paramsArr) : paramsArr; - handlerReturn = this._listener.apply(this.context, params); - if (this._isOnce) { - this.detach(); - } - } - return handlerReturn; - }, - - /** - * Detach binding from signal. - * - alias to: mySignal.remove(myBinding.getListener()); - * @return {Function|null} Handler function bound to the signal or `null` if binding was previously detached. - */ - detach : function () { - return this.isBound()? this._signal.remove(this._listener, this.context) : null; - }, - - /** - * @return {Boolean} `true` if binding is still bound to the signal and have a listener. - */ - isBound : function () { - return (!!this._signal && !!this._listener); - }, - - /** - * @return {Function} Handler function bound to the signal. - */ - getListener : function () { - return this._listener; - }, - - /** - * Delete instance properties - * @private - */ - _destroy : function () { - delete this._signal; - delete this._listener; - delete this.context; - }, - - /** - * @return {boolean} If SignalBinding will only be executed once. - */ - isOnce : function () { - return this._isOnce; - }, - - /** - * @return {string} String representation of the object. - */ - toString : function () { - return '[SignalBinding isOnce:' + this._isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']'; - } - - }; +/* abyssa 1.3.0 - A stateful router library for single page applications */ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.Abyssa=e():"undefined"!=typeof global?global.Abyssa=e():"undefined"!=typeof self&&(self.Abyssa=e())}(function(){var define,module,exports; +return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o- inspired by Robert Penner's AS3 Signals. - * @name Signal - * @author Miller Medeiros - * @constructor - */ - function Signal() { - /** - * @type Array. - * @private - */ - this._bindings = []; - this._prevParams = null; - } +/* +* Create a new Router instance, passing any state defined declaratively. +* More states can be added using addState() before the router is initialized. +* +* Because a router manages global state (the URL), only one instance of Router +* should be used inside an application. +*/ +function Router(declarativeStates) { + var router = {}, + states = util.copyObject(declarativeStates), + roads = crossroads.create(), + firstTransition = true, + initOptions = { + enableLogs: false, + interceptAnchorClicks: true + }, + currentPathQuery, + currentState, + transition, + leafStates, + stateFound, + poppedState, + initialized; - Signal.prototype = { - - /** - * Signals Version Number - * @type String - * @const - */ - VERSION : '0.8.1', - - /** - * If Signal should keep record of previously dispatched parameters and - * automatically execute listener during `add()`/`addOnce()` if Signal was - * already dispatched before. - * @type boolean - */ - memorize : false, - - /** - * @type boolean - * @private - */ - _shouldPropagate : true, - - /** - * If Signal is active and should broadcast events. - *

IMPORTANT: Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead.

- * @type boolean - */ - active : true, - - /** - * @param {Function} listener - * @param {boolean} isOnce - * @param {Object} [listenerContext] - * @param {Number} [priority] - * @return {SignalBinding} - * @private - */ - _registerListener : function (listener, isOnce, listenerContext, priority) { - - var prevIndex = this._indexOfListener(listener, listenerContext), - binding; - - if (prevIndex !== -1) { - binding = this._bindings[prevIndex]; - if (binding.isOnce() !== isOnce) { - throw new Error('You cannot add'+ (isOnce? '' : 'Once') +'() then add'+ (!isOnce? '' : 'Once') +'() the same listener without removing the relationship first.'); - } - } else { - binding = new SignalBinding(this, listener, isOnce, listenerContext, priority); - this._addBinding(binding); - } - - if(this.memorize && this._prevParams){ - binding.execute(this._prevParams); - } - - return binding; - }, - - /** - * @param {SignalBinding} binding - * @private - */ - _addBinding : function (binding) { - //simplified insertion sort - var n = this._bindings.length; - do { --n; } while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); - this._bindings.splice(n + 1, 0, binding); - }, - - /** - * @param {Function} listener - * @return {number} - * @private - */ - _indexOfListener : function (listener, context) { - var n = this._bindings.length, - cur; - while (n--) { - cur = this._bindings[n]; - if (cur._listener === listener && cur.context === context) { - return n; - } - } - return -1; - }, - - /** - * Check if listener was attached to Signal. - * @param {Function} listener - * @param {Object} [context] - * @return {boolean} if Signal has the specified listener. - */ - has : function (listener, context) { - return this._indexOfListener(listener, context) !== -1; - }, - - /** - * Add a listener to the signal. - * @param {Function} listener Signal handler function. - * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) - * @return {SignalBinding} An Object representing the binding between the Signal and listener. - */ - add : function (listener, listenerContext, priority) { - validateListener(listener, 'add'); - return this._registerListener(listener, false, listenerContext, priority); - }, - - /** - * Add listener to the signal that should be removed after first execution (will be executed only once). - * @param {Function} listener Signal handler function. - * @param {Object} [listenerContext] Context on which listener will be executed (object that should represent the `this` variable inside listener function). - * @param {Number} [priority] The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0) - * @return {SignalBinding} An Object representing the binding between the Signal and listener. - */ - addOnce : function (listener, listenerContext, priority) { - validateListener(listener, 'addOnce'); - return this._registerListener(listener, true, listenerContext, priority); - }, - - /** - * Remove a single listener from the dispatch queue. - * @param {Function} listener Handler function that should be removed. - * @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context). - * @return {Function} Listener handler function. - */ - remove : function (listener, context) { - validateListener(listener, 'remove'); - - var i = this._indexOfListener(listener, context); - if (i !== -1) { - this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal - this._bindings.splice(i, 1); - } - return listener; - }, - - /** - * Remove all listeners from the Signal. - */ - removeAll : function () { - var n = this._bindings.length; - while (n--) { - this._bindings[n]._destroy(); - } - this._bindings.length = 0; - }, - - /** - * @return {number} Number of listeners attached to the Signal. - */ - getNumListeners : function () { - return this._bindings.length; - }, - - /** - * Stop propagation of the event, blocking the dispatch to next listeners on the queue. - *

IMPORTANT: should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.

- * @see Signal.prototype.disable - */ - halt : function () { - this._shouldPropagate = false; - }, - - /** - * Dispatch/Broadcast Signal to all listeners added to the queue. - * @param {...*} [params] Parameters that should be passed to each handler. - */ - dispatch : function (params) { - if (! this.active) { - return; - } - - var paramsArr = Array.prototype.slice.call(arguments), - n = this._bindings.length, - bindings; - - if (this.memorize) { - this._prevParams = paramsArr; - } - - if (! n) { - //should come after memorize - return; - } - - bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch - this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. - - //execute all callbacks until end of the list or until a callback returns `false` or stops propagation - //reverse loop since listeners with higher priority will be added at the end of the list - do { n--; } while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); - }, - - /** - * Forget memorized arguments. - * @see Signal.memorize - */ - forget : function(){ - this._prevParams = null; - }, - - /** - * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). - *

IMPORTANT: calling any method on the signal instance after calling dispose will throw errors.

- */ - dispose : function () { - this.removeAll(); - delete this._bindings; - delete this._prevParams; - }, - - /** - * @return {string} String representation of the object. - */ - toString : function () { - return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']'; - } + // Routes params should be type casted. e.g the dynamic path items/:id when id is 33 + // will end up passing the integer 33 as an argument, not the string "33". + roads.shouldTypecast = true; + // Nil transitions are prevented from our side. + roads.ignoreState = true; - }; + /* + * Setting a new state will start a transition from the current state to the target state. + * A successful transition will result in the URL being changed. + * A failed transition will leave the router in its current state. + */ + function setState(state, params) { + if (isSameState(state, params)) return; + var fromState; + var toState = StateWithParams(state, params); - // Namespace ----------------------------------------------------- - //================================================================ + if (transition) { + cancelTransition(); + fromState = StateWithParams(transition.currentState, transition.toParams); + } + else { + fromState = currentState; + } - /** - * Signals namespace - * @namespace - * @name signals - */ - var signals = Signal; + // While the transition is running, any code asking the router about the current state should + // get the end result state. The currentState is rollbacked if the transition fails. + currentState = toState; - /** - * Custom event broadcaster - * @see Signal - */ - // alias for backwards compatibility (see #gh-44) - signals.Signal = Signal; + startingTransition(fromState, toState); + transition = Transition( + fromState, + toState, + paramDiff(fromState && fromState.params, params)); - global['signals'] = signals; + transition.then( + function success() { + var historyState; + transition = null; - return Signal; + if (!poppedState && !firstTransition) { + historyState = ('/' + currentPathQuery).replace('//', '/'); + log('Pushing state: {0}', historyState); + history.pushState(historyState, document.title, historyState); + } -}(window)); -/** @license - * crossroads - * Author: Miller Medeiros | MIT License - * v0.12.0 (2013/01/21 13:47) - */ + transitionCompleted(fromState, toState); + }, + function fail(error) { + transition = null; + currentState = fromState; -var crossroads = (function () { -var factory = function (signals) { + logError('Transition from {0} to {1} failed: {2}', fromState, toState, error); + router.transition.failed.dispatch(fromState, toState); + }); + } - var crossroads, - _hasOptionalGroupBug, - UNDEF; + function cancelTransition() { + log('Cancelling existing transition from {0} to {1}', + transition.from, transition.to); - // Helpers ----------- - //==================== + transition.cancel(); - // IE 7-8 capture optional groups as empty strings while other browsers - // capture as `undefined` - _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === ''; + router.transition.cancelled.dispatch(transition.from, transition.to); + } - function arrayIndexOf(arr, val) { - if (arr.indexOf) { - return arr.indexOf(val); - } else { - //Array.indexOf doesn't work on IE 6-7 - var n = arr.length; - while (n--) { - if (arr[n] === val) { - return n; - } - } - return -1; - } - } + function startingTransition(fromState, toState) { + log('Starting transition from {0} to {1}', fromState, toState); - function arrayRemove(arr, item) { - var i = arrayIndexOf(arr, item); - if (i !== -1) { - arr.splice(i, 1); - } - } + router.transition.started.dispatch(fromState, toState); + } - function isKind(val, kind) { - return '[object '+ kind +']' === Object.prototype.toString.call(val); - } + function transitionCompleted(fromState, toState) { + log('Transition from {0} to {1} completed', fromState, toState); - function isRegExp(val) { - return isKind(val, 'RegExp'); - } + router.transition.completed.dispatch(fromState, toState); + firstTransition = false; + } - function isArray(val) { - return isKind(val, 'Array'); - } + /* + * Return whether the passed state is the same as the current one; + * in which case the router can ignore the change. + */ + function isSameState(newState, newParams) { + var state, params, diff; - function isFunction(val) { - return typeof val === 'function'; + if (transition) { + state = transition.to; + params = transition.toParams; } - - //borrowed from AMD-utils - function typecastValue(val) { - var r; - if (val === null || val === 'null') { - r = null; - } else if (val === 'true') { - r = true; - } else if (val === 'false') { - r = false; - } else if (val === UNDEF || val === 'undefined') { - r = UNDEF; - } else if (val === '' || isNaN(val)) { - //isNaN('') returns false - r = val; - } else { - //parseFloat(null || '') returns NaN - r = parseFloat(val); - } - return r; + else if (currentState) { + state = currentState._state; + params = currentState.params; } - function typecastArrayValues(values) { - var n = values.length, - result = []; - while (n--) { - result[n] = typecastValue(values[n]); - } - return result; - } + diff = paramDiff(params, newParams); - //borrowed from AMD-Utils - function decodeQueryString(str, shouldTypecast) { - var queryArr = (str || '').replace('?', '').split('&'), - n = queryArr.length, - obj = {}, - item, val; - while (n--) { - item = queryArr[n].split('='); - val = shouldTypecast ? typecastValue(item[1]) : item[1]; - obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val; - } - return obj; - } + return (newState == state) && (util.objectSize(diff) == 0); + } + /* + * Return the set of all the params that changed (Either added, removed or changed). + */ + function paramDiff(oldParams, newParams) { + var diff = {}, + oldParams = oldParams || {}; - // Crossroads -------- - //==================== + for (var name in oldParams) + if (oldParams[name] != newParams[name]) diff[name] = 1; - /** - * @constructor - */ - function Crossroads() { - this.bypassed = new signals.Signal(); - this.routed = new signals.Signal(); - this._routes = []; - this._prevRoutes = []; - this._piped = []; - this.resetState(); - } + for (var name in newParams) + if (oldParams[name] != newParams[name]) diff[name] = 1; - Crossroads.prototype = { - - greedy : false, - - greedyEnabled : true, - - ignoreCase : true, - - ignoreState : false, - - shouldTypecast : false, - - normalizeFn : null, - - resetState : function(){ - this._prevRoutes.length = 0; - this._prevMatchedRequest = null; - this._prevBypassedRequest = null; - }, - - create : function () { - return new Crossroads(); - }, - - addRoute : function (pattern, callback, priority) { - var route = new Route(pattern, callback, priority, this); - this._sortedInsert(route); - return route; - }, - - removeRoute : function (route) { - arrayRemove(this._routes, route); - route._destroy(); - }, - - removeAllRoutes : function () { - var n = this.getNumRoutes(); - while (n--) { - this._routes[n]._destroy(); - } - this._routes.length = 0; - }, - - parse : function (request, defaultArgs) { - request = request || ''; - defaultArgs = defaultArgs || []; - - // should only care about different requests if ignoreState isn't true - if ( !this.ignoreState && - (request === this._prevMatchedRequest || - request === this._prevBypassedRequest) ) { - return; - } - - var routes = this._getMatchedRoutes(request), - i = 0, - n = routes.length, - cur; - - if (n) { - this._prevMatchedRequest = request; - - this._notifyPrevRoutes(routes, request); - this._prevRoutes = routes; - //should be incremental loop, execute routes in order - while (i < n) { - cur = routes[i]; - cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params)); - cur.isFirst = !i; - this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur])); - i += 1; - } - } else { - this._prevBypassedRequest = request; - this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request])); - } - - this._pipeParse(request, defaultArgs); - }, - - _notifyPrevRoutes : function(matchedRoutes, request) { - var i = 0, prev; - while (prev = this._prevRoutes[i++]) { - //check if switched exist since route may be disposed - if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) { - prev.route.switched.dispatch(request); - } - } - }, - - _didSwitch : function (route, matchedRoutes){ - var matched, - i = 0; - while (matched = matchedRoutes[i++]) { - // only dispatch switched if it is going to a different route - if (matched.route === route) { - return false; - } - } - return true; - }, - - _pipeParse : function(request, defaultArgs) { - var i = 0, route; - while (route = this._piped[i++]) { - route.parse(request, defaultArgs); - } - }, - - getNumRoutes : function () { - return this._routes.length; - }, - - _sortedInsert : function (route) { - //simplified insertion sort - var routes = this._routes, - n = routes.length; - do { --n; } while (routes[n] && route._priority <= routes[n]._priority); - routes.splice(n+1, 0, route); - }, - - _getMatchedRoutes : function (request) { - var res = [], - routes = this._routes, - n = routes.length, - route; - //should be decrement loop since higher priorities are added at the end of array - while (route = routes[--n]) { - if ((!res.length || this.greedy || route.greedy) && route.match(request)) { - res.push({ - route : route, - params : route._getParamsArray(request) - }); - } - if (!this.greedyEnabled && res.length) { - break; - } - } - return res; - }, - - pipe : function (otherRouter) { - this._piped.push(otherRouter); - }, - - unpipe : function (otherRouter) { - arrayRemove(this._piped, otherRouter); - }, - - toString : function () { - return '[crossroads numRoutes:'+ this.getNumRoutes() +']'; - } - }; + return diff; + } - //"static" instance - crossroads = new Crossroads(); - crossroads.VERSION = '0.12.0'; + /* + * The state wasn't found; + * Transition to the 'notFound' state if the developer specified it or else throw an error. + */ + function notFound(state) { + log('State not found: {0}', state); - crossroads.NORM_AS_ARRAY = function (req, vals) { - return [vals.vals_]; - }; + if (states.notFound) setState(states.notFound); + else throw new Error ('State "' + state + '" could not be found'); + } - crossroads.NORM_AS_OBJECT = function (req, vals) { - return [vals]; - }; + /* + * Configure the router before its initialization. + */ + function configure(options) { + util.mergeObjects(initOptions, options); + return router; + } + /* + * Initialize and freeze the router (states can not be added afterwards). + * The router will immediately initiate a transition to, in order of priority: + * 1) The state captured by the current URL + * 2) The init state passed as an argument + * 3) The default state (pathless and queryless) + */ + function init(initState) { + if (initOptions.enableLogs) + Router.enableLogs(); - // Route -------------- - //===================== - - /** - * @constructor - */ - function Route(pattern, callback, priority, router) { - var isRegexPattern = isRegExp(pattern), - patternLexer = router.patternLexer; - this._router = router; - this._pattern = pattern; - this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern); - this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern); - this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase); - this.matched = new signals.Signal(); - this.switched = new signals.Signal(); - if (callback) { - this.matched.add(callback); - } - this._priority = priority || 0; - } + if (initOptions.interceptAnchorClicks) + interceptAnchorClicks(router); - Route.prototype = { - - greedy : false, - - rules : void(0), - - match : function (request) { - request = request || ''; - return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule. - }, - - _validateParams : function (request) { - var rules = this.rules, - values = this._getParamsObject(request), - key; - for (key in rules) { - // normalize_ isn't a validation rule... (#39) - if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){ - return false; - } - } - return true; - }, - - _isValidParam : function (request, prop, values) { - var validationRule = this.rules[prop], - val = values[prop], - isValid = false, - isQuery = (prop.indexOf('?') === 0); - - if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) { - isValid = true; - } - else if (isRegExp(validationRule)) { - if (isQuery) { - val = values[prop +'_']; //use raw string - } - isValid = validationRule.test(val); - } - else if (isArray(validationRule)) { - if (isQuery) { - val = values[prop +'_']; //use raw string - } - isValid = this._isValidArrayRule(validationRule, val); - } - else if (isFunction(validationRule)) { - isValid = validationRule(val, request, values); - } - - return isValid; //fail silently if validationRule is from an unsupported type - }, - - _isValidArrayRule : function (arr, val) { - if (! this._router.ignoreCase) { - return arrayIndexOf(arr, val) !== -1; - } - - if (typeof val === 'string') { - val = val.toLowerCase(); - } - - var n = arr.length, - item, - compareVal; - - while (n--) { - item = arr[n]; - compareVal = (typeof item === 'string')? item.toLowerCase() : item; - if (compareVal === val) { - return true; - } - } - return false; - }, - - _getParamsObject : function (request) { - var shouldTypecast = this._router.shouldTypecast, - values = this._router.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast), - o = {}, - n = values.length, - param, val; - while (n--) { - val = values[n]; - if (this._paramsIds) { - param = this._paramsIds[n]; - if (param.indexOf('?') === 0 && val) { - //make a copy of the original string so array and - //RegExp validation can be applied properly - o[param +'_'] = val; - //update vals_ array as well since it will be used - //during dispatch - val = decodeQueryString(val, shouldTypecast); - values[n] = val; - } - // IE will capture optional groups as empty strings while other - // browsers will capture `undefined` so normalize behavior. - // see: #gh-58, #gh-59, #gh-60 - if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) { - val = void(0); - values[n] = val; - } - o[param] = val; - } - //alias to paths and for RegExp pattern - o[n] = val; - } - o.request_ = shouldTypecast? typecastValue(request) : request; - o.vals_ = values; - return o; - }, - - _getParamsArray : function (request) { - var norm = this.rules? this.rules.normalize_ : null, - params; - norm = norm || this._router.normalizeFn; // default normalize - if (norm && isFunction(norm)) { - params = norm(request, this._getParamsObject(request)); - } else { - params = this._getParamsObject(request).vals_; - } - return params; - }, - - interpolate : function(replacements) { - var str = this._router.patternLexer.interpolate(this._pattern, replacements); - if (! this._validateParams(str) ) { - throw new Error('Generated string doesn\'t validate against `Route.rules`.'); - } - return str; - }, - - dispose : function () { - this._router.removeRoute(this); - }, - - _destroy : function () { - this.matched.dispose(); - this.switched.dispose(); - this.matched = this.switched = this._pattern = this._matchRegexp = null; - }, - - toString : function () { - return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']'; - } + log('Router init'); + initStates(); - }; + var initialState = (!Router.ignoreInitialURL && urlPathQuery()) || initState || ''; + log('Initializing to state {0}', initialState || '""'); + state(initialState); + window.onpopstate = function(evt) { + // history.js will dispatch fake popstate events on HTML4 browsers' hash changes; + // in these cases, evt.state is null. + var newState = evt.state || urlPathQuery(); - // Pattern Lexer ------ - //===================== - - Crossroads.prototype.patternLexer = (function () { - - var - //match chars that should be escaped on string regexp - ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, - - //trailing slashes (begin/end of string) - LOOSE_SLASHES_REGEXP = /^\/|\/$/g, - LEGACY_SLASHES_REGEXP = /\/$/g, - - //params - everything between `{ }` or `: :` - PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, - - //used to save params during compile (avoid escaping things that - //shouldn't be escaped). - TOKENS = { - 'OS' : { - //optional slashes - //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?` - rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g, - save : '$1{{id}}$2', - res : '\\/?' - }, - 'RS' : { - //required slashes - //used to insert slash between `:{` and `}{` - rgx : /([:}])\/?(\{)/g, - save : '$1{{id}}$2', - res : '\\/' - }, - 'RQ' : { - //required query string - everything in between `{? }` - rgx : /\{\?([^}]+)\}/g, - //everything from `?` till `#` or end of string - res : '\\?([^#]+)' - }, - 'OQ' : { - //optional query string - everything in between `:? :` - rgx : /:\?([^:]+):/g, - //everything from `?` till `#` or end of string - res : '(?:\\?([^#]*))?' - }, - 'OR' : { - //optional rest - everything in between `: *:` - rgx : /:([^:]+)\*:/g, - res : '(.*)?' // optional group to avoid passing empty string as captured - }, - 'RR' : { - //rest param - everything in between `{ *}` - rgx : /\{([^}]+)\*\}/g, - res : '(.+)' - }, - // required/optional params should come after rest segments - 'RP' : { - //required params - everything between `{ }` - rgx : /\{([^}]+)\}/g, - res : '([^\\/?]+)' - }, - 'OP' : { - //optional params - everything between `: :` - rgx : /:([^:]+):/g, - res : '([^\\/?]+)?\/?' - } - }, - - LOOSE_SLASH = 1, - STRICT_SLASH = 2, - LEGACY_SLASH = 3, - - _slashMode = LOOSE_SLASH; - - - function precompileTokens(){ - var key, cur; - for (key in TOKENS) { - if (TOKENS.hasOwnProperty(key)) { - cur = TOKENS[key]; - cur.id = '__CR_'+ key +'__'; - cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id; - cur.rRestore = new RegExp(cur.id, 'g'); - } - } - } - precompileTokens(); - - - function captureVals(regex, pattern) { - var vals = [], match; - // very important to reset lastIndex since RegExp can have "g" flag - // and multiple runs might affect the result, specially if matching - // same string multiple times on IE 7-8 - regex.lastIndex = 0; - while (match = regex.exec(pattern)) { - vals.push(match[1]); - } - return vals; - } + log('Popped state: {0}', newState); + poppedState = true; + setStateForPathQuery(newState); + }; - function getParamIds(pattern) { - return captureVals(PARAMS_REGEXP, pattern); - } + initialized = true; + return router; + } - function getOptionalParamsIds(pattern) { - return captureVals(TOKENS.OP.rgx, pattern); - } + function initStates() { + eachRootState(function(name, state) { + state.init(name); + }); - function compilePattern(pattern, ignoreCase) { - pattern = pattern || ''; - - if(pattern){ - if (_slashMode === LOOSE_SLASH) { - pattern = pattern.replace(LOOSE_SLASHES_REGEXP, ''); - } - else if (_slashMode === LEGACY_SLASH) { - pattern = pattern.replace(LEGACY_SLASHES_REGEXP, ''); - } - - //save tokens - pattern = replaceTokens(pattern, 'rgx', 'save'); - //regexp escape - pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&'); - //restore tokens - pattern = replaceTokens(pattern, 'rRestore', 'res'); - - if (_slashMode === LOOSE_SLASH) { - pattern = '\\/?'+ pattern; - } - } - - if (_slashMode !== STRICT_SLASH) { - //single slash is treated as empty and end slash is optional - pattern += '\\/?'; - } - return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : ''); - } + leafStates = {}; - function replaceTokens(pattern, regexpName, replaceName) { - var cur, key; - for (key in TOKENS) { - if (TOKENS.hasOwnProperty(key)) { - cur = TOKENS[key]; - pattern = pattern.replace(cur[regexpName], cur[replaceName]); - } - } - return pattern; - } + // Only leaf states can be transitioned to. + eachLeafState(function(state) { + leafStates[state.fullName] = state; - function getParamValues(request, regexp, shouldTypecast) { - var vals = regexp.exec(request); - if (vals) { - vals.shift(); - if (shouldTypecast) { - vals = typecastArrayValues(vals); - } - } - return vals; - } + state.route = roads.addRoute(state.fullPath() + ":?query:"); + state.route.matched.add(function() { + stateFound = true; + setState(state, toParams(state, arguments)); + }); + }); + } - function interpolate(pattern, replacements) { - if (typeof pattern !== 'string') { - throw new Error('Route pattern should be a string.'); - } - - var replaceFn = function(match, prop){ - var val; - prop = (prop.substr(0, 1) === '?')? prop.substr(1) : prop; - if (replacements[prop] != null) { - if (typeof replacements[prop] === 'object') { - var queryParts = []; - for(var key in replacements[prop]) { - queryParts.push(encodeURI(key + '=' + replacements[prop][key])); - } - val = '?' + queryParts.join('&'); - } else { - // make sure value is a string see #gh-54 - val = String(replacements[prop]); - } - - if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) { - throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".'); - } - } - else if (match.indexOf('{') !== -1) { - throw new Error('The segment '+ match +' is required.'); - } - else { - val = ''; - } - return val; - }; - - if (! TOKENS.OS.trail) { - TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$'); - } - - return pattern - .replace(TOKENS.OS.rgx, TOKENS.OS.save) - .replace(PARAMS_REGEXP, replaceFn) - .replace(TOKENS.OS.trail, '') // remove trailing - .replace(TOKENS.OS.rRestore, '/'); // add slash between segments - } + function eachRootState(callback) { + for (var name in states) callback(name, states[name]); + } - //API - return { - strict : function(){ - _slashMode = STRICT_SLASH; - }, - loose : function(){ - _slashMode = LOOSE_SLASH; - }, - legacy : function(){ - _slashMode = LEGACY_SLASH; - }, - getParamIds : getParamIds, - getOptionalParamsIds : getOptionalParamsIds, - getParamValues : getParamValues, - compilePattern : compilePattern, - interpolate : interpolate - }; - - }()); - - - return crossroads; -}; + function eachLeafState(callback) { + var name, state; + function callbackIfLeaf(states) { + states.forEach(function(state) { + if (state.children.length) + callbackIfLeaf(state.children); + else + callback(state); + }); + } -return factory(window['signals']); + callbackIfLeaf(util.objectToArray(states)); + } + /* + * Request a programmatic state change. + * + * Two notations are supported: + * state('my.target.state', {id: 33, filter: 'desc'}) + * state('target/33?filter=desc') + */ + function state(pathQueryOrName, params) { + var isName = (pathQueryOrName.indexOf('.') > -1 || leafStates[pathQueryOrName]); -}()); + log('Changing state to {0}', pathQueryOrName || '""'); -/** @license MIT License (c) copyright 2011-2013 original author or authors */ + poppedState = false; + if (isName) setStateByName(pathQueryOrName, params || {}); + else setStateForPathQuery(pathQueryOrName); + } -/** - * A lightweight CommonJS Promises/A and when() implementation - * when is part of the cujo.js family of libraries (http://cujojs.com/) - * - * Licensed under the MIT License at: - * http://www.opensource.org/licenses/mit-license.php - * - * @author Brian Cavalier - * @author John Hann - * @version 2.5.1 - */ -var when = (function(global) { 'use strict'; + /* + * An alias of 'state'. You can use 'redirect' when it makes more sense semantically. + */ + function redirect(pathQueryOrName, params) { + log('Redirecting...'); + state(pathQueryOrName, params); + } - var require; + function setStateForPathQuery(pathQuery) { + currentPathQuery = pathQuery; + stateFound = false; + roads.parse(pathQuery); - // Public API + if (!stateFound) notFound(pathQuery); + } - when.promise = promise; // Create a pending promise - when.resolve = resolve; // Create a resolved promise - when.reject = reject; // Create a rejected promise - when.defer = defer; // Create a {promise, resolver} pair + function setStateByName(name, params) { + var state = leafStates[name]; - when.join = join; // Join 2 or more promises + if (!state) return notFound(name); - when.all = all; // Resolve a list of promises - when.map = map; // Array.map() for promises - when.reduce = reduce; // Array.reduce() for promises - when.settle = settle; // Settle a list of promises + var pathQuery = state.route.interpolate(params); + setStateForPathQuery(pathQuery); + } - when.any = any; // One-winner race - when.some = some; // Multi-winner race - - when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike - when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable - - /** - * Register an observer for a promise or immediate value. - * - * @param {*} promiseOrValue - * @param {function?} [onFulfilled] callback to be called when promiseOrValue is - * successfully fulfilled. If promiseOrValue is an immediate value, callback - * will be invoked immediately. - * @param {function?} [onRejected] callback to be called when promiseOrValue is - * rejected. - * @param {function?} [onProgress] callback to be called when progress updates - * are issued for promiseOrValue. - * @returns {Promise} a new {@link Promise} that will complete with the return - * value of callback or errback or the completion value of promiseOrValue if - * callback and/or errback is not supplied. - */ - function when(promiseOrValue, onFulfilled, onRejected, onProgress) { - // Get a trusted promise for the input promiseOrValue, and then - // register promise handlers - return cast(promiseOrValue).then(onFulfilled, onRejected, onProgress); - } + /* + * Add a new root state to the router. + * The name must be unique among root states. + */ + function addState(name, state) { + if (initialized) + throw new Error('States can only be added before the Router is initialized'); - function cast(x) { - return x instanceof Promise ? x : resolve(x); - } + if (states[name]) + throw new Error('A state already exist in the router with the name ' + name); - /** - * Trusted Promise constructor. A Promise created from this constructor is - * a trusted when.js promise. Any other duck-typed promise is considered - * untrusted. - * @constructor - * @param {function} sendMessage function to deliver messages to the promise's handler - * @param {function?} inspect function that reports the promise's state - * @name Promise - */ - function Promise(sendMessage, inspect) { - this._message = sendMessage; - this.inspect = inspect; - } + log('Adding state {0}', name); - Promise.prototype = { - /** - * Register handlers for this promise. - * @param [onFulfilled] {Function} fulfillment handler - * @param [onRejected] {Function} rejection handler - * @param [onProgress] {Function} progress handler - * @return {Promise} new Promise - */ - then: function(onFulfilled, onRejected, onProgress) { - /*jshint unused:false*/ - var args, sendMessage; - - args = arguments; - sendMessage = this._message; - - return _promise(function(resolve, reject, notify) { - sendMessage('when', args, resolve, notify); - }, this._status && this._status.observed()); - }, - - /** - * Register a rejection handler. Shortcut for .then(undefined, onRejected) - * @param {function?} onRejected - * @return {Promise} - */ - otherwise: function(onRejected) { - return this.then(undef, onRejected); - }, - - /** - * Ensures that onFulfilledOrRejected will be called regardless of whether - * this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT - * receive the promises' value or reason. Any returned value will be disregarded. - * onFulfilledOrRejected may throw or return a rejected promise to signal - * an additional error. - * @param {function} onFulfilledOrRejected handler to be called regardless of - * fulfillment or rejection - * @returns {Promise} - */ - ensure: function(onFulfilledOrRejected) { - return typeof onFulfilledOrRejected === 'function' - ? this.then(injectHandler, injectHandler)['yield'](this) - : this; - - function injectHandler() { - return resolve(onFulfilledOrRejected()); - } - }, - - /** - * Shortcut for .then(function() { return value; }) - * @param {*} value - * @return {Promise} a promise that: - * - is fulfilled if value is not a promise, or - * - if value is a promise, will fulfill with its value, or reject - * with its reason. - */ - 'yield': function(value) { - return this.then(function() { - return value; - }); - }, - - /** - * Runs a side effect when this promise fulfills, without changing the - * fulfillment value. - * @param {function} onFulfilledSideEffect - * @returns {Promise} - */ - tap: function(onFulfilledSideEffect) { - return this.then(onFulfilledSideEffect)['yield'](this); - }, - - /** - * Assumes that this promise will fulfill with an array, and arranges - * for the onFulfilled to be called with the array as its argument list - * i.e. onFulfilled.apply(undefined, array). - * @param {function} onFulfilled function to receive spread arguments - * @return {Promise} - */ - spread: function(onFulfilled) { - return this.then(function(array) { - // array may contain promises, so resolve its contents. - return all(array, function(array) { - return onFulfilled.apply(undef, array); - }); - }); - }, - - /** - * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected) - * @deprecated - */ - always: function(onFulfilledOrRejected, onProgress) { - return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); - } - }; + states[name] = state; - /** - * Returns a resolved promise. The returned promise will be - * - fulfilled with promiseOrValue if it is a value, or - * - if promiseOrValue is a promise - * - fulfilled with promiseOrValue's value after it is fulfilled - * - rejected with promiseOrValue's reason after it is rejected - * @param {*} value - * @return {Promise} - */ - function resolve(value) { - return promise(function(resolve) { - resolve(value); - }); + return router; } - /** - * Returns a rejected promise for the supplied promiseOrValue. The returned - * promise will be rejected with: - * - promiseOrValue, if it is a value, or - * - if promiseOrValue is a promise - * - promiseOrValue's value after it is fulfilled - * - promiseOrValue's reason after it is rejected - * @param {*} promiseOrValue the rejected value of the returned {@link Promise} - * @return {Promise} rejected {@link Promise} - */ - function reject(promiseOrValue) { - return when(promiseOrValue, rejected); + function urlPathQuery() { + var hashSlash = location.href.indexOf('#/'); + return hashSlash > -1 + ? location.href.slice(hashSlash + 2) + : (location.pathname + location.search).slice(1); } - /** - * Creates a {promise, resolver} pair, either or both of which - * may be given out safely to consumers. - * The resolver has resolve, reject, and progress. The promise - * has then plus extended promise API. - * - * @return {{ - * promise: Promise, - * resolve: function:Promise, - * reject: function:Promise, - * notify: function:Promise - * resolver: { - * resolve: function:Promise, - * reject: function:Promise, - * notify: function:Promise - * }}} - */ - function defer() { - var deferred, pending, resolved; - - // Optimize object shape - deferred = { - promise: undef, resolve: undef, reject: undef, notify: undef, - resolver: { resolve: undef, reject: undef, notify: undef } - }; + /* + * Translate the crossroads argument format to what we want to use. + * We want to keep the path and query names and merge them all in one object for convenience. + */ + function toParams(state, crossroadsArgs) { + var args = Array.prototype.slice.apply(crossroadsArgs), + query = args.pop(), + params = {}, + pathName; - deferred.promise = pending = promise(makeDeferred); + state.fullPath().replace(/\{\w*\}/g, function(match) { + pathName = match.slice(1, -1); + params[pathName] = args.shift(); + return ''; + }); - return deferred; + if (query) util.mergeObjects(params, query); - function makeDeferred(resolvePending, rejectPending, notifyPending) { - deferred.resolve = deferred.resolver.resolve = function(value) { - if(resolved) { - return resolve(value); - } - resolved = true; - resolvePending(value); - return pending; - }; - - deferred.reject = deferred.resolver.reject = function(reason) { - if(resolved) { - return resolve(rejected(reason)); - } - resolved = true; - rejectPending(reason); - return pending; - }; - - deferred.notify = deferred.resolver.notify = function(update) { - notifyPending(update); - return update; - }; + // Decode all params + for (var i in params) { + if (util.isString(params[i])) params[i] = decodeURIComponent(params[i]); } - } - /** - * Creates a new promise whose fate is determined by resolver. - * @param {function} resolver function(resolve, reject, notify) - * @returns {Promise} promise whose fate is determine by resolver - */ - function promise(resolver) { - return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); + return params; } - /** - * Creates a new promise, linked to parent, whose fate is determined - * by resolver. - * @param {function} resolver function(resolve, reject, notify) - * @param {Promise?} status promise from which the new promise is begotten - * @returns {Promise} promise whose fate is determine by resolver - * @private - */ - function _promise(resolver, status) { - var self, value, consumers = []; - - self = new Promise(_message, inspect); - self._status = status; - - // Call the provider resolver to seal the promise's fate - try { - resolver(promiseResolve, promiseReject, promiseNotify); - } catch(e) { - promiseReject(e); - } - - // Return the promise - return self; - - /** - * Private message delivery. Queues and delivers messages to - * the promise's ultimate fulfillment value or rejection reason. - * @private - * @param {String} type - * @param {Array} args - * @param {Function} resolve - * @param {Function} notify - */ - function _message(type, args, resolve, notify) { - consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); - - function deliver(p) { - p._message(type, args, resolve, notify); - } - } - - /** - * Returns a snapshot of the promise's state at the instant inspect() - * is called. The returned object is not live and will not update as - * the promise's state changes. - * @returns {{ state:String, value?:*, reason?:* }} status snapshot - * of the promise. - */ - function inspect() { - return value ? value.inspect() : toPendingState(); - } - - /** - * Transition from pre-resolution state to post-resolution state, notifying - * all listeners of the ultimate fulfillment or rejection - * @param {*|Promise} val resolution value - */ - function promiseResolve(val) { - if(!consumers) { - return; - } - - var queue = consumers; - consumers = undef; - - enqueue(function () { - value = coerce(self, val); - if(status) { - updateStatus(value, status); - } - runHandlers(queue, value); - }); + /* + * Compute a link that can be used in anchors' href attributes + * from a state name and a list of params, a.k.a reverse routing. + */ + function link(stateName, params) { + var query = {}, + allQueryParams = {}, + hasQuery = false, + state = leafStates[stateName]; - } + if (!state) throw new Error('Cannot find state ' + stateName); - /** - * Reject this promise with the supplied reason, which will be used verbatim. - * @param {*} reason reason for the rejection - */ - function promiseReject(reason) { - promiseResolve(rejected(reason)); - } + [state].concat(state.parents).forEach(function(s) { + util.mergeObjects(allQueryParams, s.queryParams); + }); - /** - * Issue a progress event, notifying all progress listeners - * @param {*} update progress event payload to pass to all listeners - */ - function promiseNotify(update) { - if(consumers) { - var queue = consumers; - enqueue(function () { - runHandlers(queue, progressed(update)); - }); + // The passed params are path and query params lumped together, + // Separate them for crossroads' to compute its interpolation. + for (var key in params) { + if (allQueryParams[key]) { + query[key] = params[key]; + delete params[key]; + hasQuery = true; } } - } - - /** - * Run a queue of functions as quickly as possible, passing - * value to each. - */ - function runHandlers(queue, value) { - for (var i = 0; i < queue.length; i++) { - queue[i](value); - } - } - - /** - * Creates a fulfilled, local promise as a proxy for a value - * NOTE: must never be exposed - * @param {*} value fulfillment value - * @returns {Promise} - */ - function fulfilled(value) { - return near( - new NearFulfilledProxy(value), - function() { return toFulfilledState(value); } - ); - } - /** - * Creates a rejected, local promise with the supplied reason - * NOTE: must never be exposed - * @param {*} reason rejection reason - * @returns {Promise} - */ - function rejected(reason) { - return near( - new NearRejectedProxy(reason), - function() { return toRejectedState(reason); } - ); - } + if (hasQuery) params.query = query; - /** - * Creates a near promise using the provided proxy - * NOTE: must never be exposed - * @param {object} proxy proxy for the promise's ultimate value or reason - * @param {function} inspect function that returns a snapshot of the - * returned near promise's state - * @returns {Promise} - */ - function near(proxy, inspect) { - return new Promise(function (type, args, resolve) { - try { - resolve(proxy[type].apply(proxy, args)); - } catch(e) { - resolve(rejected(e)); - } - }, inspect); + return '/' + state.route.interpolate(params).replace('/?', '?'); } - /** - * Create a progress promise with the supplied update. - * @private - * @param {*} update - * @return {Promise} progress promise - */ - function progressed(update) { - return new Promise(function (type, args, _, notify) { - var onProgress = args[2]; - try { - notify(typeof onProgress === 'function' ? onProgress(update) : update); - } catch(e) { - notify(e); - } - }); + /* + * Returns a StateWithParams object representing the current state of the router. + */ + function getCurrentState() { + return currentState; } - /** - * Coerces x to a trusted Promise - * @param {*} x thing to coerce - * @returns {*} Guaranteed to return a trusted Promise. If x - * is trusted, returns x, otherwise, returns a new, trusted, already-resolved - * Promise whose resolution value is: - * * the resolution value of x if it's a foreign promise, or - * * x if it's a value - */ - function coerce(self, x) { - if (x === self) { - return rejected(new TypeError()); - } - - if (x instanceof Promise) { - return x; - } - try { - var untrustedThen = x === Object(x) && x.then; + // Public methods - return typeof untrustedThen === 'function' - ? assimilate(untrustedThen, x) - : fulfilled(x); - } catch(e) { - return rejected(e); - } - } + router.configure = configure; + router.init = init; + router.state = state; + router.redirect = redirect; + router.addState = addState; + router.link = link; + router.currentState = getCurrentState; - /** - * Safely assimilates a foreign thenable by wrapping it in a trusted promise - * @param {function} untrustedThen x's then() method - * @param {object|function} x thenable - * @returns {Promise} - */ - function assimilate(untrustedThen, x) { - return promise(function (resolve, reject) { - fcall(untrustedThen, x, resolve, reject); - }); - } - /** - * Proxy for a near, fulfilled value - * @param {*} value - * @constructor - */ - function NearFulfilledProxy(value) { - this.value = value; - } + // Signals - NearFulfilledProxy.prototype.when = function(onResult) { - return typeof onResult === 'function' ? onResult(this.value) : this.value; + router.transition = { + // Dispatched when a transition started. + started: new Signal(), + // Dispatched when a transition either completed, failed or got cancelled. + ended: new Signal(), + // Dispatched when a transition successfuly completed + completed: new Signal(), + // Dispatched when a transition failed to complete + failed: new Signal(), + // Dispatched when a transition got cancelled + cancelled: new Signal() }; - /** - * Proxy for a near rejection - * @param {*} reason - * @constructor - */ - function NearRejectedProxy(reason) { - this.reason = reason; - } + // Dispatched once after the router successfully reached its initial state. + router.initialized = new Signal(); - NearRejectedProxy.prototype.when = function(_, onError) { - if(typeof onError === 'function') { - return onError(this.reason); - } else { - throw this.reason; - } - }; + router.transition.completed.addOnce(function() { + router.initialized.dispatch(); + }); - function updateStatus(value, status) { - value.then(statusFulfilled, statusRejected); + router.transition.completed.add(transitionEnded); + router.transition.failed.add(transitionEnded); + router.transition.cancelled.add(transitionEnded); - function statusFulfilled() { status.fulfilled(); } - function statusRejected(r) { status.rejected(r); } + function transitionEnded(oldState, newState) { + router.transition.ended.dispatch(oldState, newState); } - /** - * Determines if x is promise-like, i.e. a thenable object - * NOTE: Will return true for *any thenable object*, and isn't truly - * safe, since it may attempt to access the `then` property of x (i.e. - * clever/malicious getters may do weird things) - * @param {*} x anything - * @returns {boolean} true if x is promise-like - */ - function isPromiseLike(x) { - return x && typeof x.then === 'function'; - } + return router; +} - /** - * Initiates a competitive race, returning a promise that will resolve when - * howMany of the supplied promisesOrValues have resolved, or will reject when - * it becomes impossible for howMany to resolve, for example, when - * (promisesOrValues.length - howMany) + 1 input promises reject. - * - * @param {Array} promisesOrValues array of anything, may contain a mix - * of promises and values - * @param howMany {number} number of promisesOrValues to resolve - * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() - * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() - * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() - * @returns {Promise} promise that will resolve to an array of howMany values that - * resolved first, or will reject with an array of - * (promisesOrValues.length - howMany) + 1 rejection reasons. - */ - function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { - - return when(promisesOrValues, function(promisesOrValues) { - - return promise(resolveSome).then(onFulfilled, onRejected, onProgress); - - function resolveSome(resolve, reject, notify) { - var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i; - - len = promisesOrValues.length >>> 0; - - toResolve = Math.max(0, Math.min(howMany, len)); - values = []; - - toReject = (len - toResolve) + 1; - reasons = []; - - // No items in the input, resolve immediately - if (!toResolve) { - resolve(values); - - } else { - rejectOne = function(reason) { - reasons.push(reason); - if(!--toReject) { - fulfillOne = rejectOne = identity; - reject(reasons); - } - }; - - fulfillOne = function(val) { - // This orders the values based on promise resolution order - values.push(val); - if (!--toResolve) { - fulfillOne = rejectOne = identity; - resolve(values); - } - }; - - for(i = 0; i < len; ++i) { - if(i in promisesOrValues) { - when(promisesOrValues[i], fulfiller, rejecter, notify); - } - } - } - function rejecter(reason) { - rejectOne(reason); - } +// Logging - function fulfiller(val) { - fulfillOne(val); - } - } - }); - } +var log = logError = util.noop; - /** - * Initiates a competitive race, returning a promise that will resolve when - * any one of the supplied promisesOrValues has resolved or will reject when - * *all* promisesOrValues have rejected. - * - * @param {Array|Promise} promisesOrValues array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() - * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() - * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() - * @returns {Promise} promise that will resolve to the value that resolved first, or - * will reject with an array of all rejected inputs. - */ - function any(promisesOrValues, onFulfilled, onRejected, onProgress) { +Router.enableLogs = function() { + log = function() { + console.log(getLogMessage(arguments)); + }; - function unwrapSingleResult(val) { - return onFulfilled ? onFulfilled(val[0]) : val[0]; - } + logError = function() { + console.error(getLogMessage(arguments)); + }; - return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress); - } + function getLogMessage(args) { + var message = args[0], + tokens = Array.prototype.slice.call(args, 1); - /** - * Return a promise that will resolve only once all the supplied promisesOrValues - * have resolved. The resolution value of the returned promise will be an array - * containing the resolution values of each of the promisesOrValues. - * @memberOf when - * - * @param {Array|Promise} promisesOrValues array of anything, may contain a mix - * of {@link Promise}s and values - * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() - * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() - * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() - * @returns {Promise} - */ - function all(promisesOrValues, onFulfilled, onRejected, onProgress) { - return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); - } + for (var i = 0, l = tokens.length; i < l; i++) + message = message.replace('{' + i + '}', tokens[i]); - /** - * Joins multiple promises into a single returned promise. - * @return {Promise} a promise that will fulfill when *all* the input promises - * have fulfilled, or will reject when *any one* of the input promises rejects. - */ - function join(/* ...promises */) { - return _map(arguments, identity); + return message; } +}; - /** - * Settles all input promises such that they are guaranteed not to - * be pending once the returned promise fulfills. The returned promise - * will always fulfill, except in the case where `array` is a promise - * that rejects. - * @param {Array|Promise} array or promise for array of promises to settle - * @returns {Promise} promise that always fulfills with an array of - * outcome snapshots for each input promise. - */ - function settle(array) { - return _map(array, toFulfilledState, toRejectedState); - } - /** - * Promise-aware array map function, similar to `Array.prototype.map()`, - * but input array may contain promises or values. - * @param {Array|Promise} array array of anything, may contain promises and values - * @param {function} mapFunc map function which may return a promise or value - * @returns {Promise} promise that will fulfill with an array of mapped values - * or reject if any input promise rejects. - */ - function map(array, mapFunc) { - return _map(array, mapFunc); - } +module.exports = Router; +},{"./StateWithParams":4,"./Transition":5,"./anchorClicks":6,"./util":8,"crossroads":1,"signals":1}],3:[function(require,module,exports){ - /** - * Internal map that allows a fallback to handle rejections - * @param {Array|Promise} array array of anything, may contain promises and values - * @param {function} mapFunc map function which may return a promise or value - * @param {function?} fallback function to handle rejected promises - * @returns {Promise} promise that will fulfill with an array of mapped values - * or reject if any input promise rejects. - */ - function _map(array, mapFunc, fallback) { - return when(array, function(array) { +var util = require('./util'); +var async = require('./Transition').asyncPromises.register; - return _promise(resolveMap); +/* +* Create a new State instance. +* +* State() // A state without options and an empty path. +* State('path', {options}) // A state with a static named path and options +* State(':path', {options}) // A state with a dynamic named path and options +* State('path?query', {options}) // Same as above with an optional query string param named 'query' +* State({options}) // Its path is the empty string. +* +* options is an object with the following optional properties: +* enter, exit, enterPrereqs, exitPrereqs. +* +* Child states can also be specified in the options: +* State({ myChildStateName: State() }) +* This is the declarative equivalent to the addState method. +* +* Finally, options can contain any arbitrary data value +* that will get stored in the state and made available via the data() method: +* State({myData: 55}) +* This is the declarative equivalent to the data(key, value) method. +*/ +function State() { + var state = { _isState: true }, + args = getArgs(arguments), + options = args.options, + states = getStates(args.options), + initialized; - function resolveMap(resolve, reject, notify) { - var results, len, toResolve, i; - // Since we know the resulting length, we can preallocate the results - // array to avoid array expansions. - toResolve = len = array.length >>> 0; - results = []; + state.path = args.path; + state.params = args.params; + state.queryParams = args.queryParams; + state.states = states; - if(!toResolve) { - resolve(results); - return; - } + state.enter = options.enter || util.noop; + state.exit = options.exit || util.noop; + state.enterPrereqs = options.enterPrereqs; + state.exitPrereqs = options.exitPrereqs; - // Since mapFunc may be async, get all invocations of it into flight - for(i = 0; i < len; i++) { - if(i in array) { - resolveOne(array[i], i); - } else { - --toResolve; - } - } + state.ownData = getOwnData(options); - function resolveOne(item, i) { - when(item, mapFunc, fallback).then(function(mapped) { - results[i] = mapped; + /* + * Initialize and freeze this state. + */ + function init(name, parent) { + state.name = name; + state.parent = parent; + state.parents = getParents(); + state.children = getChildren(); + state.fullName = getFullName(); + state.root = state.parents[state.parents.length - 1]; + state.async = async; - if(!--toResolve) { - resolve(results); - } - }, reject, notify); - } - } + eachChildState(function(name, childState) { + childState.init(name, state); }); - } - - /** - * Traditional reduce function, similar to `Array.prototype.reduce()`, but - * input may contain promises and/or values, and reduceFunc - * may return either a value or a promise, *and* initialValue may - * be a promise for the starting value. - * - * @param {Array|Promise} promise array or promise for an array of anything, - * may contain a mix of promises and values. - * @param {function} reduceFunc reduce function reduce(currentValue, nextValue, index, total), - * where total is the total number of items being reduced, and will be the same - * in each call to reduceFunc. - * @returns {Promise} that will resolve to the final reduced value - */ - function reduce(promise, reduceFunc /*, initialValue */) { - var args = fcall(slice, arguments, 1); - - return when(promise, function(array) { - var total; - total = array.length; - - // Wrap the supplied reduceFunc with one that handles promises and then - // delegates to the supplied. - args[0] = function (current, val, i) { - return when(current, function (c) { - return when(val, function (value) { - return reduceFunc(c, value, i, total); - }); - }); - }; - - return reduceArray.apply(array, args); - }); + initialized = true; } - // Snapshot states - - /** - * Creates a fulfilled state snapshot - * @private - * @param {*} x any value - * @returns {{state:'fulfilled',value:*}} - */ - function toFulfilledState(x) { - return { state: 'fulfilled', value: x }; - } + /* + * The full path, composed of all the individual paths of this state and its parents. + */ + function fullPath() { + var result = state.path, + stateParent = state.parent; - /** - * Creates a rejected state snapshot - * @private - * @param {*} x any reason - * @returns {{state:'rejected',reason:*}} - */ - function toRejectedState(x) { - return { state: 'rejected', reason: x }; - } + while (stateParent) { + if (stateParent.path) result = stateParent.path + '/' + result; + stateParent = stateParent.parent; + } - /** - * Creates a pending state snapshot - * @private - * @returns {{state:'pending'}} - */ - function toPendingState() { - return { state: 'pending' }; + return result; } - // - // Internals, utilities, etc. - // - - var reduceArray, slice, fcall, nextTick, handlerQueue, - setTimeout, funcProto, call, arrayProto, monitorApi, - cjsRequire, MutationObserver, undef; - - cjsRequire = require; - - // - // Shared handler queue processing - // - // Credit to Twisol (https://github.com/Twisol) for suggesting - // this type of extensible queue + trampoline approach for - // next-tick conflation. - - handlerQueue = []; + /* + * The list of all parents, starting from the closest ones. + */ + function getParents() { + var parents = [], + parent = state.parent; - /** - * Enqueue a task. If the queue is not currently scheduled to be - * drained, schedule it. - * @param {function} task - */ - function enqueue(task) { - if(handlerQueue.push(task) === 1) { - nextTick(drainQueue); + while (parent) { + parents.push(parent); + parent = parent.parent; } - } - /** - * Drain the handler queue entirely, being careful to allow the - * queue to be extended while it is being processed, and to continue - * processing until it is truly empty. - */ - function drainQueue() { - runHandlers(handlerQueue); - handlerQueue = []; - } - - // capture setTimeout to avoid being caught by fake timers - // used in time based tests - setTimeout = global.setTimeout; - - // Allow attaching the monitor to when() if env has no console - monitorApi = typeof console != 'undefined' ? console : when; - - // Sniff "best" async scheduling option - // Prefer process.nextTick or MutationObserver, then check for - // vertx and finally fall back to setTimeout - /*global process*/ - if (typeof process === 'object' && process.nextTick) { - nextTick = process.nextTick; - } else if(MutationObserver = global.MutationObserver || global.WebKitMutationObserver) { - nextTick = (function(document, MutationObserver, drainQueue) { - var el = document.createElement('div'); - new MutationObserver(drainQueue).observe(el, { attributes: true }); - - return function() { - el.setAttribute('x', 'x'); - }; - }(document, MutationObserver, drainQueue)); - } else { - try { - // vert.x 1.x || 2.x - nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; - } catch(ignore) { - nextTick = function(t) { setTimeout(t, 0); }; - } + return parents; } - // - // Capture/polyfill function and array utils - // - - // Safe function calls - funcProto = Function.prototype; - call = funcProto.call; - fcall = funcProto.bind - ? call.bind(call) - : function(f, context) { - return f.apply(context, slice.call(arguments, 2)); - }; - - // Safe array ops - arrayProto = []; - slice = arrayProto.slice; - - // ES5 reduce implementation if native not available - // See: http://es5.github.com/#x15.4.4.21 as there are many - // specifics and edge cases. ES5 dictates that reduce.length === 1 - // This implementation deviates from ES5 spec in the following ways: - // 1. It does not check if reduceFunc is a Callable - reduceArray = arrayProto.reduce || - function(reduceFunc /*, initialValue */) { - /*jshint maxcomplexity: 7*/ - var arr, args, reduced, len, i; - - i = 0; - arr = Object(this); - len = arr.length >>> 0; - args = arguments; - - // If no initialValue, use first item of array (we know length !== 0 here) - // and adjust i to start at second item - if(args.length <= 1) { - // Skip to the first real element in the array - for(;;) { - if(i in arr) { - reduced = arr[i++]; - break; - } - - // If we reached the end of the array without finding any real - // elements, it's a TypeError - if(++i >= len) { - throw new TypeError(); - } - } - } else { - // If initialValue provided, use it - reduced = args[1]; - } - - // Do the actual reduce - for(;i < len; ++i) { - if(i in arr) { - reduced = reduceFunc(reduced, arr[i], i, arr); - } - } + /* + * The list of child states as an Array. + */ + function getChildren() { + var children = []; - return reduced; - }; + for (var name in states) { + children.push(states[name]); + } - function identity(x) { - return x; + return children; } - return when; -})(this); - -/*! - * History API JavaScript Library v4.0.8 - * - * Support: IE8+, FF3+, Opera 9+, Safari, Chrome and other - * - * Copyright 2011-2013, Dmitrii Pakhtinov ( spb.piksel@gmail.com ) - * - * http://spb-piksel.ru/ - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Update: 2013-10-31 16:06 - */ -(function(window) { - // Prevent the code from running if there is no window.history object - if (!window.history) return; - // symlink to document - var document = window.document; - // HTML element - var documentElement = document.documentElement; - // symlink to sessionStorage - var sessionStorage = null; - // symlink to constructor of Object - var Object = window['Object']; - // symlink to JSON Object - var JSON = window['JSON']; - // symlink to instance object of 'Location' - var windowLocation = window.location; - // symlink to instance object of 'History' - var windowHistory = window.history; - // new instance of 'History'. The default is a reference to the original object instance - var historyObject = windowHistory; - // symlink to method 'history.pushState' - var historyPushState = windowHistory.pushState; - // symlink to method 'history.replaceState' - var historyReplaceState = windowHistory.replaceState; - // if the browser supports HTML5-History-API - var isSupportHistoryAPI = !!historyPushState; - // verifies the presence of an object 'state' in interface 'History' - var isSupportStateObjectInHistory = 'state' in windowHistory; - // symlink to method 'Object.defineProperty' - var defineProperty = Object.defineProperty; - // new instance of 'Location', for IE8 will use the element HTMLAnchorElement, instead of pure object - var locationObject = redefineProperty({}, 't') ? {} : document.createElement('a'); - // prefix for the names of events - var eventNamePrefix = ''; - // String that will contain the name of the method - var addEventListenerName = window.addEventListener ? 'addEventListener' : (eventNamePrefix = 'on') && 'attachEvent'; - // String that will contain the name of the method - var removeEventListenerName = window.removeEventListener ? 'removeEventListener' : 'detachEvent'; - // String that will contain the name of the method - var dispatchEventName = window.dispatchEvent ? 'dispatchEvent' : 'fireEvent'; - // reference native methods for the events - var addEvent = window[addEventListenerName]; - var removeEvent = window[removeEventListenerName]; - var dispatch = window[dispatchEventName]; - // default settings - var settings = {"basepath": '/', "redirect": 0, "type": '/'}; - // key for the sessionStorage - var sessionStorageKey = '__historyAPI__'; - // Anchor Element for parseURL function - var anchorElement = document.createElement('a'); - // last URL before change to new URL - var lastURL = windowLocation.href; - // Control URL, need to fix the bug in Opera - var checkUrlForPopState = ''; - // trigger event 'onpopstate' on page load - var isFireInitialState = false; - // store a list of 'state' objects in the current session - var stateStorage = {}; - // in this object will be stored custom handlers - var eventsList = {}; - // stored last title - var lastTitle = document.title; - - /** - * Properties that will be replaced in the global - * object 'window', to prevent conflicts - * - * @type {Object} - */ - var eventsDescriptors = { - "onhashchange": null, - "onpopstate": null - }; - - /** - * Fix for Chrome in iOS - * See https://github.com/devote/HTML5-History-API/issues/29 - */ - var fastFixChrome = function(method, args) { - var isNeedFix = window.history !== windowHistory; - if (isNeedFix) { - window.history = windowHistory; - } - method.apply(windowHistory, args); - if (isNeedFix) { - window.history = historyObject; - } - }; - - /** - * Properties that will be replaced/added to object - * 'window.history', includes the object 'history.location', - * for a complete the work with the URL address - * - * @type {Object} - */ - var historyDescriptors = { - /** - * @namespace history - * @param {String} [type] - * @param {String} [basepath] - */ - "redirect": function(type, basepath) { - settings["basepath"] = basepath = basepath == null ? settings["basepath"] : basepath; - settings["type"] = type = type == null ? settings["type"] : type; - if (window.top == window.self) { - var relative = parseURL(null, false, true)._relative; - var path = windowLocation.pathname + windowLocation.search; - if (isSupportHistoryAPI) { - path = path.replace(/([^\/])$/, '$1/'); - if (relative != basepath && (new RegExp("^" + basepath + "$", "i")).test(path)) { - windowLocation.replace(relative); - } - } else if (path != basepath) { - path = path.replace(/([^\/])\?/, '$1/?'); - if ((new RegExp("^" + basepath, "i")).test(path)) { - windowLocation.replace(basepath + '#' + path. - replace(new RegExp("^" + basepath, "i"), type) + windowLocation.hash); - } - } - } - }, - /** - * The method adds a state object entry - * to the history. - * - * @namespace history - * @param {Object} state - * @param {string} title - * @param {string} [url] - */ - pushState: function(state, title, url) { - var t = document.title; - if (lastTitle != null) { - document.title = lastTitle; - } - historyPushState && fastFixChrome(historyPushState, arguments); - changeState(state, url); - document.title = t; - lastTitle = title; - }, - /** - * The method updates the state object, - * title, and optionally the URL of the - * current entry in the history. - * - * @namespace history - * @param {Object} state - * @param {string} title - * @param {string} [url] - */ - replaceState: function(state, title, url) { - var t = document.title; - if (lastTitle != null) { - document.title = lastTitle; - } - delete stateStorage[windowLocation.href]; - historyReplaceState && fastFixChrome(historyReplaceState, arguments); - changeState(state, url, true); - document.title = t; - lastTitle = title; - }, - /** - * Object 'history.location' is similar to the - * object 'window.location', except that in - * HTML4 browsers it will behave a bit differently - * - * @namespace history - */ - "location": { - set: function(value) { - window.location = value; - }, - get: function() { - return isSupportHistoryAPI ? windowLocation : locationObject; - } - }, - /** - * A state object is an object representing - * a user interface state. - * - * @namespace history - */ - "state": { - get: function() { - return stateStorage[windowLocation.href] || null; - } - } - }; - - /** - * Properties for object 'history.location'. - * Object 'history.location' is similar to the - * object 'window.location', except that in - * HTML4 browsers it will behave a bit differently - * - * @type {Object} - */ - var locationDescriptors = { - /** - * Navigates to the given page. - * - * @namespace history.location - */ - assign: function(url) { - if (('' + url).indexOf('#') === 0) { - changeState(null, url); - } else { - windowLocation.assign(url); - } - }, - /** - * Reloads the current page. - * - * @namespace history.location - */ - reload: function() { - windowLocation.reload(); - }, - /** - * Removes the current page from - * the session history and navigates - * to the given page. - * - * @namespace history.location - */ - replace: function(url) { - if (('' + url).indexOf('#') === 0) { - changeState(null, url, true); - } else { - windowLocation.replace(url); - } - }, - /** - * Returns the current page's location. - * - * @namespace history.location - */ - toString: function() { - return this.href; - }, - /** - * Returns the current page's location. - * Can be set, to navigate to another page. - * - * @namespace history.location - */ - "href": { - get: function() { - return parseURL()._href; - } - }, - /** - * Returns the current page's protocol. - * - * @namespace history.location - */ - "protocol": null, - /** - * Returns the current page's host and port number. - * - * @namespace history.location - */ - "host": null, - /** - * Returns the current page's host. - * - * @namespace history.location - */ - "hostname": null, - /** - * Returns the current page's port number. - * - * @namespace history.location - */ - "port": null, - /** - * Returns the current page's path only. - * - * @namespace history.location - */ - "pathname": { - get: function() { - return parseURL()._pathname; - } - }, - /** - * Returns the current page's search - * string, beginning with the character - * '?' and to the symbol '#' - * - * @namespace history.location - */ - "search": { - get: function() { - return parseURL()._search; - } - }, - /** - * Returns the current page's hash - * string, beginning with the character - * '#' and to the end line - * - * @namespace history.location - */ - "hash": { - set: function(value) { - changeState(null, ('' + value).replace(/^(#|)/, '#'), false, lastURL); - }, - get: function() { - return parseURL()._hash; - } - } - }; - - /** - * Just empty function - * - * @return void - */ - function emptyFunction() { - // dummy - } - - /** - * Prepares a parts of the current or specified reference for later use in the library - * - * @param {string} [href] - * @param {boolean} [isWindowLocation] - * @param {boolean} [isNotAPI] - * @return {Object} - */ - function parseURL(href, isWindowLocation, isNotAPI) { - var re = /(?:([\w0-9]+:))?(?:\/\/(?:[^@]*@)?([^\/:\?#]+)(?::([0-9]+))?)?([^\?#]*)(?:(\?[^#]+)|\?)?(?:(#.*))?/; - if (href != null && href !== '' && !isWindowLocation) { - var current = parseURL(), _pathname = current._pathname, _protocol = current._protocol; - // convert to type of string - href = '' + href; - // convert relative link to the absolute - href = /^(?:[\w0-9]+\:)?\/\//.test(href) ? href.indexOf("/") === 0 - ? _protocol + href : href : _protocol + "//" + current._host + ( - href.indexOf("/") === 0 ? href : href.indexOf("?") === 0 - ? _pathname + href : href.indexOf("#") === 0 - ? _pathname + current._search + href : _pathname.replace(/[^\/]+$/g, '') + href - ); - } else { - href = isWindowLocation ? href : windowLocation.href; - // if current browser not support History-API - if (!isSupportHistoryAPI || isNotAPI) { - // get hash fragment - href = href.replace(/^[^#]*/, '') || "#"; - // form the absolute link from the hash - href = windowLocation.protocol + '//' + windowLocation.host + settings['basepath'] - + href.replace(new RegExp("^#[\/]?(?:" + settings["type"] + ")?"), ""); - } - } - // that would get rid of the links of the form: /../../ - anchorElement.href = href; - // decompose the link in parts - var result = re.exec(anchorElement.href); - // host name with the port number - var host = result[2] + (result[3] ? ':' + result[3] : ''); - // folder - var pathname = result[4] || '/'; - // the query string - var search = result[5] || ''; - // hash - var hash = result[6] === '#' ? '' : (result[6] || ''); - // relative link, no protocol, no host - var relative = pathname + search + hash; - // special links for set to hash-link, if browser not support History API - var nohash = pathname.replace(new RegExp("^" + settings["basepath"], "i"), settings["type"]) + search; - // result - return { - _href: result[1] + '//' + host + relative, - _protocol: result[1], - _host: host, - _hostname: result[2], - _port: result[3] || '', - _pathname: pathname, - _search: search, - _hash: hash, - _relative: relative, - _nohash: nohash, - _special: nohash + hash - } - } + /* + * The map of initial child states by name. + */ + function getStates(options) { + var states = {}; - /** - * Initializing storage for the custom state's object - */ - function storageInitialize() { - var storage = ''; - if (sessionStorage) { - // get cache from the storage in browser - storage += sessionStorage.getItem(sessionStorageKey); - } else { - var cookie = document.cookie.split(sessionStorageKey + "="); - if (cookie.length > 1) { - storage += (cookie.pop().split(";").shift() || 'null'); - } - } - try { - stateStorage = JSON.parse(storage) || {}; - } catch(_e_) { - stateStorage = {}; - } - // hang up the event handler to event unload page - addEvent(eventNamePrefix + 'unload', function() { - if (sessionStorage) { - // save current state's object - sessionStorage.setItem(sessionStorageKey, JSON.stringify(stateStorage)); - } else { - // save the current 'state' in the cookie - var state = {}; - if (state[windowLocation.href] = historyObject.state) { - document.cookie = sessionStorageKey + '=' + JSON.stringify(state); - } - } - }, false); + for (var key in options) { + if (options[key]._isState) states[key] = options[key]; } - /** - * This method is implemented to override the built-in(native) - * properties in the browser, unfortunately some browsers are - * not allowed to override all the properties and even add. - * For this reason, this was written by a method that tries to - * do everything necessary to get the desired result. - * - * @param {Object} object The object in which will be overridden/added property - * @param {String} prop The property name to be overridden/added - * @param {Object} [descriptor] An object containing properties set/get - * @param {Function} [onWrapped] The function to be called when the wrapper is created - * @return {Object|Boolean} Returns an object on success, otherwise returns false - */ - function redefineProperty(object, prop, descriptor, onWrapped) { - // test only if descriptor is undefined - descriptor = descriptor || {set: emptyFunction}; - // variable will have a value of true the success of attempts to set descriptors - var isDefinedSetter = !descriptor.set; - var isDefinedGetter = !descriptor.get; - // for tests of attempts to set descriptors - var test = {configurable: true, set: function() { - isDefinedSetter = 1; - }, get: function() { - isDefinedGetter = 1; - }}; - - try { - // testing for the possibility of overriding/adding properties - defineProperty(object, prop, test); - // running the test - object[prop] = object[prop]; - // attempt to override property using the standard method - defineProperty(object, prop, descriptor); - } catch(_e_) { - } - - // If the variable 'isDefined' has a false value, it means that need to try other methods - if (!isDefinedSetter || !isDefinedGetter) { - // try to override/add the property, using deprecated functions - if (object.__defineGetter__) { - // testing for the possibility of overriding/adding properties - object.__defineGetter__(prop, test.get); - object.__defineSetter__(prop, test.set); - // running the test - object[prop] = object[prop]; - // attempt to override property using the deprecated functions - descriptor.get && object.__defineGetter__(prop, descriptor.get); - descriptor.set && object.__defineSetter__(prop, descriptor.set); - } - - // Browser refused to override the property, using the standard and deprecated methods - if ((!isDefinedSetter || !isDefinedGetter) && object === window) { - try { - // save original value from this property - var originalValue = object[prop]; - // set null to built-in(native) property - object[prop] = null; - } catch(_e_) { - } - // This rule for Internet Explorer 8 - if ('execScript' in window) { - /** - * to IE8 override the global properties using - * VBScript, declaring it in global scope with - * the same names. - */ - window['execScript']('Public ' + prop, 'VBScript'); - } else { - try { - /** - * This hack allows to override a property - * with the set 'configurable: false', working - * in the hack 'Safari' to 'Mac' - */ - defineProperty(object, prop, {value: emptyFunction}); - } catch(_e_) { - } - } - // set old value to new variable - object[prop] = originalValue; - - } else if (!isDefinedSetter || !isDefinedGetter) { - // the last stage of trying to override the property - try { - try { - // wrap the object in a new empty object - var temp = Object.create(object); - defineProperty(Object.getPrototypeOf(temp) === object ? temp : object, prop, descriptor); - for(var key in object) { - // need to bind a function to the original object - if (typeof object[key] === 'function') { - temp[key] = object[key].bind(object); - } - } - try { - // to run a function that will inform about what the object was to wrapped - onWrapped.call(temp, temp, object); - } catch(_e_) { - } - object = temp; - } catch(_e_) { - // sometimes works override simply by assigning the prototype property of the constructor - defineProperty(object.constructor.prototype, prop, descriptor); - } - } catch(_e_) { - // all methods have failed - return false; - } - } - } + return states; + } - return object; - } + /* + * The fully qualified name of this state. + * e.g granparentName.parentName.name + */ + function getFullName() { + return state.parents.reduceRight(function(acc, parent) { + return acc + parent.name + '.'; + }, '') + state.name; + } - /** - * Adds the missing property in descriptor - * - * @param {Object} object An object that stores values - * @param {String} prop Name of the property in the object - * @param {Object|null} descriptor Descriptor - * @return {Object} Returns the generated descriptor - */ - function prepareDescriptorsForObject(object, prop, descriptor) { - descriptor = descriptor || {}; - // the default for the object 'location' is the standard object 'window.location' - object = object === locationDescriptors ? windowLocation : object; - // setter for object properties - descriptor.set = (descriptor.set || function(value) { - object[prop] = value; - }); - // getter for object properties - descriptor.get = (descriptor.get || function() { - return object[prop]; - }); - return descriptor; - } + function getOwnData(options) { + var reservedKeys = {'enter': 1, 'exit': 1, 'enterPrereqs': 1, 'exitPrereqs': 1}, + result = {}; - /** - * Wrapper for the methods 'addEventListener/attachEvent' in the context of the 'window' - * - * @param {String} event The event type for which the user is registering - * @param {Function} listener The method to be called when the event occurs. - * @param {Boolean} capture If true, capture indicates that the user wishes to initiate capture. - * @return void - */ - function addEventListener(event, listener, capture) { - if (event in eventsList) { - // here stored the event listeners 'popstate/hashchange' - eventsList[event].push(listener); - } else { - // FireFox support non-standart four argument aWantsUntrusted - // https://github.com/devote/HTML5-History-API/issues/13 - if (arguments.length > 3) { - addEvent(event, listener, capture, arguments[3]); - } else { - addEvent(event, listener, capture); - } - } + for (var key in options) { + if (reservedKeys[key] || options[key]._isState) continue; + result[key] = options[key]; } - /** - * Wrapper for the methods 'removeEventListener/detachEvent' in the context of the 'window' - * - * @param {String} event The event type for which the user is registered - * @param {Function} listener The parameter indicates the Listener to be removed. - * @param {Boolean} capture Was registered as a capturing listener or not. - * @return void - */ - function removeEventListener(event, listener, capture) { - var list = eventsList[event]; - if (list) { - for(var i = list.length; --i;) { - if (list[i] === listener) { - list.splice(i, 1); - break; - } - } - } else { - removeEvent(event, listener, capture); - } - } + return result; + } - /** - * Wrapper for the methods 'dispatchEvent/fireEvent' in the context of the 'window' - * - * @param {Event|String} event Instance of Event or event type string if 'eventObject' used - * @param {*} [eventObject] For Internet Explorer 8 required event object on this argument - * @return {Boolean} If 'preventDefault' was called the value is false, else the value is true. - */ - function dispatchEvent(event, eventObject) { - var eventType = ('' + (typeof event === "string" ? event : event.type)).replace(/^on/, ''); - var list = eventsList[eventType]; - if (list) { - // need to understand that there is one object of Event - eventObject = typeof event === "string" ? eventObject : event; - if (eventObject.target == null) { - // need to override some of the properties of the Event object - for(var props = ['target', 'currentTarget', 'srcElement', 'type']; event = props.pop();) { - // use 'redefineProperty' to override the properties - eventObject = redefineProperty(eventObject, event, { - get: event === 'type' ? function() { - return eventType; - } : function() { - return window; - } - }); - } - } - // run function defined in the attributes 'onpopstate/onhashchange' in the 'window' context - ((eventType === 'popstate' ? window.onpopstate : window.onhashchange) - || emptyFunction).call(window, eventObject); - // run other functions that are in the list of handlers - for(var i = 0, len = list.length; i < len; i++) { - list[i].call(window, eventObject); - } - return true; - } else { - return dispatch(event, eventObject); - } + /* + * Get or Set some arbitrary data by key on this state. + * child states have access to their parents' data. + * + * This can be useful when using external models/services + * as a mean to communicate between states is not desired. + */ + function data(key, value) { + if (value !== undefined) { + if (state.ownData[key] !== undefined) + throw new Error('State ' + state.fullName + ' already has data with the key ' + key); + state.ownData[key] = value; + return state; } - /** - * dispatch current state event - */ - function firePopState() { - var o = document.createEvent ? document.createEvent('Event') : document.createEventObject(); - if (o.initEvent) { - o.initEvent('popstate', false, false); - } else { - o.type = 'popstate'; - } - o.state = historyObject.state; - // send a newly created events to be processed - dispatchEvent(o); - } + var currentState = state; - /** - * fire initial state for non-HTML5 browsers - */ - function fireInitialState() { - if (isFireInitialState) { - isFireInitialState = false; - firePopState(); - } - } + while (currentState.ownData[key] === undefined && currentState.parent) + currentState = currentState.parent; - /** - * Change the data of the current history for HTML4 browsers - * - * @param {Object} state - * @param {string} [url] - * @param {Boolean} [replace] - * @param {string} [lastURLValue] - * @return void - */ - function changeState(state, url, replace, lastURLValue) { - if (!isSupportHistoryAPI) { - // normalization url - var urlObject = parseURL(url); - // if current url not equal new url - if (urlObject._relative !== parseURL()._relative) { - // if empty lastURLValue to skip hash change event - lastURL = lastURLValue; - if (replace) { - // only replace hash, not store to history - windowLocation.replace("#" + urlObject._special); - } else { - // change hash and add new record to history - windowLocation.hash = urlObject._special; - } - } - } - if (!isSupportStateObjectInHistory && state) { - stateStorage[windowLocation.href] = state; - } - isFireInitialState = false; - } + return currentState.ownData[key]; + } - /** - * Event handler function changes the hash in the address bar - * - * @param {Event} event - * @return void - */ - function onHashChange(event) { - // if not empty lastURL, otherwise skipped the current handler event - if (lastURL) { - // if checkUrlForPopState equal current url, this means that the event was raised popstate browser - if (checkUrlForPopState !== windowLocation.href) { - // otherwise, - // the browser does not support popstate event or just does not run the event by changing the hash. - firePopState(); - } - // current event object - event = event || window.event; - - var oldURLObject = parseURL(lastURL, true); - var newURLObject = parseURL(); - // HTML4 browser not support properties oldURL/newURL - if (!event.oldURL) { - event.oldURL = oldURLObject._href; - event.newURL = newURLObject._href; - } - if (oldURLObject._hash !== newURLObject._hash) { - // if current hash not equal previous hash - dispatchEvent(event); - } - } - // new value to lastURL - lastURL = windowLocation.href; - } + function eachChildState(callback) { + for (var name in states) callback(name, states[name]); + } - /** - * The event handler is fully loaded document - * - * @param {*} [noScroll] - * @return void - */ - function onLoad(noScroll) { - // Get rid of the events popstate when the first loading a document in the webkit browsers - setTimeout(function() { - // hang up the event handler for the built-in popstate event in the browser - addEvent('popstate', function(e) { - // set the current url, that suppress the creation of the popstate event by changing the hash - checkUrlForPopState = windowLocation.href; - // for Safari browser in OS Windows not implemented 'state' object in 'History' interface - // and not implemented in old HTML4 browsers - if (!isSupportStateObjectInHistory) { - e = redefineProperty(e, 'state', {get: function() { - return historyObject.state; - }}); - } - // send events to be processed - dispatchEvent(e); - }, false); - }, 0); - // for non-HTML5 browsers - if (!isSupportHistoryAPI && noScroll !== true && historyObject.location) { - // scroll window to anchor element - scrollToAnchorId(historyObject.location.hash); - // fire initial state for non-HTML5 browser after load page - fireInitialState(); - } - } + /* + * Add a child state. + */ + function addState(name, childState) { + if (initialized) + throw new Error('States can only be added before the Router is initialized'); - /** - * Finds the closest ancestor anchor element (including the target itself). - * - * @param {HTMLElement} target The element to start scanning from. - * @return {HTMLElement} An element which is the closest ancestor anchor. - */ - function anchorTarget(target) { - while (target) { - if (target.nodeName === 'A') return target; - target = target.parentNode; - } - } + if (states[name]) + throw new Error('The state {0} already has a child state named {1}' + .replace('{0}', state.name) + .replace('{1}', name) + ); - /** - * Handles anchor elements with a hash fragment for non-HTML5 browsers - * - * @param {Event} e - */ - function onAnchorClick(e) { - var event = e || window.event; - var target = anchorTarget(event.target || event.srcElement); - var defaultPrevented = "defaultPrevented" in event ? event['defaultPrevented'] : event.returnValue === false; - if (target && target.nodeName === "A" && !defaultPrevented) { - var current = parseURL(); - var expect = parseURL(target.getAttribute("href", 2)); - var isEqualBaseURL = current._href.split('#').shift() === expect._href.split('#').shift(); - if (isEqualBaseURL && expect._hash) { - if (current._hash !== expect._hash) { - historyObject.location.hash = expect._hash; - } - scrollToAnchorId(expect._hash); - if (event.preventDefault) { - event.preventDefault(); - } else { - event.returnValue = false; - } - } - } - } + states[name] = childState; - /** - * Scroll page to current anchor in url-hash - * - * @param hash - */ - function scrollToAnchorId(hash) { - var target = document.getElementById(hash = (hash || '').replace(/^#/, '')); - if (target && target.id === hash && target.nodeName === "A") { - var rect = target.getBoundingClientRect(); - window.scrollTo((documentElement.scrollLeft || 0), rect.top + (documentElement.scrollTop || 0) - - (documentElement.clientTop || 0)); - } - } + return state; + }; - /** - * Library initialization - * - * @return {Boolean} return true if all is well, otherwise return false value - */ - function initialize() { - /** - * Get custom settings from the query string - */ - var scripts = document.getElementsByTagName('script'); - var src = (scripts[scripts.length - 1] || {}).src || ''; - var arg = src.indexOf('?') !== -1 ? src.split('?').pop() : ''; - arg.replace(/(\w+)(?:=([^&]*))?/g, function(a, key, value) { - settings[key] = (value || (key === 'basepath' ? '/' : '')).replace(/^(0|false)$/, ''); - }); - - /** - * sessionStorage throws error when cookies are disabled - * Chrome content settings when running the site in a Facebook IFrame. - * see: https://github.com/devote/HTML5-History-API/issues/34 - */ - try { - sessionStorage = window['sessionStorage']; - } catch(_e_) {} - - /** - * hang up the event handler to listen to the events hashchange - */ - addEvent(eventNamePrefix + 'hashchange', onHashChange, false); - - // a list of objects with pairs of descriptors/object - var data = [locationDescriptors, locationObject, eventsDescriptors, window, historyDescriptors, historyObject]; - - // if browser support object 'state' in interface 'History' - if (isSupportStateObjectInHistory) { - // remove state property from descriptor - delete historyDescriptors['state']; - } + function toString() { + return state.fullName; + } - // initializing descriptors - for(var i = 0; i < data.length; i += 2) { - for(var prop in data[i]) { - if (data[i].hasOwnProperty(prop)) { - if (typeof data[i][prop] === 'function') { - // If the descriptor is a simple function, simply just assign it an object - data[i + 1][prop] = data[i][prop]; - } else { - // prepare the descriptor the required format - var descriptor = prepareDescriptorsForObject(data[i], prop, data[i][prop]); - // try to set the descriptor object - if (!redefineProperty(data[i + 1], prop, descriptor, function(n, o) { - // is satisfied if the failed override property - if (o === historyObject) { - // the problem occurs in Safari on the Mac - window.history = historyObject = data[i + 1] = n; - } - })) { - // if there is no possibility override. - // This browser does not support descriptors, such as IE7 - - // remove previously hung event handlers - removeEvent(eventNamePrefix + 'hashchange', onHashChange, false); - - // fail to initialize :( - return false; - } - - // create a repository for custom handlers onpopstate/onhashchange - if (data[i + 1] === window) { - eventsList[prop] = eventsList[prop.substr(2)] = []; - } - } - } - } - } - // redirect if necessary - if (settings['redirect']) { - historyObject['redirect'](); - } + state.init = init; + state.fullPath = fullPath; - // If browser does not support object 'state' in interface 'History' - if (!isSupportStateObjectInHistory && JSON) { - storageInitialize(); - } + // Public methods - // track clicks on anchors - if (!isSupportHistoryAPI) { - document[addEventListenerName](eventNamePrefix + "click", onAnchorClick, false); - } + state.data = data; + state.addState = addState; + state.toString = toString; - if (document.readyState === 'complete') { - onLoad(true); - } else { - if (!isSupportHistoryAPI && parseURL()._relative !== settings["basepath"]) { - isFireInitialState = true; - } - /** - * Need to avoid triggering events popstate the initial page load. - * Hang handler popstate as will be fully loaded document that - * would prevent triggering event onpopstate - */ - addEvent(eventNamePrefix + 'load', onLoad, false); - } + return state; +} - // everything went well - return true; - } - /** - * Starting the library - */ - if (!initialize()) { - // if unable to initialize descriptors - // therefore quite old browser and there - // is no sense to continue to perform - return; - } +// Extract the arguments of the overloaded State "constructor" function. +function getArgs(args) { + var result = { path: '', options: {}, params: {}, queryParams: {} }, + arg1 = args[0], + arg2 = args[1], + queryIndex, + param; - /** - * If the property history.emulate will be true, - * this will be talking about what's going on - * emulation capabilities HTML5-History-API. - * Otherwise there is no emulation, ie the - * built-in browser capabilities. - * - * @type {boolean} - * @const - */ - historyObject['emulate'] = !isSupportHistoryAPI; - - /** - * Replace the original methods on the wrapper - */ - window[addEventListenerName] = addEventListener; - window[removeEventListenerName] = removeEventListener; - window[dispatchEventName] = dispatchEvent; - -})(window); -(function (factory) { - - // No AMD/CommonJS dependencies for now, just use the local (slighty modified) lib/ files. - // html5-history-api is not in npm yet. To be continued. - - if (typeof define === 'function' && define.amd) { - define([], factory); - } - else if (typeof exports === "object") { - module.exports = factory(); + if (args.length == 1) { + if (util.isString(arg1)) result.path = arg1; + else result.options = arg1; } - else { - window.Abyssa = factory(); + else if (args.length == 2) { + result.path = arg1; + result.options = (typeof arg2 == 'object') ? arg2 : {enter: arg2}; } -})(function() { - var Abyssa = {}; - var Signal = signals.Signal; - + // Extract the query string + queryIndex = result.path.indexOf('?'); + if (queryIndex != -1) { + result.queryParams = result.path.slice(queryIndex + 1); + result.path = result.path.slice(0, queryIndex); + result.queryParams = util.arrayToObject(result.queryParams.split('&')); + } + // Replace dynamic params like :id with {id}, which is what crossroads uses, + // and store them for later lookup. + result.path = result.path.replace(/:\w*/g, function(match) { + param = match.substring(1); + result.params[param] = 1; + return '{' + param + '}'; + }); -function isString(instance) { - return Object.prototype.toString.call(instance) == '[object String]'; + return result; } -function noop() {} -function arrayToObject(array) { - return array.reduce(function(obj, item) { - obj[item] = 1; - return obj; - }, {}); -} +module.exports = State; +},{"./Transition":5,"./util":8}],4:[function(require,module,exports){ -function objectToArray(obj) { - var array = []; - for (var key in obj) array.push(obj[key]); - return array; +/* +* Creates a new StateWithParams instance. +* +* StateWithParams is the merge between a State object (created and added to the router before init) +* and params (both path and query params, extracted from the URL after init) +*/ +function StateWithParams(state, params) { + return { + _state: state, + name: state && state.name, + fullName: state && state.fullName, + data: state && state.data, + params: params, + is: is, + isIn: isIn, + toString: toString + }; } -function copyObject(obj) { - var copy = {}; - for (var key in obj) copy[key] = obj[key]; - return copy; +/* +* Returns whether this state has the given fullName. +*/ +function is(fullStateName) { + return this.fullName == fullStateName; } -function mergeObjects(to, from) { - for (var key in from) to[key] = from[key]; +/* +* Returns whether this state or any of its parents has the given fullName. +*/ +function isIn(fullStateName) { + var current = this._state; + while (current) { + if (current.fullName == fullStateName) return true; + current = current.parent; + } + return false; } -function objectSize(obj) { - var size = 0; - for (var key in obj) size++; - return size; +function toString() { + return this.fullName + ':' + JSON.stringify(this.params) } -var StateWithParams = (function() { - /* - * Creates a new StateWithParams instance. - * - * StateWithParams is the merge between a State object (created and added to the router before init) - * and params (both path and query params, extracted from the URL after init) - */ - function StateWithParams(state, params) { - return { - _state: state, - name: state && state.name, - fullName: state && state.fullName, - data: state && state.data, - params: params, - is: is, - isIn: isIn, - toString: toString - }; - } - - /* - * Returns whether this state has the given fullName. - */ - function is(fullStateName) { - return this.fullName == fullStateName; - } - - /* - * Returns whether this state or any of its parents has the given fullName. - */ - function isIn(fullStateName) { - var current = this._state; - while (current) { - if (current.fullName == fullStateName) return true; - current = current.parent; - } - return false; - } +module.exports = StateWithParams; +},{}],5:[function(require,module,exports){ - function toString() { - return this.fullName + ':' + JSON.stringify(this.params) - } - - - return StateWithParams; - -})(); +var when = require('when'); /* * Create a new Transition instance. @@ -3188,903 +811,276 @@ function prereqs(enters, exits, params) { function success(value) { if (state._exitPrereqs == prereqs) state._exitPrereqs.value = value; }, - function fail(cause) { - throw new Error('Failed to resolve EXIT prereqs of ' + state.fullName); - } - ); - }); - - enters.forEach(function(state) { - if (!state.enterPrereqs) return; - - var prereqs = state._enterPrereqs = when(state.enterPrereqs(params)).then( - function success(value) { - if (state._enterPrereqs == prereqs) state._enterPrereqs.value = value; - }, - function fail(cause) { - throw new Error('Failed to resolve ENTER prereqs of ' + state.fullName); - } - ); - }); - - return when.all(enters.concat(exits).map(function(state) { - return state._enterPrereqs || state._exitPrereqs; - })); -} - -function doTransition(enters, exits, params, transition) { - exits.forEach(function(state) { - state.exit(state._exitPrereqs && state._exitPrereqs.value); - }); - - // Async promises are only allowed in 'enter' hooks. - // Make it explicit to prevent programming errors. - asyncPromises.allowed = true; - - enters.forEach(function(state) { - if (!transition.cancelled) { - transition.currentState = state; - state.enter(params, state._enterPrereqs && state._enterPrereqs.value); - } - }); - - asyncPromises.allowed = false; -} - -/* -* The top-most current state's parent that must be exited. -*/ -function transitionRoot(fromState, toState, paramOnlyChange, paramDiff) { - var root, - parent, - param; - - // For a param-only change, the root is the top-most state owning the param(s), - if (paramOnlyChange) { - fromState.parents.slice().reverse().forEach(function(parent) { - for (param in paramDiff) { - if (parent.params[param] || parent.queryParams[param]) { - root = parent; - break; - } - } - }); - } - // Else, the root is the closest common parent of the two states. - else { - for (var i = 0; i < fromState.parents.length; i++) { - parent = fromState.parents[i]; - if (toState.parents.indexOf(parent) > -1) { - root = parent; - break; - } - } - } - - return root; -} - -function withParents(state, upTo, inclusive) { - var p = state.parents, - end = Math.min(p.length, p.indexOf(upTo) + (inclusive ? 1 : 0)); - return [state].concat(p.slice(0, end)); -} - -function transitionStates(state, root, paramOnlyChange) { - var inclusive = !root || paramOnlyChange; - return withParents(state, root || state.root, inclusive); -} - - -var asyncPromises = (function () { - - var that; - var activeDeferreds = []; - - /* - * Returns a promise that will not be fullfilled if the navigation context - * changes before the wrapped promise is fullfilled. - */ - function register(promise) { - if (!that.allowed) - throw new Error('Async can only be called from within state.enter()'); - - var defer = when.defer(); - - activeDeferreds.push(defer); - - when(promise).then( - function(value) { - if (activeDeferreds.indexOf(defer) > -1) - defer.resolve(value); - }, - function(error) { - if (activeDeferreds.indexOf(defer) > -1) - defer.reject(error); - } - ); - - return defer.promise; - } - - function newTransitionStarted() { - activeDeferreds.length = 0; - } - - that = { - register: register, - newTransitionStarted: newTransitionStarted, - allowed: false - }; - - return that; - -})(); - - -Abyssa.Async = asyncPromises.register; - -/* -* Create a new State instance. -* -* State() // A state without options and an empty path. -* State('path', {options}) // A state with a static named path and options -* State(':path', {options}) // A state with a dynamic named path and options -* State('path?query', {options}) // Same as above with an optional query string param named 'query' -* State({options}) // Its path is the empty string. -* -* options is an object with the following optional properties: -* enter, exit, enterPrereqs, exitPrereqs. -* -* Child states can also be specified in the options: -* State({ myChildStateName: State() }) -* This is the declarative equivalent to the addState method. -* -* Finally, options can contain any arbitrary data value -* that will get stored in the state and made available via the data() method: -* State({myData: 55}) -* This is the declarative equivalent to the data(key, value) method. -*/ -function State() { - var state = { _isState: true }, - args = getArgs(arguments), - options = args.options, - states = getStates(args.options), - initialized; - - - state.path = args.path; - state.params = args.params; - state.queryParams = args.queryParams; - state.states = states; - - state.enter = options.enter || noop; - state.exit = options.exit || noop; - state.enterPrereqs = options.enterPrereqs; - state.exitPrereqs = options.exitPrereqs; - - state.ownData = getOwnData(options); - - /* - * Initialize and freeze this state. - */ - function init(name, parent) { - state.name = name; - state.parent = parent; - state.parents = getParents(); - state.children = getChildren(); - state.fullName = getFullName(); - state.root = state.parents[state.parents.length - 1]; - state.async = Abyssa.Async; - - eachChildState(function(name, childState) { - childState.init(name, state); - }); - - initialized = true; - } - - /* - * The full path, composed of all the individual paths of this state and its parents. - */ - function fullPath() { - var result = state.path, - stateParent = state.parent; - - while (stateParent) { - if (stateParent.path) result = stateParent.path + '/' + result; - stateParent = stateParent.parent; - } - - return result; - } - - /* - * The list of all parents, starting from the closest ones. - */ - function getParents() { - var parents = [], - parent = state.parent; - - while (parent) { - parents.push(parent); - parent = parent.parent; - } - - return parents; - } - - /* - * The list of child states as an Array. - */ - function getChildren() { - var children = []; - - for (var name in states) { - children.push(states[name]); - } - - return children; - } - - /* - * The map of initial child states by name. - */ - function getStates(options) { - var states = {}; - - for (var key in options) { - if (options[key]._isState) states[key] = options[key]; - } - - return states; - } - - /* - * The fully qualified name of this state. - * e.g granparentName.parentName.name - */ - function getFullName() { - return state.parents.reduceRight(function(acc, parent) { - return acc + parent.name + '.'; - }, '') + state.name; - } - - function getOwnData(options) { - var reservedKeys = {'enter': 1, 'exit': 1, 'enterPrereqs': 1, 'exitPrereqs': 1}, - result = {}; - - for (var key in options) { - if (reservedKeys[key] || options[key]._isState) continue; - result[key] = options[key]; - } - - return result; - } - - /* - * Get or Set some arbitrary data by key on this state. - * child states have access to their parents' data. - * - * This can be useful when using external models/services - * as a mean to communicate between states is not desired. - */ - function data(key, value) { - if (value !== undefined) { - if (state.ownData[key] !== undefined) - throw new Error('State ' + state.fullName + ' already has data with the key ' + key); - state.ownData[key] = value; - return state; - } - - var currentState = state; - - while (currentState.ownData[key] === undefined && currentState.parent) - currentState = currentState.parent; - - return currentState.ownData[key]; - } - - function eachChildState(callback) { - for (var name in states) callback(name, states[name]); - } - - /* - * Add a child state. - */ - function addState(name, childState) { - if (initialized) - throw new Error('States can only be added before the Router is initialized'); - - if (states[name]) - throw new Error('The state {0} already has a child state named {1}' - .replace('{0}', state.name) - .replace('{1}', name) - ); - - states[name] = childState; - - return state; - }; - - function toString() { - return state.fullName; - } - - - state.init = init; - state.fullPath = fullPath; - - // Public methods - - state.data = data; - state.addState = addState; - state.toString = toString; - - return state; -} - - -// Extract the arguments of the overloaded State "constructor" function. -function getArgs(args) { - var result = { path: '', options: {}, params: {}, queryParams: {} }, - arg1 = args[0], - arg2 = args[1], - queryIndex, - param; - - if (args.length == 1) { - if (isString(arg1)) result.path = arg1; - else result.options = arg1; - } - else if (args.length == 2) { - result.path = arg1; - result.options = (typeof arg2 == 'object') ? arg2 : {enter: arg2}; - } - - // Extract the query string - queryIndex = result.path.indexOf('?'); - if (queryIndex != -1) { - result.queryParams = result.path.slice(queryIndex + 1); - result.path = result.path.slice(0, queryIndex); - result.queryParams = arrayToObject(result.queryParams.split('&')); - } - - // Replace dynamic params like :id with {id}, which is what crossroads uses, - // and store them for later lookup. - result.path = result.path.replace(/:\w*/g, function(match) { - param = match.substring(1); - result.params[param] = 1; - return '{' + param + '}'; - }); - - return result; -} - - -Abyssa.State = State; - -/* -* Create a new Router instance, passing any state defined declaratively. -* More states can be added using addState() before the router is initialized. -* -* Because a router manages global state (the URL), only one instance of Router -* should be used inside an application. -*/ -function Router(declarativeStates) { - var router = {}, - states = copyObject(declarativeStates), - // Abyssa internally depends on the lower-level crossroads.js router. - roads = crossroads.create(), - firstTransition = true, - initOptions = { - enableLogs: false, - interceptAnchorClicks: true - }, - currentPathQuery, - currentState, - transition, - leafStates, - stateFound, - poppedState, - initialized; - - // Routes params should be type casted. e.g the dynamic path items/:id when id is 33 - // will end up passing the integer 33 as an argument, not the string "33". - roads.shouldTypecast = true; - // Nil transitions are prevented from our side. - roads.ignoreState = true; - - /* - * Setting a new state will start a transition from the current state to the target state. - * A successful transition will result in the URL being changed. - * A failed transition will leave the router in its current state. - */ - function setState(state, params) { - if (isSameState(state, params)) return; - - var fromState; - var toState = StateWithParams(state, params); - - if (transition) { - cancelTransition(); - fromState = StateWithParams(transition.currentState, transition.toParams); - } - else { - fromState = currentState; - } - - // While the transition is running, any code asking the router about the current state should - // get the end result state. The currentState is rollbacked if the transition fails. - currentState = toState; - - startingTransition(fromState, toState); - - transition = Transition( - fromState, - toState, - paramDiff(fromState && fromState.params, params)); - - transition.then( - function success() { - var historyState; - - transition = null; - - if (!poppedState && !firstTransition) { - historyState = ('/' + currentPathQuery).replace('//', '/'); - log('Pushing state: {0}', historyState); - history.pushState(historyState, document.title, historyState); - } - - transitionCompleted(fromState, toState); - }, - function fail(error) { - transition = null; - currentState = fromState; - - logError('Transition from {0} to {1} failed: {2}', fromState, toState, error); - router.transition.failed.dispatch(fromState, toState); - }); - } - - function cancelTransition() { - log('Cancelling existing transition from {0} to {1}', - transition.from, transition.to); - - transition.cancel(); - - router.transition.cancelled.dispatch(transition.from, transition.to); - } - - function startingTransition(fromState, toState) { - log('Starting transition from {0} to {1}', fromState, toState); - - router.transition.started.dispatch(fromState, toState); - } - - function transitionCompleted(fromState, toState) { - log('Transition from {0} to {1} completed', fromState, toState); - - router.transition.completed.dispatch(fromState, toState); - firstTransition = false; - } - - /* - * Return whether the passed state is the same as the current one; - * in which case the router can ignore the change. - */ - function isSameState(newState, newParams) { - var state, params, diff; - - if (transition) { - state = transition.to; - params = transition.toParams; - } - else if (currentState) { - state = currentState._state; - params = currentState.params; - } - - diff = paramDiff(params, newParams); - - return (newState == state) && (objectSize(diff) == 0); - } - - /* - * Return the set of all the params that changed (Either added, removed or changed). - */ - function paramDiff(oldParams, newParams) { - var diff = {}, - oldParams = oldParams || {}; - - for (var name in oldParams) - if (oldParams[name] != newParams[name]) diff[name] = 1; - - for (var name in newParams) - if (oldParams[name] != newParams[name]) diff[name] = 1; - - return diff; - } - - /* - * The state wasn't found; - * Transition to the 'notFound' state if the developer specified it or else throw an error. - */ - function notFound(state) { - log('State not found: {0}', state); - - if (states.notFound) setState(states.notFound); - else throw new Error ('State "' + state + '" could not be found'); - } - - /* - * Configure the router before its initialization. - */ - function configure(options) { - mergeObjects(initOptions, options); - return router; - } - - /* - * Initialize and freeze the router (states can not be added afterwards). - * The router will immediately initiate a transition to, in order of priority: - * 1) The state captured by the current URL - * 2) The init state passed as an argument - * 3) The default state (pathless and queryless) - */ - function init(initState) { - if (initOptions.enableLogs) - Router.enableLogs(); - - if (initOptions.interceptAnchorClicks) - interceptAnchorClicks(router); - - log('Router init'); - initStates(); - - var initialState = (!Router.ignoreInitialURL && urlPathQuery()) || initState || ''; - - log('Initializing to state {0}', initialState || '""'); - state(initialState); - - window.onpopstate = function(evt) { - // history.js will dispatch fake popstate events on HTML4 browsers' hash changes; - // in these cases, evt.state is null. - var newState = evt.state || urlPathQuery(); - - log('Popped state: {0}', newState); - poppedState = true; - setStateForPathQuery(newState); - }; - - initialized = true; - return router; - } - - function initStates() { - eachRootState(function(name, state) { - state.init(name); - }); - - leafStates = {}; - - // Only leaf states can be transitioned to. - eachLeafState(function(state) { - leafStates[state.fullName] = state; - - state.route = roads.addRoute(state.fullPath() + ":?query:"); - state.route.matched.add(function() { - stateFound = true; - setState(state, toParams(state, arguments)); - }); - }); - } - - function eachRootState(callback) { - for (var name in states) callback(name, states[name]); - } - - function eachLeafState(callback) { - var name, state; - - function callbackIfLeaf(states) { - states.forEach(function(state) { - if (state.children.length) - callbackIfLeaf(state.children); - else - callback(state); - }); - } - - callbackIfLeaf(objectToArray(states)); - } - - /* - * Request a programmatic state change. - * - * Two notations are supported: - * state('my.target.state', {id: 33, filter: 'desc'}) - * state('target/33?filter=desc') - */ - function state(pathQueryOrName, params) { - var isName = (pathQueryOrName.indexOf('.') > -1 || leafStates[pathQueryOrName]); - - log('Changing state to {0}', pathQueryOrName || '""'); - - poppedState = false; - if (isName) setStateByName(pathQueryOrName, params || {}); - else setStateForPathQuery(pathQueryOrName); - } - - /* - * An alias of 'state'. You can use 'redirect' when it makes more sense semantically. - */ - function redirect(pathQueryOrName, params) { - log('Redirecting...'); - state(pathQueryOrName, params); - } - - function setStateForPathQuery(pathQuery) { - currentPathQuery = pathQuery; - stateFound = false; - roads.parse(pathQuery); - - if (!stateFound) notFound(pathQuery); - } - - function setStateByName(name, params) { - var state = leafStates[name]; - - if (!state) return notFound(name); - - var pathQuery = state.route.interpolate(params); - setStateForPathQuery(pathQuery); - } - - /* - * Add a new root state to the router. - * The name must be unique among root states. - */ - function addState(name, state) { - if (initialized) - throw new Error('States can only be added before the Router is initialized'); - - if (states[name]) - throw new Error('A state already exist in the router with the name ' + name); - - log('Adding state {0}', name); - - states[name] = state; + function fail(cause) { + throw new Error('Failed to resolve EXIT prereqs of ' + state.fullName); + } + ); + }); - return router; - } + enters.forEach(function(state) { + if (!state.enterPrereqs) return; - function urlPathQuery() { - var hashSlash = location.href.indexOf('#/'); - return hashSlash > -1 - ? location.href.slice(hashSlash + 2) - : (location.pathname + location.search).slice(1); - } + var prereqs = state._enterPrereqs = when(state.enterPrereqs(params)).then( + function success(value) { + if (state._enterPrereqs == prereqs) state._enterPrereqs.value = value; + }, + function fail(cause) { + throw new Error('Failed to resolve ENTER prereqs of ' + state.fullName); + } + ); + }); - /* - * Translate the crossroads argument format to what we want to use. - * We want to keep the path and query names and merge them all in one object for convenience. - */ - function toParams(state, crossroadsArgs) { - var args = Array.prototype.slice.apply(crossroadsArgs), - query = args.pop(), - params = {}, - pathName; + return when.all(enters.concat(exits).map(function(state) { + return state._enterPrereqs || state._exitPrereqs; + })); +} - state.fullPath().replace(/\{\w*\}/g, function(match) { - pathName = match.slice(1, -1); - params[pathName] = args.shift(); - return ''; - }); +function doTransition(enters, exits, params, transition) { + exits.forEach(function(state) { + state.exit(state._exitPrereqs && state._exitPrereqs.value); + }); - if (query) mergeObjects(params, query); + // Async promises are only allowed in 'enter' hooks. + // Make it explicit to prevent programming errors. + asyncPromises.allowed = true; - // Decode all params - for (var i in params) { - if (isString(params[i])) params[i] = decodeURIComponent(params[i]); + enters.forEach(function(state) { + if (!transition.cancelled) { + transition.currentState = state; + state.enter(params, state._enterPrereqs && state._enterPrereqs.value); } + }); - return params; - } - - /* - * Compute a link that can be used in anchors' href attributes - * from a state name and a list of params, a.k.a reverse routing. - */ - function link(stateName, params) { - var query = {}, - allQueryParams = {}, - hasQuery = false, - state = leafStates[stateName]; + asyncPromises.allowed = false; +} - if (!state) throw new Error('Cannot find state ' + stateName); +/* +* The top-most current state's parent that must be exited. +*/ +function transitionRoot(fromState, toState, paramOnlyChange, paramDiff) { + var root, + parent, + param; - [state].concat(state.parents).forEach(function(s) { - mergeObjects(allQueryParams, s.queryParams); + // For a param-only change, the root is the top-most state owning the param(s), + if (paramOnlyChange) { + fromState.parents.slice().reverse().forEach(function(parent) { + for (param in paramDiff) { + if (parent.params[param] || parent.queryParams[param]) { + root = parent; + break; + } + } }); - - // The passed params are path and query params lumped together, - // Separate them for crossroads' to compute its interpolation. - for (var key in params) { - if (allQueryParams[key]) { - query[key] = params[key]; - delete params[key]; - hasQuery = true; + } + // Else, the root is the closest common parent of the two states. + else { + for (var i = 0; i < fromState.parents.length; i++) { + parent = fromState.parents[i]; + if (toState.parents.indexOf(parent) > -1) { + root = parent; + break; } } - - if (hasQuery) params.query = query; - - return '/' + state.route.interpolate(params).replace('/?', '?'); } - /* - * Returns a StateWithParams object representing the current state of the router. - */ - function getCurrentState() { - return currentState; - } + return root; +} +function withParents(state, upTo, inclusive) { + var p = state.parents, + end = Math.min(p.length, p.indexOf(upTo) + (inclusive ? 1 : 0)); + return [state].concat(p.slice(0, end)); +} - // Public methods +function transitionStates(state, root, paramOnlyChange) { + var inclusive = !root || paramOnlyChange; + return withParents(state, root || state.root, inclusive); +} - router.configure = configure; - router.init = init; - router.state = state; - router.redirect = redirect; - router.addState = addState; - router.link = link; - router.currentState = getCurrentState; +var asyncPromises = Transition.asyncPromises = (function () { - // Signals + var that; + var activeDeferreds = []; - router.transition = { - // Dispatched when a transition started. - started: new Signal(), - // Dispatched when a transition either completed, failed or got cancelled. - ended: new Signal(), - // Dispatched when a transition successfuly completed - completed: new Signal(), - // Dispatched when a transition failed to complete - failed: new Signal(), - // Dispatched when a transition got cancelled - cancelled: new Signal() - }; + /* + * Returns a promise that will not be fullfilled if the navigation context + * changes before the wrapped promise is fullfilled. + */ + function register(promise) { + if (!that.allowed) + throw new Error('Async can only be called from within state.enter()'); - // Dispatched once after the router successfully reached its initial state. - router.initialized = new Signal(); + var defer = when.defer(); - router.transition.completed.addOnce(function() { - router.initialized.dispatch(); - }); + activeDeferreds.push(defer); - router.transition.completed.add(transitionEnded); - router.transition.failed.add(transitionEnded); - router.transition.cancelled.add(transitionEnded); + when(promise).then( + function(value) { + if (activeDeferreds.indexOf(defer) > -1) + defer.resolve(value); + }, + function(error) { + if (activeDeferreds.indexOf(defer) > -1) + defer.reject(error); + } + ); - function transitionEnded(oldState, newState) { - router.transition.ended.dispatch(oldState, newState); + return defer.promise; } - return router; -} - + function newTransitionStarted() { + activeDeferreds.length = 0; + } -// Logging + that = { + register: register, + newTransitionStarted: newTransitionStarted, + allowed: false + }; -var log = logError = noop; + return that; -Router.enableLogs = function() { - log = function() { - console.log(getLogMessage(arguments)); - }; +})(); - logError = function() { - console.error(getLogMessage(arguments)); - }; - function getLogMessage(args) { - var message = args[0], - tokens = Array.prototype.slice.call(args, 1); +module.exports = Transition; +},{"when":1}],6:[function(require,module,exports){ - for (var i = 0, l = tokens.length; i < l; i++) - message = message.replace('{' + i + '}', tokens[i]); +var ieButton; - return message; - } -}; +function anchorClickHandler(evt) { + evt = evt || window.event; + var defaultPrevented = ('defaultPrevented' in evt) + ? evt.defaultPrevented + : (evt.returnValue === false); -Abyssa.Router = Router; + if (defaultPrevented || evt.metaKey || evt.ctrlKey || !isLeftButtonClick(evt)) return; -var interceptAnchorClicks = (function() { + var target = evt.target || evt.srcElement; + var anchor = anchorTarget(target); + if (!anchor) return; - var ieButton; + var href = anchor.getAttribute('href'); - function anchorClickHandler(evt) { - evt = evt || window.event; + if (href.charAt(0) == '#') return; + if (anchor.getAttribute('target') == '_blank') return; + if (!isLocalLink(anchor)) return; - var defaultPrevented = ('defaultPrevented' in evt) - ? evt.defaultPrevented - : (evt.returnValue === false); + if (evt.preventDefault) + evt.preventDefault(); + else + evt.returnValue = false; - if (defaultPrevented || evt.metaKey || evt.ctrlKey || !isLeftButtonClick(evt)) return; + router.state(href); +} - var target = evt.target || evt.srcElement; - var anchor = anchorTarget(target); - if (!anchor) return; +function isLeftButtonClick(evt) { + evt = evt || window.event; + var button = (evt.which !== undefined) ? evt.which : ieButton; + return button == 1; +} - var href = anchor.getAttribute('href'); +function anchorTarget(target) { + while (target) { + if (target.nodeName == 'A') return target; + target = target.parentNode; + } +} - if (href.charAt(0) == '#') return; - if (anchor.getAttribute('target') == '_blank') return; - if (!isLocalLink(anchor)) return; +// IE does not provide the correct event.button information on 'onclick' handlers +// but it does on mousedown/mouseup handlers. +function rememberIeButton(evt) { + ieButton = (evt || window.event).button; +} - if (evt.preventDefault) - evt.preventDefault(); - else - evt.returnValue = false; +function isLocalLink(anchor) { + var hostname = anchor.hostname; + var port = anchor.port; - router.state(href); + // IE10 and below can lose the hostname/port property when setting a relative href from JS + if (!hostname) { + var tempAnchor = document.createElement("a"); + tempAnchor.href = anchor.href; + hostname = tempAnchor.hostname; + port = tempAnchor.port; } - function isLeftButtonClick(evt) { - evt = evt || window.event; - var button = (evt.which !== undefined) ? evt.which : ieButton; - return button == 1; - } + var sameHostname = (hostname == location.hostname); + var samePort = (port || '80') == (location.port || '80'); - function anchorTarget(target) { - while (target) { - if (target.nodeName == 'A') return target; - target = target.parentNode; - } - } + return sameHostname && samePort; +} - // IE does not provide the correct event.button information on 'onclick' handlers - // but it does on mousedown/mouseup handlers. - function rememberIeButton(evt) { - ieButton = (evt || window.event).button; + +module.exports = function interceptAnchorClicks(router) { + if (document.addEventListener) + document.addEventListener('click', anchorClickHandler); + else { + document.attachEvent('onmousedown', rememberIeButton); + document.attachEvent('onclick', anchorClickHandler); } +}; +},{}],7:[function(require,module,exports){ - function isLocalLink(anchor) { - var hostname = anchor.hostname; - var port = anchor.port; +require('html5-history-api/history.iegte8'); - // IE10 and below can lose the hostname/port property when setting a relative href from JS - if (!hostname) { - var tempAnchor = document.createElement("a"); - tempAnchor.href = anchor.href; - hostname = tempAnchor.hostname; - port = tempAnchor.port; - } +var Abyssa = { + Router: require('./Router'), + State: require('./State'), + Async: require('./Transition').asyncPromises.register +}; - var sameHostname = (hostname == location.hostname); - var samePort = (port || '80') == (location.port || '80'); +module.exports = Abyssa; +},{"./Router":2,"./State":3,"./Transition":5,"html5-history-api/history.iegte8":1}],8:[function(require,module,exports){ - return sameHostname && samePort; - } +function isString(instance) { + return Object.prototype.toString.call(instance) == '[object String]'; +} +function noop() {} - return function (router) { - if (document.addEventListener) - document.addEventListener('click', anchorClickHandler); - else { - document.attachEvent('onmousedown', rememberIeButton); - document.attachEvent('onclick', anchorClickHandler); - } - }; +function arrayToObject(array) { + return array.reduce(function(obj, item) { + obj[item] = 1; + return obj; + }, {}); +} + +function objectToArray(obj) { + var array = []; + for (var key in obj) array.push(obj[key]); + return array; +} +function copyObject(obj) { + var copy = {}; + for (var key in obj) copy[key] = obj[key]; + return copy; +} -})(); +function mergeObjects(to, from) { + for (var key in from) to[key] = from[key]; +} -return Abyssa; +function objectSize(obj) { + var size = 0; + for (var key in obj) size++; + return size; +} -}); \ No newline at end of file +module.exports = { + isString: isString, + noop: noop, + arrayToObject: arrayToObject, + objectToArray: objectToArray, + copyObject: copyObject, + mergeObjects: mergeObjects, + objectSize: objectSize +}; +},{}]},{},[7]) +(7) +}); +; \ No newline at end of file diff --git a/target/abyssa.min.js b/target/abyssa.min.js index 6c3824a..205b0b8 100644 --- a/target/abyssa.min.js +++ b/target/abyssa.min.js @@ -1,3 +1,3 @@ -/* abyssa 1.2.4 - A stateful router library for single page applications */ +/* abyssa 1.3.0 - A stateful router library for single page applications */ -var Signal=function(a){function b(a,b,c,d,e){this._listener=b,this._isOnce=c,this.context=d,this._signal=a,this._priority=e||0}function c(a,b){if("function"!=typeof a)throw new Error("listener is a required param of {fn}() and should be a Function.".replace("{fn}",b))}function d(){this._bindings=[],this._prevParams=null}b.prototype={active:!0,params:null,execute:function(a){var b,c;return this.active&&this._listener&&(c=this.params?this.params.concat(a):a,b=this._listener.apply(this.context,c),this._isOnce&&this.detach()),b},detach:function(){return this.isBound()?this._signal.remove(this._listener,this.context):null},isBound:function(){return!!this._signal&&!!this._listener},getListener:function(){return this._listener},_destroy:function(){delete this._signal,delete this._listener,delete this.context},isOnce:function(){return this._isOnce},toString:function(){return"[SignalBinding isOnce:"+this._isOnce+", isBound:"+this.isBound()+", active:"+this.active+"]"}},d.prototype={VERSION:"0.8.1",memorize:!1,_shouldPropagate:!0,active:!0,_registerListener:function(a,c,d,e){var f,g=this._indexOfListener(a,d);if(-1!==g){if(f=this._bindings[g],f.isOnce()!==c)throw new Error("You cannot add"+(c?"":"Once")+"() then add"+(c?"Once":"")+"() the same listener without removing the relationship first.")}else f=new b(this,a,c,d,e),this._addBinding(f);return this.memorize&&this._prevParams&&f.execute(this._prevParams),f},_addBinding:function(a){var b=this._bindings.length;do--b;while(this._bindings[b]&&a._priority<=this._bindings[b]._priority);this._bindings.splice(b+1,0,a)},_indexOfListener:function(a,b){for(var c,d=this._bindings.length;d--;)if(c=this._bindings[d],c._listener===a&&c.context===b)return d;return-1},has:function(a,b){return-1!==this._indexOfListener(a,b)},add:function(a,b,d){return c(a,"add"),this._registerListener(a,!1,b,d)},addOnce:function(a,b,d){return c(a,"addOnce"),this._registerListener(a,!0,b,d)},remove:function(a,b){c(a,"remove");var d=this._indexOfListener(a,b);return-1!==d&&(this._bindings[d]._destroy(),this._bindings.splice(d,1)),a},removeAll:function(){for(var a=this._bindings.length;a--;)this._bindings[a]._destroy();this._bindings.length=0},getNumListeners:function(){return this._bindings.length},halt:function(){this._shouldPropagate=!1},dispatch:function(){if(this.active){var a,b=Array.prototype.slice.call(arguments),c=this._bindings.length;if(this.memorize&&(this._prevParams=b),c){a=this._bindings.slice(),this._shouldPropagate=!0;do c--;while(a[c]&&this._shouldPropagate&&a[c].execute(b)!==!1)}}},forget:function(){this._prevParams=null},dispose:function(){this.removeAll(),delete this._bindings,delete this._prevParams},toString:function(){return"[Signal active:"+this.active+" numListeners:"+this.getNumListeners()+"]"}};var e=d;return e.Signal=d,a.signals=e,d}(window),crossroads=function(){var a=function(a){function b(a,b){if(a.indexOf)return a.indexOf(b);for(var c=a.length;c--;)if(a[c]===b)return c;return-1}function c(a,c){var d=b(a,c);-1!==d&&a.splice(d,1)}function d(a,b){return"[object "+b+"]"===Object.prototype.toString.call(a)}function e(a){return d(a,"RegExp")}function f(a){return d(a,"Array")}function g(a){return"function"==typeof a}function h(a){var b;return b=null===a||"null"===a?null:"true"===a?!0:"false"===a?!1:a===o||"undefined"===a?o:""===a||isNaN(a)?a:parseFloat(a)}function i(a){for(var b=a.length,c=[];b--;)c[b]=h(a[b]);return c}function j(a,b){for(var c,d,e=(a||"").replace("?","").split("&"),f=e.length,g={};f--;)c=e[f].split("="),d=b?h(c[1]):c[1],g[c[0]]="string"==typeof d?decodeURIComponent(d):d;return g}function k(){this.bypassed=new a.Signal,this.routed=new a.Signal,this._routes=[],this._prevRoutes=[],this._piped=[],this.resetState()}function l(b,c,d,f){var g=e(b),h=f.patternLexer;this._router=f,this._pattern=b,this._paramsIds=g?null:h.getParamIds(b),this._optionalParamsIds=g?null:h.getOptionalParamsIds(b),this._matchRegexp=g?b:h.compilePattern(b,f.ignoreCase),this.matched=new a.Signal,this.switched=new a.Signal,c&&this.matched.add(c),this._priority=d||0}var m,n,o;return n=""===/t(.+)?/.exec("t")[1],k.prototype={greedy:!1,greedyEnabled:!0,ignoreCase:!0,ignoreState:!1,shouldTypecast:!1,normalizeFn:null,resetState:function(){this._prevRoutes.length=0,this._prevMatchedRequest=null,this._prevBypassedRequest=null},create:function(){return new k},addRoute:function(a,b,c){var d=new l(a,b,c,this);return this._sortedInsert(d),d},removeRoute:function(a){c(this._routes,a),a._destroy()},removeAllRoutes:function(){for(var a=this.getNumRoutes();a--;)this._routes[a]._destroy();this._routes.length=0},parse:function(a,b){if(a=a||"",b=b||[],this.ignoreState||a!==this._prevMatchedRequest&&a!==this._prevBypassedRequest){var c,d=this._getMatchedRoutes(a),e=0,f=d.length;if(f)for(this._prevMatchedRequest=a,this._notifyPrevRoutes(d,a),this._prevRoutes=d;f>e;)c=d[e],c.route.matched.dispatch.apply(c.route.matched,b.concat(c.params)),c.isFirst=!e,this.routed.dispatch.apply(this.routed,b.concat([a,c])),e+=1;else this._prevBypassedRequest=a,this.bypassed.dispatch.apply(this.bypassed,b.concat([a]));this._pipeParse(a,b)}},_notifyPrevRoutes:function(a,b){for(var c,d=0;c=this._prevRoutes[d++];)c.route.switched&&this._didSwitch(c.route,a)&&c.route.switched.dispatch(b)},_didSwitch:function(a,b){for(var c,d=0;c=b[d++];)if(c.route===a)return!1;return!0},_pipeParse:function(a,b){for(var c,d=0;c=this._piped[d++];)c.parse(a,b)},getNumRoutes:function(){return this._routes.length},_sortedInsert:function(a){var b=this._routes,c=b.length;do--c;while(b[c]&&a._priority<=b[c]._priority);b.splice(c+1,0,a)},_getMatchedRoutes:function(a){for(var b,c=[],d=this._routes,e=d.length;(b=d[--e])&&((!c.length||this.greedy||b.greedy)&&b.match(a)&&c.push({route:b,params:b._getParamsArray(a)}),this.greedyEnabled||!c.length););return c},pipe:function(a){this._piped.push(a)},unpipe:function(a){c(this._piped,a)},toString:function(){return"[crossroads numRoutes:"+this.getNumRoutes()+"]"}},m=new k,m.VERSION="0.12.0",m.NORM_AS_ARRAY=function(a,b){return[b.vals_]},m.NORM_AS_OBJECT=function(a,b){return[b]},l.prototype={greedy:!1,rules:void 0,match:function(a){return a=a||"",this._matchRegexp.test(a)&&this._validateParams(a)},_validateParams:function(a){var b,c=this.rules,d=this._getParamsObject(a);for(b in c)if("normalize_"!==b&&c.hasOwnProperty(b)&&!this._isValidParam(a,b,d))return!1;return!0},_isValidParam:function(a,c,d){var h=this.rules[c],i=d[c],j=!1,k=0===c.indexOf("?");return null==i&&this._optionalParamsIds&&-1!==b(this._optionalParamsIds,c)?j=!0:e(h)?(k&&(i=d[c+"_"]),j=h.test(i)):f(h)?(k&&(i=d[c+"_"]),j=this._isValidArrayRule(h,i)):g(h)&&(j=h(i,a,d)),j},_isValidArrayRule:function(a,c){if(!this._router.ignoreCase)return-1!==b(a,c);"string"==typeof c&&(c=c.toLowerCase());for(var d,e,f=a.length;f--;)if(d=a[f],e="string"==typeof d?d.toLowerCase():d,e===c)return!0;return!1},_getParamsObject:function(a){for(var c,d,e=this._router.shouldTypecast,f=this._router.patternLexer.getParamValues(a,this._matchRegexp,e),g={},i=f.length;i--;)d=f[i],this._paramsIds&&(c=this._paramsIds[i],0===c.indexOf("?")&&d&&(g[c+"_"]=d,d=j(d,e),f[i]=d),n&&""===d&&-1!==b(this._optionalParamsIds,c)&&(d=void 0,f[i]=d),g[c]=d),g[i]=d;return g.request_=e?h(a):a,g.vals_=f,g},_getParamsArray:function(a){var b,c=this.rules?this.rules.normalize_:null;return c=c||this._router.normalizeFn,b=c&&g(c)?c(a,this._getParamsObject(a)):this._getParamsObject(a).vals_},interpolate:function(a){var b=this._router.patternLexer.interpolate(this._pattern,a);if(!this._validateParams(b))throw new Error("Generated string doesn't validate against `Route.rules`.");return b},dispose:function(){this._router.removeRoute(this)},_destroy:function(){this.matched.dispose(),this.switched.dispose(),this.matched=this.switched=this._pattern=this._matchRegexp=null},toString:function(){return'[Route pattern:"'+this._pattern+'", numListeners:'+this.matched.getNumListeners()+"]"}},k.prototype.patternLexer=function(){function a(){var a,b;for(a in n)n.hasOwnProperty(a)&&(b=n[a],b.id="__CR_"+a+"__",b.save="save"in b?b.save.replace("{{id}}",b.id):b.id,b.rRestore=new RegExp(b.id,"g"))}function b(a,b){var c,d=[];for(a.lastIndex=0;c=a.exec(b);)d.push(c[1]);return d}function c(a){return b(m,a)}function d(a){return b(n.OP.rgx,a)}function e(a,b){return a=a||"",a&&(r===o?a=a.replace(k,""):r===q&&(a=a.replace(l,"")),a=f(a,"rgx","save"),a=a.replace(j,"\\$&"),a=f(a,"rRestore","res"),r===o&&(a="\\/?"+a)),r!==p&&(a+="\\/?"),new RegExp("^"+a+"$",b?"i":"")}function f(a,b,c){var d,e;for(e in n)n.hasOwnProperty(e)&&(d=n[e],a=a.replace(d[b],d[c]));return a}function g(a,b,c){var d=b.exec(a);return d&&(d.shift(),c&&(d=i(d))),d}function h(a,b){if("string"!=typeof a)throw new Error("Route pattern should be a string.");var c=function(a,c){var d;if(c="?"===c.substr(0,1)?c.substr(1):c,null!=b[c]){if("object"==typeof b[c]){var e=[];for(var f in b[c])e.push(encodeURI(f+"="+b[c][f]));d="?"+e.join("&")}else d=String(b[c]);if(-1===a.indexOf("*")&&-1!==d.indexOf("/"))throw new Error('Invalid value "'+d+'" for segment "'+a+'".')}else{if(-1!==a.indexOf("{"))throw new Error("The segment "+a+" is required.");d=""}return d};return n.OS.trail||(n.OS.trail=new RegExp("(?:"+n.OS.id+")+$")),a.replace(n.OS.rgx,n.OS.save).replace(m,c).replace(n.OS.trail,"").replace(n.OS.rRestore,"/")}var j=/[\\.+*?\^$\[\](){}\/'#]/g,k=/^\/|\/$/g,l=/\/$/g,m=/(?:\{|:)([^}:]+)(?:\}|:)/g,n={OS:{rgx:/([:}]|\w(?=\/))\/?(:|(?:\{\?))/g,save:"$1{{id}}$2",res:"\\/?"},RS:{rgx:/([:}])\/?(\{)/g,save:"$1{{id}}$2",res:"\\/"},RQ:{rgx:/\{\?([^}]+)\}/g,res:"\\?([^#]+)"},OQ:{rgx:/:\?([^:]+):/g,res:"(?:\\?([^#]*))?"},OR:{rgx:/:([^:]+)\*:/g,res:"(.*)?"},RR:{rgx:/\{([^}]+)\*\}/g,res:"(.+)"},RP:{rgx:/\{([^}]+)\}/g,res:"([^\\/?]+)"},OP:{rgx:/:([^:]+):/g,res:"([^\\/?]+)?/?"}},o=1,p=2,q=3,r=o;return a(),{strict:function(){r=p},loose:function(){r=o},legacy:function(){r=q},getParamIds:c,getOptionalParamsIds:d,getParamValues:g,compilePattern:e,interpolate:h}}(),m};return a(window.signals)}(),when=function(a){"use strict";function b(a,b,d,e){return c(a).then(b,d,e)}function c(a){return a instanceof d?a:e(a)}function d(a,b){this._message=a,this.inspect=b}function e(a){return h(function(b){b(a)})}function f(a){return b(a,l)}function g(){function a(a,f,g){b.resolve=b.resolver.resolve=function(b){return d?e(b):(d=!0,a(b),c)},b.reject=b.resolver.reject=function(a){return d?e(l(a)):(d=!0,f(a),c)},b.notify=b.resolver.notify=function(a){return g(a),a}}var b,c,d;return b={promise:V,resolve:V,reject:V,notify:V,resolver:{resolve:V,reject:V,notify:V}},b.promise=c=h(a),b}function h(a){return i(a,S.PromiseStatus&&S.PromiseStatus())}function i(a,b){function c(a,b,c,d){function e(e){e._message(a,b,c,d)}m?m.push(e):F(function(){e(k)})}function e(){return k?k.inspect():E()}function f(a){if(m){var c=m;m=V,F(function(){k=o(i,a),b&&s(k,b),j(c,k)})}}function g(a){f(l(a))}function h(a){if(m){var b=m;F(function(){j(b,n(a))})}}var i,k,m=[];i=new d(c,e),i._status=b;try{a(f,g,h)}catch(p){g(p)}return i}function j(a,b){for(var c=0;c>>0,i=Math.max(0,Math.min(c,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=H,e(l))},m=function(a){k.push(a),--i||(m=n=H,d(k))},p=0;o>p;++p)p in a&&b(a[p],h,g,f);else d(k)}return h(g).then(d,e,f)})}function v(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return u(a,1,e,c,d)}function w(a,b,c,d){return A(a,H).then(b,c,d)}function x(){return A(arguments,H)}function y(a){return A(a,C,D)}function z(a,b){return A(a,b)}function A(a,c,d){return b(a,function(a){function e(e,f,g){function h(a,h){b(a,c,d).then(function(a){i[h]=a,--k||e(i)},f,g)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return i(e)})}function B(a,c){var d=L(K,arguments,1);return b(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return b(a,function(a){return b(d,function(b){return c(a,b,f,e)})})},J.apply(a,d)})}function C(a){return{state:"fulfilled",value:a}}function D(a){return{state:"rejected",reason:a}}function E(){return{state:"pending"}}function F(a){1===N.push(a)&&M(G)}function G(){j(N),N=[]}function H(a){return a}var I;b.promise=h,b.resolve=e,b.reject=f,b.defer=g,b.join=x,b.all=w,b.map=z,b.reduce=B,b.settle=y,b.any=v,b.some=u,b.isPromise=t,b.isPromiseLike=t,d.prototype={then:function(){var a,b;return a=arguments,b=this._message,i(function(c,d,e){b("when",a,c,e)},this._status&&this._status.observed())},otherwise:function(a){return this.then(V,a)},ensure:function(a){function b(){return e(a())}return"function"==typeof a?this.then(b,b).yield(this):this},yield:function(a){return this.then(function(){return a})},tap:function(a){return this.then(a).yield(this)},spread:function(a){return this.then(function(b){return w(b,function(b){return a.apply(V,b)})})},always:function(a,b){return this.then(a,a,b)}},q.prototype.when=function(a){return"function"==typeof a?a(this.value):this.value},r.prototype.when=function(a,b){if("function"==typeof b)return b(this.reason);throw this.reason};var J,K,L,M,N,O,P,Q,R,S,T,U,V;if(T=I,N=[],O=a.setTimeout,S="undefined"!=typeof console?console:b,"object"==typeof process&&process.nextTick)M=process.nextTick;else if(U=a.MutationObserver||a.WebKitMutationObserver)M=function(a,b,c){var d=a.createElement("div");return new b(c).observe(d,{attributes:!0}),function(){d.setAttribute("x","x")}}(document,U,G);else try{M=T("vertx").runOnLoop||T("vertx").runOnContext}catch(W){M=function(a){O(a,0)}}return P=Function.prototype,Q=P.call,L=P.bind?Q.bind(Q):function(a,b){return a.apply(b,K.call(arguments,2))},R=[],K=R.slice,J=R.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},b}(this);!function(a){function b(){}function c(a,b,d){var e=/(?:([\w0-9]+:))?(?:\/\/(?:[^@]*@)?([^\/:\?#]+)(?::([0-9]+))?)?([^\?#]*)(?:(\?[^#]+)|\?)?(?:(#.*))?/;if(null==a||""===a||b)a=b?a:x.href,(!C||d)&&(a=a.replace(/^[^#]*/,"")||"#",a=x.protocol+"//"+x.host+N.basepath+a.replace(new RegExp("^#[/]?(?:"+N.type+")?"),""));else{var f=c(),g=f._pathname,h=f._protocol;a=""+a,a=/^(?:[\w0-9]+\:)?\/\//.test(a)?0===a.indexOf("/")?h+a:a:h+"//"+f._host+(0===a.indexOf("/")?a:0===a.indexOf("?")?g+a:0===a.indexOf("#")?g+f._search+a:g.replace(/[^\/]+$/g,"")+a)}P.href=a;var i=e.exec(P.href),j=i[2]+(i[3]?":"+i[3]:""),k=i[4]||"/",l=i[5]||"",m="#"===i[6]?"":i[6]||"",n=k+l+m,o=k.replace(new RegExp("^"+N.basepath,"i"),N.type)+l;return{_href:i[1]+"//"+j+n,_protocol:i[1],_host:j,_hostname:i[2],_port:i[3]||"",_pathname:k,_search:l,_hash:m,_relative:n,_nohash:o,_special:o+m}}function d(){var a="";if(u)a+=u.getItem(O);else{var b=s.cookie.split(O+"=");b.length>1&&(a+=b.pop().split(";").shift()||"null")}try{T=w.parse(a)||{}}catch(c){T={}}K(G+"unload",function(){if(u)u.setItem(O,w.stringify(T));else{var a={};(a[x.href]=z.state)&&(s.cookie=O+"="+w.stringify(a))}},!1)}function e(c,d,e,f){e=e||{set:b};var g=!e.set,h=!e.get,i={configurable:!0,set:function(){g=1},get:function(){h=1}};try{E(c,d,i),c[d]=c[d],E(c,d,e)}catch(j){}if(!g||!h)if(c.__defineGetter__&&(c.__defineGetter__(d,i.get),c.__defineSetter__(d,i.set),c[d]=c[d],e.get&&c.__defineGetter__(d,e.get),e.set&&c.__defineSetter__(d,e.set)),g&&h||c!==a){if(!g||!h)try{try{var k=v.create(c);E(v.getPrototypeOf(k)===c?k:c,d,e);for(var l in c)"function"==typeof c[l]&&(k[l]=c[l].bind(c));try{f.call(k,k,c)}catch(j){}c=k}catch(j){E(c.constructor.prototype,d,e)}}catch(j){return!1}}else{try{var m=c[d];c[d]=null}catch(j){}if("execScript"in a)a.execScript("Public "+d,"VBScript");else try{E(c,d,{value:b})}catch(j){}c[d]=m}return c}function f(a,b,c){return c=c||{},a=a===Z?x:a,c.set=c.set||function(c){a[b]=c},c.get=c.get||function(){return a[b]},c}function g(a,b,c){a in U?U[a].push(b):arguments.length>3?K(a,b,c,arguments[3]):K(a,b,c)}function h(a,b,c){var d=U[a];if(d){for(var e=d.length;--e;)if(d[e]===b){d.splice(e,1);break}}else L(a,b,c)}function i(c,d){var f=(""+("string"==typeof c?c:c.type)).replace(/^on/,""),g=U[f];if(g){if(d="string"==typeof c?d:c,null==d.target)for(var h=["target","currentTarget","srcElement","type"];c=h.pop();)d=e(d,c,{get:"type"===c?function(){return f}:function(){return a}});(("popstate"===f?a.onpopstate:a.onhashchange)||b).call(a,d);for(var i=0,j=g.length;j>i;i++)g[i].call(a,d);return!0}return M(c,d)}function j(){var a=s.createEvent?s.createEvent("Event"):s.createEventObject();a.initEvent?a.initEvent("popstate",!1,!1):a.type="popstate",a.state=z.state,i(a)}function k(){S&&(S=!1,j())}function l(a,b,d,e){if(!C){var f=c(b);f._relative!==c()._relative&&(Q=e,d?x.replace("#"+f._special):x.hash=f._special)}!D&&a&&(T[x.href]=a),S=!1}function m(b){if(Q){R!==x.href&&j(),b=b||a.event;var d=c(Q,!0),e=c();b.oldURL||(b.oldURL=d._href,b.newURL=e._href),d._hash!==e._hash&&i(b)}Q=x.href}function n(a){setTimeout(function(){K("popstate",function(a){R=x.href,D||(a=e(a,"state",{get:function(){return z.state}})),i(a)},!1)},0),!C&&a!==!0&&z.location&&(q(z.location.hash),k())}function o(a){for(;a;){if("A"===a.nodeName)return a;a=a.parentNode}}function p(b){var d=b||a.event,e=o(d.target||d.srcElement),f="defaultPrevented"in d?d.defaultPrevented:d.returnValue===!1;if(e&&"A"===e.nodeName&&!f){var g=c(),h=c(e.getAttribute("href",2)),i=g._href.split("#").shift()===h._href.split("#").shift();i&&h._hash&&(g._hash!==h._hash&&(z.location.hash=h._hash),q(h._hash),d.preventDefault?d.preventDefault():d.returnValue=!1)}}function q(b){var c=s.getElementById(b=(b||"").replace(/^#/,""));if(c&&c.id===b&&"A"===c.nodeName){var d=c.getBoundingClientRect();a.scrollTo(t.scrollLeft||0,d.top+(t.scrollTop||0)-(t.clientTop||0))}}function r(){var b=s.getElementsByTagName("script"),g=(b[b.length-1]||{}).src||"",h=-1!==g.indexOf("?")?g.split("?").pop():"";h.replace(/(\w+)(?:=([^&]*))?/g,function(a,b,c){N[b]=(c||("basepath"===b?"/":"")).replace(/^(0|false)$/,"")});try{u=a.sessionStorage}catch(i){}K(G+"hashchange",m,!1);var j=[Z,F,W,a,Y,z];D&&delete Y.state;for(var k=0;k-1){e=f;break}return e}function l(a,b,c){var d=a.parents,e=Math.min(d.length,d.indexOf(b)+(c?1:0));return[a].concat(d.slice(0,e))}function m(a,b,c){var d=!b||c;return l(a,b||a.root,d)}function n(){function a(a,b){n.name=a,n.parent=b,n.parents=d(),n.children=e(),n.fullName=g(),n.root=n.parents[n.parents.length-1],n.async=q.Async,j(function(a,b){b.init(a,n)}),m=!0}function c(){for(var a=n.path,b=n.parent;b;)b.path&&(a=b.path+"/"+a),b=b.parent;return a}function d(){for(var a=[],b=n.parent;b;)a.push(b),b=b.parent;return a}function e(){var a=[];for(var b in s)a.push(s[b]);return a}function f(a){var b={};for(var c in a)a[c]._isState&&(b[c]=a[c]);return b}function g(){return n.parents.reduceRight(function(a,b){return a+b.name+"."},"")+n.name}function h(a){var b={enter:1,exit:1,enterPrereqs:1,exitPrereqs:1},c={};for(var d in a)b[d]||a[d]._isState||(c[d]=a[d]);return c}function i(a,b){if(void 0!==b){if(void 0!==n.ownData[a])throw new Error("State "+n.fullName+" already has data with the key "+a);return n.ownData[a]=b,n}for(var c=n;void 0===c.ownData[a]&&c.parent;)c=c.parent;return c.ownData[a]}function j(a){for(var b in s)a(b,s[b])}function k(a,b){if(m)throw new Error("States can only be added before the Router is initialized");if(s[a])throw new Error("The state {0} already has a child state named {1}".replace("{0}",n.name).replace("{1}",a));return s[a]=b,n}function l(){return n.fullName}var m,n={_isState:!0},p=o(arguments),r=p.options,s=f(p.options);return n.path=p.path,n.params=p.params,n.queryParams=p.queryParams,n.states=s,n.enter=r.enter||b,n.exit=r.exit||b,n.enterPrereqs=r.enterPrereqs,n.exitPrereqs=r.exitPrereqs,n.ownData=h(r),n.init=a,n.fullPath=c,n.data=i,n.addState=k,n.toString=l,n}function o(b){var d,e,f={path:"",options:{},params:{},queryParams:{}},g=b[0],h=b[1];return 1==b.length?a(g)?f.path=g:f.options=g:2==b.length&&(f.path=g,f.options="object"==typeof h?h:{enter:h}),d=f.path.indexOf("?"),-1!=d&&(f.queryParams=f.path.slice(d+1),f.path=f.path.slice(0,d),f.queryParams=c(f.queryParams.split("&"))),f.path=f.path.replace(/:\w*/g,function(a){return e=a.substring(1),f.params[e]=1,"{"+e+"}"}),f}function p(b){function c(a,b){if(!l(a,b)){var c,d=s(a,b);K?(i(),c=s(K.currentState,K.toParams)):c=J,J=d,j(c,d),K=h(c,d,m(c&&c.params,b)),K.then(function(){var a;K=null,N||S||(a=("/"+I).replace("//","/"),u("Pushing state: {0}",a),history.pushState(a,document.title,a)),k(c,d)},function(a){K=null,J=c,logError("Transition from {0} to {1} failed: {2}",c,d,a),P.transition.failed.dispatch(c,d)})}}function i(){u("Cancelling existing transition from {0} to {1}",K.from,K.to),K.cancel(),P.transition.cancelled.dispatch(K.from,K.to)}function j(a,b){u("Starting transition from {0} to {1}",a,b),P.transition.started.dispatch(a,b)}function k(a,b){u("Transition from {0} to {1} completed",a,b),P.transition.completed.dispatch(a,b),S=!1}function l(a,b){var c,d,e;return K?(c=K.to,d=K.toParams):J&&(c=J._state,d=J.params),e=m(d,b),a==c&&0==g(e)}function m(a,b){var c={},a=a||{};for(var d in a)a[d]!=b[d]&&(c[d]=1);for(var d in b)a[d]!=b[d]&&(c[d]=1);return c}function n(a){if(u("State not found: {0}",a),!Q.notFound)throw new Error('State "'+a+'" could not be found');c(Q.notFound)}function o(a){return f(T,a),P}function q(a){T.enableLogs&&p.enableLogs(),T.interceptAnchorClicks&&v(P),u("Router init"),t();var b=!p.ignoreInitialURL&&D()||a||"";return u("Initializing to state {0}",b||'""'),y(b),window.onpopstate=function(a){var b=a.state||D();u("Popped state: {0}",b),N=!0,A(b)},O=!0,P}function t(){w(function(a,b){b.init(a)}),L={},x(function(a){L[a.fullName]=a,a.route=R.addRoute(a.fullPath()+":?query:"),a.route.matched.add(function(){M=!0,c(a,E(a,arguments))})})}function w(a){for(var b in Q)a(b,Q[b])}function x(a){function b(c){c.forEach(function(c){c.children.length?b(c.children):a(c)})}b(d(Q))}function y(a,b){var c=a.indexOf(".")>-1||L[a];u("Changing state to {0}",a||'""'),N=!1,c?B(a,b||{}):A(a)}function z(a,b){u("Redirecting..."),y(a,b)}function A(a){I=a,M=!1,R.parse(a),M||n(a)}function B(a,b){var c=L[a];if(!c)return n(a);var d=c.route.interpolate(b);A(d)}function C(a,b){if(O)throw new Error("States can only be added before the Router is initialized");if(Q[a])throw new Error("A state already exist in the router with the name "+a);return u("Adding state {0}",a),Q[a]=b,P}function D(){var a=location.href.indexOf("#/");return a>-1?location.href.slice(a+2):(location.pathname+location.search).slice(1)}function E(b,c){var d,e=Array.prototype.slice.apply(c),g=e.pop(),h={};b.fullPath().replace(/\{\w*\}/g,function(a){return d=a.slice(1,-1),h[d]=e.shift(),""}),g&&f(h,g);for(var i in h)a(h[i])&&(h[i]=decodeURIComponent(h[i]));return h}function F(a,b){var c={},d={},e=!1,g=L[a];if(!g)throw new Error("Cannot find state "+a);[g].concat(g.parents).forEach(function(a){f(d,a.queryParams)});for(var h in b)d[h]&&(c[h]=b[h],delete b[h],e=!0);return e&&(b.query=c),"/"+g.route.interpolate(b).replace("/?","?")}function G(){return J}function H(a,b){P.transition.ended.dispatch(a,b)}var I,J,K,L,M,N,O,P={},Q=e(b),R=crossroads.create(),S=!0,T={enableLogs:!1,interceptAnchorClicks:!0};return R.shouldTypecast=!0,R.ignoreState=!0,P.configure=o,P.init=q,P.state=y,P.redirect=z,P.addState=C,P.link=F,P.currentState=G,P.transition={started:new r,ended:new r,completed:new r,failed:new r,cancelled:new r},P.initialized=new r,P.transition.completed.addOnce(function(){P.initialized.dispatch()}),P.transition.completed.add(H),P.transition.failed.add(H),P.transition.cancelled.add(H),P}var q={},r=signals.Signal,s=function(){function a(a,e){return{_state:a,name:a&&a.name,fullName:a&&a.fullName,data:a&&a.data,params:e,is:b,isIn:c,toString:d}}function b(a){return this.fullName==a}function c(a){for(var b=this._state;b;){if(b.fullName==a)return!0;b=b.parent}return!1}function d(){return this.fullName+":"+JSON.stringify(this.params)}return a}(),t=function(){function a(a){if(!c.allowed)throw new Error("Async can only be called from within state.enter()");var b=when.defer();return d.push(b),when(a).then(function(a){d.indexOf(b)>-1&&b.resolve(a)},function(a){d.indexOf(b)>-1&&b.reject(a)}),b.promise}function b(){d.length=0}var c,d=[];return c={register:a,newTransitionStarted:b,allowed:!1}}();q.Async=t.register,q.State=n;var u=logError=b;p.enableLogs=function(){function a(a){for(var b=a[0],c=Array.prototype.slice.call(a,1),d=0,e=c.length;e>d;d++)b=b.replace("{"+d+"}",c[d]);return b}u=function(){console.log(a(arguments))},logError=function(){console.error(a(arguments))}},q.Router=p;var v=function(){function a(a){a=a||window.event;var d="defaultPrevented"in a?a.defaultPrevented:a.returnValue===!1;if(!(d||a.metaKey||a.ctrlKey)&&b(a)){var f=a.target||a.srcElement,g=c(f);if(g){var h=g.getAttribute("href");"#"!=h.charAt(0)&&"_blank"!=g.getAttribute("target")&&e(g)&&(a.preventDefault?a.preventDefault():a.returnValue=!1,router.state(h))}}}function b(a){a=a||window.event;var b=void 0!==a.which?a.which:f;return 1==b}function c(a){for(;a;){if("A"==a.nodeName)return a;a=a.parentNode}}function d(a){f=(a||window.event).button}function e(a){var b=a.hostname,c=a.port;if(!b){var d=document.createElement("a");d.href=a.href,b=d.hostname,c=d.port}var e=b==location.hostname,f=(c||"80")==(location.port||"80");return e&&f}var f;return function(){document.addEventListener?document.addEventListener("click",a):(document.attachEvent("onmousedown",d),document.attachEvent("onclick",a))}}();return q}); \ No newline at end of file +!function(a){"object"==typeof exports?module.exports=a():"function"==typeof define&&define.amd?define(a):"undefined"!=typeof window?window.Abyssa=a():"undefined"!=typeof global?global.Abyssa=a():"undefined"!=typeof self&&(self.Abyssa=a())}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g-1||I[a];j("Changing state to {0}",a||'""'),K=!1,c?y(a,b||{}):x(a)}function w(a,b){j("Redirecting..."),v(a,b)}function x(a){F=a,J=!1,O.parse(a),J||p(a)}function y(a,b){var c=I[a];if(!c)return p(a);var d=c.route.interpolate(b);x(d)}function z(a,b){if(L)throw new Error("States can only be added before the Router is initialized");if(N[a])throw new Error("A state already exist in the router with the name "+a);return j("Adding state {0}",a),N[a]=b,M}function A(){var a=location.href.indexOf("#/");return a>-1?location.href.slice(a+2):(location.pathname+location.search).slice(1)}function B(a,b){var c,d=Array.prototype.slice.apply(b),e=d.pop(),f={};a.fullPath().replace(/\{\w*\}/g,function(a){return c=a.slice(1,-1),f[c]=d.shift(),""}),e&&i.mergeObjects(f,e);for(var g in f)i.isString(f[g])&&(f[g]=decodeURIComponent(f[g]));return f}function C(a,b){var c={},d={},e=!1,f=I[a];if(!f)throw new Error("Cannot find state "+a);[f].concat(f.parents).forEach(function(a){i.mergeObjects(d,a.queryParams)});for(var g in b)d[g]&&(c[g]=b[g],delete b[g],e=!0);return e&&(b.query=c),"/"+f.route.interpolate(b).replace("/?","?")}function D(){return G}function E(a,b){M.transition.ended.dispatch(a,b)}var F,G,H,I,J,K,L,M={},N=i.copyObject(a),O=e.create(),P=!0,Q={enableLogs:!1,interceptAnchorClicks:!0};return O.shouldTypecast=!0,O.ignoreState=!0,M.configure=q,M.init=r,M.state=v,M.redirect=w,M.addState=z,M.link=C,M.currentState=D,M.transition={started:new d,ended:new d,completed:new d,failed:new d,cancelled:new d},M.initialized=new d,M.transition.completed.addOnce(function(){M.initialized.dispatch()}),M.transition.completed.add(E),M.transition.failed.add(E),M.transition.cancelled.add(E),M}var d=a("signals").Signal,e=a("crossroads"),f=a("./anchorClicks"),g=a("./StateWithParams"),h=a("./Transition"),i=a("./util"),j=logError=i.noop;c.enableLogs=function(){function a(a){for(var b=a[0],c=Array.prototype.slice.call(a,1),d=0,e=c.length;e>d;d++)b=b.replace("{"+d+"}",c[d]);return b}j=function(){console.log(a(arguments))},logError=function(){console.error(a(arguments))}},b.exports=c},{"./StateWithParams":4,"./Transition":5,"./anchorClicks":6,"./util":8,crossroads:1,signals:1}],3:[function(a,b){function c(){function a(a,b){p.name=a,p.parent=b,p.parents=c(),p.children=g(),p.fullName=i(),p.root=p.parents[p.parents.length-1],p.async=f,l(function(a,b){b.init(a,p)}),o=!0}function b(){for(var a=p.path,b=p.parent;b;)b.path&&(a=b.path+"/"+a),b=b.parent;return a}function c(){for(var a=[],b=p.parent;b;)a.push(b),b=b.parent;return a}function g(){var a=[];for(var b in s)a.push(s[b]);return a}function h(a){var b={};for(var c in a)a[c]._isState&&(b[c]=a[c]);return b}function i(){return p.parents.reduceRight(function(a,b){return a+b.name+"."},"")+p.name}function j(a){var b={enter:1,exit:1,enterPrereqs:1,exitPrereqs:1},c={};for(var d in a)b[d]||a[d]._isState||(c[d]=a[d]);return c}function k(a,b){if(void 0!==b){if(void 0!==p.ownData[a])throw new Error("State "+p.fullName+" already has data with the key "+a);return p.ownData[a]=b,p}for(var c=p;void 0===c.ownData[a]&&c.parent;)c=c.parent;return c.ownData[a]}function l(a){for(var b in s)a(b,s[b])}function m(a,b){if(o)throw new Error("States can only be added before the Router is initialized");if(s[a])throw new Error("The state {0} already has a child state named {1}".replace("{0}",p.name).replace("{1}",a));return s[a]=b,p}function n(){return p.fullName}var o,p={_isState:!0},q=d(arguments),r=q.options,s=h(q.options);return p.path=q.path,p.params=q.params,p.queryParams=q.queryParams,p.states=s,p.enter=r.enter||e.noop,p.exit=r.exit||e.noop,p.enterPrereqs=r.enterPrereqs,p.exitPrereqs=r.exitPrereqs,p.ownData=j(r),p.init=a,p.fullPath=b,p.data=k,p.addState=m,p.toString=n,p}function d(a){var b,c,d={path:"",options:{},params:{},queryParams:{}},f=a[0],g=a[1];return 1==a.length?e.isString(f)?d.path=f:d.options=f:2==a.length&&(d.path=f,d.options="object"==typeof g?g:{enter:g}),b=d.path.indexOf("?"),-1!=b&&(d.queryParams=d.path.slice(b+1),d.path=d.path.slice(0,b),d.queryParams=e.arrayToObject(d.queryParams.split("&"))),d.path=d.path.replace(/:\w*/g,function(a){return c=a.substring(1),d.params[c]=1,"{"+c+"}"}),d}var e=a("./util"),f=a("./Transition").asyncPromises.register;b.exports=c},{"./Transition":5,"./util":8}],4:[function(a,b){function c(a,b){return{_state:a,name:a&&a.name,fullName:a&&a.fullName,data:a&&a.data,params:b,is:d,isIn:e,toString:f}}function d(a){return this.fullName==a}function e(a){for(var b=this._state;b;){if(b.fullName==a)return!0;b=b.parent}return!1}function f(){return this.fullName+":"+JSON.stringify(this.params)}b.exports=c},{}],5:[function(a,b){function c(a,b,c){function g(a,b){return n.then(function(){l||a()},function(a){l||b(a)})}function i(){l=t.cancelled=!0}var k,l,m,n,o=[],p=a&&a._state,q=b._state,r=b.params,s=p==q,t={from:p,to:q,toParams:r,then:g,cancel:i,cancelled:l,currentState:p};return p&&(k=f(p,q,s,c),o=h(p,k,s)),m=h(q,k,s).reverse(),n=d(m,o,r).then(function(){l||e(m,o,r,t)}),j.newTransitionStarted(),t}function d(a,b,c){return b.forEach(function(a){if(a.exitPrereqs)var b=a._exitPrereqs=i(a.exitPrereqs()).then(function(c){a._exitPrereqs==b&&(a._exitPrereqs.value=c)},function(){throw new Error("Failed to resolve EXIT prereqs of "+a.fullName)})}),a.forEach(function(a){if(a.enterPrereqs)var b=a._enterPrereqs=i(a.enterPrereqs(c)).then(function(c){a._enterPrereqs==b&&(a._enterPrereqs.value=c)},function(){throw new Error("Failed to resolve ENTER prereqs of "+a.fullName)})}),i.all(a.concat(b).map(function(a){return a._enterPrereqs||a._exitPrereqs}))}function e(a,b,c,d){b.forEach(function(a){a.exit(a._exitPrereqs&&a._exitPrereqs.value)}),j.allowed=!0,a.forEach(function(a){d.cancelled||(d.currentState=a,a.enter(c,a._enterPrereqs&&a._enterPrereqs.value))}),j.allowed=!1}function f(a,b,c,d){var e,f,g;if(c)a.parents.slice().reverse().forEach(function(a){for(g in d)if(a.params[g]||a.queryParams[g]){e=a;break}});else for(var h=0;h-1){e=f;break}return e}function g(a,b,c){var d=a.parents,e=Math.min(d.length,d.indexOf(b)+(c?1:0));return[a].concat(d.slice(0,e))}function h(a,b,c){var d=!b||c;return g(a,b||a.root,d)}var i=a("when"),j=c.asyncPromises=function(){function a(a){if(!c.allowed)throw new Error("Async can only be called from within state.enter()");var b=i.defer();return d.push(b),i(a).then(function(a){d.indexOf(b)>-1&&b.resolve(a)},function(a){d.indexOf(b)>-1&&b.reject(a)}),b.promise}function b(){d.length=0}var c,d=[];return c={register:a,newTransitionStarted:b,allowed:!1}}();b.exports=c},{when:1}],6:[function(a,b){function c(a){a=a||window.event;var b="defaultPrevented"in a?a.defaultPrevented:a.returnValue===!1;if(!(b||a.metaKey||a.ctrlKey)&&d(a)){var c=a.target||a.srcElement,f=e(c);if(f){var h=f.getAttribute("href");"#"!=h.charAt(0)&&"_blank"!=f.getAttribute("target")&&g(f)&&(a.preventDefault?a.preventDefault():a.returnValue=!1,router.state(h))}}}function d(a){a=a||window.event;var b=void 0!==a.which?a.which:h;return 1==b}function e(a){for(;a;){if("A"==a.nodeName)return a;a=a.parentNode}}function f(a){h=(a||window.event).button}function g(a){var b=a.hostname,c=a.port;if(!b){var d=document.createElement("a");d.href=a.href,b=d.hostname,c=d.port}var e=b==location.hostname,f=(c||"80")==(location.port||"80");return e&&f}var h;b.exports=function(){document.addEventListener?document.addEventListener("click",c):(document.attachEvent("onmousedown",f),document.attachEvent("onclick",c))}},{}],7:[function(a,b){a("html5-history-api/history.iegte8");var c={Router:a("./Router"),State:a("./State"),Async:a("./Transition").asyncPromises.register};b.exports=c},{"./Router":2,"./State":3,"./Transition":5,"html5-history-api/history.iegte8":1}],8:[function(a,b){function c(a){return"[object String]"==Object.prototype.toString.call(a)}function d(){}function e(a){return a.reduce(function(a,b){return a[b]=1,a},{})}function f(a){var b=[];for(var c in a)b.push(a[c]);return b}function g(a){var b={};for(var c in a)b[c]=a[c];return b}function h(a,b){for(var c in b)a[c]=b[c]}function i(a){var b=0;for(var c in a)b++;return b}b.exports={isString:c,noop:d,arrayToObject:e,objectToArray:f,copyObject:g,mergeObjects:h,objectSize:i}},{}]},{},[7])(7)}); \ No newline at end of file diff --git a/test/testRunner.html b/test/testRunner.html index a2441eb..1f71596 100644 --- a/test/testRunner.html +++ b/test/testRunner.html @@ -13,7 +13,7 @@ - +