From 71c61cc71df49d5e597b139d2dd4777d6a27dfbb Mon Sep 17 00:00:00 2001 From: Gabriel Reitz Giannattasio Date: Mon, 25 Apr 2016 13:12:57 -0700 Subject: [PATCH 01/11] Separate PushStateTree dependencies into plugins --- .eslintrc | 5 +- src/constants.js | 7 + src/helpers.js | 86 ++++++ src/main.js | 17 ++ src/plugin/history.js | 265 +++++++++++++++++ src/push-state-tree.js | 443 +++------------------------- src/route.js | 9 + test/helper/cleanHistoryAPI.js | 25 +- test/isInt.js | 38 +-- test/route-basePath.js | 2 +- test/route-beutifyLocation.js | 2 +- test/route-createRules.js | 2 +- test/route-force-hash-navigation.js | 6 +- test/route-hash-navigation.js | 2 +- test/route-methods.js | 2 +- test/route.js | 2 +- test/rule-events.js | 2 +- test/rule-properties.js | 2 +- test/rule.js | 2 +- test/version.js | 4 +- webpack.config.js | 6 +- 21 files changed, 482 insertions(+), 447 deletions(-) create mode 100644 src/constants.js create mode 100644 src/helpers.js create mode 100644 src/main.js create mode 100644 src/plugin/history.js create mode 100644 src/route.js diff --git a/.eslintrc b/.eslintrc index c477d07..8f226ba 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,7 +16,10 @@ "strict": "off", "quotes": [ "error", - "single" + "single", + { + "allowTemplateLiterals": true + } ], "no-unused-vars": [ "error", diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..2e4cdba --- /dev/null +++ b/src/constants.js @@ -0,0 +1,7 @@ +// Constants +export const LEAVE = 'leave'; +export const UPDATE = 'update'; +export const ENTER = 'enter'; +export const CHANGE = 'change'; +export const MATCH = 'match'; +export const OLD_MATCH = 'oldMatch'; \ No newline at end of file diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..375fdac --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,86 @@ +export function isExternal(url) { + // Check if a URL is external + return (/^[a-z0-9]+:\/\//i).test(url); +} + +export function isRelative(uri) { + // Check if a URI is relative path, when begin with # or / isn't relative uri + return (/^[^#/]/).test(uri); +} + +export function convertToURI(url) { + // Remove unwanted data from url + // if it's a browser it will remove the location.origin + // else it will ignore first occurrence of / and return the rest + if (location && url == location.href) { + let host = location.host && `//${location.host}`; + return url.substr(`${location.protocol}${host}`.length); + } else { + let match = url.match(/([^\/]*)(\/+)?(.*)/); + return match[2] ? match[3] : match[1]; + } +} + +export function resolveRelativePath(path) { + // Resolve relative paths manually for browsers using hash navigation + + var parts = path.split('/'); + var i = 1; + while (i < parts.length) { + // if current part is `..` and previous part is different, remove both of them + if (parts[i] === '..' && i > 0 && parts[i-1] !== '..') { + parts.splice(i - 1, 2); + i -= 2; + } + i++; + } + return parts + .join('/') + .replace(/\/\.\/|\.\/|\.\.\//g, '/') + .replace(/^\/$/, ''); +} + +export function objectMixinProperties(destineObject, sourceObject) { + // Simple version of Object.assign + for (let property in sourceObject) { + if (sourceObject.hasOwnProperty(property)) { + destineObject[property] = sourceObject[property]; + } + } +} + +export function isInt(n) { + return typeof n != 'undefined' && !isNaN(parseFloat(n)) && n % 1 === 0 && isFinite(n); +} + +export function proxyLikePrototype(context, prototypeContext) { + // It proxy the method, or property to the prototype + + for (let property in prototypeContext) { + if (typeof prototypeContext[property] == 'function') { + + // function wrapper, it doesn't use binding because it needs to execute the current version of the property in the + // prototype to conserve the prototype chain resource + context[property] = function proxyMethodToPrototype() { + return prototypeContext[property].apply(this, arguments); + }; + continue; + } + // Proxy prototype properties to the instance, but if they're redefined in the instance, use the instance definition + // without change the prototype property value + if (typeof context[property] == 'undefined') { + let propertyValue; + Object.defineProperty(context, property, { + get() { + if (typeof propertyValue == 'undefined') { + return prototypeContext[property]; + } + return propertyValue; + }, + set(value) { + propertyValue = value; + } + }); + } + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..e28b5b1 --- /dev/null +++ b/src/main.js @@ -0,0 +1,17 @@ +// If you don't want support IE 7 and IE 8 you can remove the compatibility shim with `PST_NO_OLD_ID: false` +// new webpack.DefinePlugin({ +// PST_NO_OLD_IE: false +// }) +// https://webpack.github.io/docs/list-of-plugins.html#defineplugin +require('./es3.shim.js'); + +// TODO: Use a PushStateTree.prototype.createEvent instead of shim native CustomEvents +require('./customEvent.shim'); + +import PushStateTree from './push-state-tree'; + +import BrowserHistory from './plugin/history'; + +PushStateTree.plugins.push(new BrowserHistory()) + +module.exports = PushStateTree; \ No newline at end of file diff --git a/src/plugin/history.js b/src/plugin/history.js new file mode 100644 index 0000000..c0b58c8 --- /dev/null +++ b/src/plugin/history.js @@ -0,0 +1,265 @@ +import { MATCH } from '../constants'; +import { isExternal, isRelative, convertToURI, resolveRelativePath } from './../helpers'; +import { isIE } from '../ieOld.shim'; + +const HASH_CHANGE = 'hashchange'; +const POP_STATE = 'popstate'; + +const root = typeof window == 'object' ? window : global; + +function BrowserHistory(options) { + if (!(this instanceof BrowserHistory)) { + return new BrowserHistory(options); + } + options = options || {}; + + let location = this.location = options.location || root.location; + let history = this.history = options.history || root.history; + + if (!location) { + throw new Error(DEV_ENV && 'BrowserHistory require Location API'); + } + + /*eslint guard-for-in: "off"*/ + for (var method in history) { + if (typeof history[method] === 'function') { + preProcessUriBeforeExecuteNativeHistoryMethods.call(this, history, location, method); + } + } + + this.hasPushState = !!(history && history.pushState); +} + +BrowserHistory.prototype.create = function () { + let globalListeners = () => {}; + let enableListeners = () => { + globalListeners = this.globalListeners(); + }; + this.addEventListener('disabled', globalListeners); + this.addEventListener('enabled', enableListeners); +}; + + +BrowserHistory.prototype.globalListeners = function() { + // Called when creating a new instance of the parent of the current prototype + + // Start the browser global listeners and return a method to stop listening to them + + let beautifyLocation = () => { + // apply pushState for a beautiful URL when beautifyLocation is enable and it's possible to do it + if (this.beautifyLocation + && this.usePushState + && this.isPathValid + && this.path.indexOf('#') != -1 + ) { + + // Execute after to pop_state again + this.replace(this.uri); + return true; + } + }; + + let dispatchListener = event => { + this.path = convertToURI(location.href); + + if (beautifyLocation()) { + event.preventDefault(); + } + }; + this.addEventListener('dispatch', dispatchListener); + + let browserListener = () => { + if (this.path != convertToURI(location.href)) { + this.dispatch(); + } + }; + + root.addEventListener(POP_STATE, browserListener); + root.addEventListener(HASH_CHANGE, browserListener); + + let ieWatch; + let loadListener = () => { + browserListener(); + + if (!isIE() || isIE() > 8) return; + + // Watch for URL changes in the IE + ieWatch = setInterval(browserListener, 50); + }; + + // If the DOM is ready when running the PST, execute loadListeners and ignore others + if (document.readyState == 'complete') { + loadListener(); + } else { + // Modern browsers + document.addEventListener('DOMContentLoaded', browserListener); + // Some IE browsers + root.addEventListener('readystatechange', browserListener); + // Almost all browsers + root.addEventListener('load', loadListener); + } + + return () => { + // Method to stop watching + this.removeEventListener('dispatch', dispatchListener); + root.removeEventListener(POP_STATE, browserListener); + document.removeEventListener('DOMContentLoaded', browserListener); + root.removeEventListener('readystatechange', browserListener); + root.removeEventListener(HASH_CHANGE, browserListener); + root.removeEventListener('load', loadListener); + if (ieWatch) clearInterval(ieWatch); + }; +}; + +function preProcessUriBeforeExecuteNativeHistoryMethods(history, location, method) { + + // If not pushState or replaceState methods, execute it from history API + if (method !== 'pushState' && method !== 'replaceState') { + this[method] = function () { + history[method].apply(history, arguments); + return this; + }; + return; + } + + this[method] = function () { + // Wrap method + + // remove the method from arguments + let args = Array.prototype.slice.call(arguments); + let uri = args[0] || ''; + if (typeof args[2] === 'string') { + uri = args[2]; + } + + // if has a basePath translate the not relative paths to use the basePath + if (!isExternal(uri)) { + // When not external link, need to normalize the URI + + if (isRelative(uri)) { + // Relative to the uri + var basePath = this.uri[MATCH](/^([^?#]*)\//); + basePath = basePath ? basePath[1] + '/' : ''; + uri = basePath + uri; + } else { + // This isn't relative, will cleanup / and # from the begin and use the remain path + uri = uri[MATCH](/^([#/]*)?(.*)/)[2]; + } + + if (!this.usePushState) { + + // Ignore basePath when using location.hash and resolve relative path and keep + // the current location.pathname, some browsers history API might apply the new pathname + // with the hash content if not explicit + uri = location.pathname + '#' + resolveRelativePath(uri); + } else { + + // Add the basePath to your uri, not allowing to go by pushState outside the basePath + uri = this.basePath + uri; + } + } + + // Ignore state and make the url be the current uri + args[0] = null; + args[2] = uri; + + this.path = this.basePath + uri; + history[method].apply(history, args); + return this; + }; +} + +BrowserHistory.prototype.pushState = function (uri, ignored, deprecatedUri) { + // Does a shim for pushState when history API doesn't support pushState, + // from version 0.15.x it ignores state and title definition since they are + // never used in any production project so far and seems to make harder to + // developers to use the method since they need to add 2 useless arguments + // before the really necessary one. + // However it keeps compatible with any implementation that already add the + // url as third argument. + if (typeof deprecatedUri == 'string') { + uri = deprecatedUri; + } + if (typeof uri != 'string') { + uri = ''; + } + + // Replace hash url + if (isExternal(uri)) { + // this will redirect the browser, so doesn't matters the rest... + this.location.href = uri; + } + + // Remove the has if is it present + if (uri[0] === '#') { + uri = uri.slice(1); + } + + if (isRelative(uri)) { + uri = this.location.hash.slice(1, this.location.hash.lastIndexOf('/') + 1) + uri; + uri = resolveRelativePath(uri); + } + + // Include the basePath in the uri, if is already in the current router + // basePath, it will apply the assign without refresh, but if the basePath + // is different, it will refresh the browser to work in the current router + // basePath. + // The behavior is the same in browsers that support pushState, however they + // don't refresh when switching between basePath of different routers + this.path = this.basePath + uri; + if (this.location.pathname == this.basePath) { + this.location.hash = uri; + } else { + this.location.assign(uri); + } + + return this; +}; + +BrowserHistory.prototype.replaceState = function (uri, ignored, deprecatedUri) { + // Does a shim for replaceState when history API doesn't support pushState, + // from version 0.15.x it ignores state and title definition since they are + // never used in any production project so far and seems to make harder to + // developers to use the method since they need to add 2 useless arguments + // before the really necessary one. + // However it keeps compatible with any implementation that already add the + // url as third argument. + + if (typeof deprecatedUri == 'string') { + uri = deprecatedUri; + } + if (typeof uri != 'string') { + uri = ''; + } + + // Replace the url + if (isExternal(uri)) { + throw new Error('Invalid url replace.'); + } + + if (uri[0] === '#') { + uri = uri.slice(1); + } + + if (isRelative(uri)) { + var relativePos = this.location.hash.lastIndexOf('/') + 1; + uri = this.location.hash.slice(1, relativePos) + uri; + uri = resolveRelativePath(uri); + } + + // Always use hash navigation + uri = '#' + uri; + + // Include the basePath in the uri, if is already in the current router + // basePath, it will apply the replace without refresh, but if the basePath + // is different, it will refresh the browser to work in the current router + // basePath. + // The behavior is the same in browsers that support pushState, however they + // don't refresh when switching between basePath of different routers + this.path = this.basePath + uri; + this.location.replace(uri); + + return this; +}; + +export default BrowserHistory; \ No newline at end of file diff --git a/src/push-state-tree.js b/src/push-state-tree.js index 203b13a..2dbc5e3 100644 --- a/src/push-state-tree.js +++ b/src/push-state-tree.js @@ -1,143 +1,8 @@ const root = typeof window !== 'undefined' && window || global; -let errorApiMessage = api => new Error(`PushStateTree ${VERSION} requires ${api} API to run.`); +import { objectMixinProperties, proxyLikePrototype, isInt } from './helpers'; -if (!root.document) { - throw errorApiMessage('document'); -} -const document = root.document; -const location = root.location; -const history = root.history; - -// If you have your own shim for ES3 and old IE browsers, you can remove all shim files from your package by adding a -// webpack.DefinePlugin that translates `typeof PST_NO_SHIM === 'undefined'` to false, this will remove the section -// in the minified version: -// new webpack.DefinePlugin({ -// PST_NO_SHIM: false -// }) -// https://webpack.github.io/docs/list-of-plugins.html#defineplugin - -let isIE = require('./ieOld.shim').isIE; - -// If you don't want support IE 7 and IE 8 you can remove the compatibility shim with `PST_NO_OLD_ID: false` -// new webpack.DefinePlugin({ -// PST_NO_OLD_IE: false -// }) -// https://webpack.github.io/docs/list-of-plugins.html#defineplugin -require('./es3.shim.js'); - -// TODO: Use a PushStateTree.prototype.createEvent instead of shim native CustomEvents -require('./customEvent.shim'); - -// Constants for uglifiers -const HASH_CHANGE = 'hashchange'; -const POP_STATE = 'popstate'; -const LEAVE = 'leave'; -const UPDATE = 'update'; -const ENTER = 'enter'; -const CHANGE = 'change'; -const MATCH = 'match'; -const OLD_MATCH = 'oldMatch'; - -function convertToURI(url) { - // Remove unwanted data from url - // if it's a browser it will remove the location.origin - // else it will ignore first occurrence of / and return the rest - if (location && url == location.href) { - let host = location.host && `//${location.host}`; - return url.substr(`${location.protocol}${host}`.length); - } else { - let match = url[MATCH](/([^\/]*)(\/+)?(.*)/); - return match[2] ? match[3] : match[1]; - } -} - -// Helpers -function isInt(n) { - return typeof n != 'undefined' && !isNaN(parseFloat(n)) && n % 1 === 0 && isFinite(n); -} - -function proxyReadOnlyProperty(context, property, targetObject) { - // Proxy the property with same name from the targetObject into the defined context - // if `targetObject` is `false` it will always return `false`. - - Object.defineProperty(context, property, { - get() { - return targetObject && targetObject[property]; - }, - // Must have set to ignore when try to set a new value and not throw error. - set() {} - }); -} - -function proxyLikePrototype(context, prototypeContext) { - // It proxy the method, or property to the prototype - - for (let property in prototypeContext) { - if (typeof prototypeContext[property] === 'function') { - // function wrapper, it doesn't use binding because it needs to execute the current version of the property in the - // prototype to conserve the prototype chain resource - context[property] = function proxyMethodToPrototype() { - return prototypeContext[property].apply(this, arguments); - }; - continue; - } - // Proxy prototype properties to the instance, but if they're redefined in the instance, use the instance definition - // without change the prototype property value - if (typeof context[property] == 'undefined') { - let propertyValue; - Object.defineProperty(context, property, { - get() { - if (typeof propertyValue == 'undefined') { - return prototypeContext[property]; - } - return propertyValue; - }, - set(value) { - propertyValue = value; - } - }); - } - } -} - -function objectMixinProperties(destineObject, sourceObject) { - // Simple version of Object.assign - for (let property in sourceObject) { - if (sourceObject.hasOwnProperty(property)) { - destineObject[property] = sourceObject[property]; - } - } -} - -function isExternal(url) { - // Check if a URL is external - return (/^[a-z0-9]+:\/\//i).test(url); -} - -function isRelative(uri) { - // Check if a URI is relative path, when begin with # or / isn't relative uri - return (/^[^#/]/).test(uri); -} - -function resolveRelativePath(path) { - // Resolve relative paths manually for browsers using hash navigation - - var parts = path.split('/'); - var i = 1; - while (i < parts.length) { - // if current part is `..` and previous part is different, remove both of them - if (parts[i] === '..' && i > 0 && parts[i-1] !== '..') { - parts.splice(i - 1, 2); - i -= 2; - } - i++; - } - return parts - .join('/') - .replace(/\/\.\/|\.\/|\.\.\//g, '/') - .replace(/^\/$/, ''); -} +import { LEAVE, UPDATE, ENTER, CHANGE, MATCH, OLD_MATCH } from './constants'; // Add compatibility with old IE browsers var elementPrototype = typeof HTMLElement !== 'undefined' ? HTMLElement : Element; @@ -150,6 +15,9 @@ function PushStateTree(options) { return PushStateTree.apply(PushStateTree.createElement('pushstatetree-route'), arguments); } + // Allow plugins override the instance create + PushStateTree.create.apply(this, arguments); + this.eventStack = { leave: [], change: [], @@ -157,29 +25,23 @@ function PushStateTree(options) { match: [] }; - proxyLikePrototype(this, PushStateTree.prototype); - // Allow switch between pushState or hash navigation modes, in browser that doesn't support // pushState it will always be false. and use hash navigation enforced. // use backend non permanent redirect when old browsers are detected in the request. - if (!PushStateTree.hasPushState) { - proxyReadOnlyProperty(this, 'usePushState', false); - } else { - let usePushState = true; - Object.defineProperty(this, 'usePushState', { - get() { - return usePushState; - }, - set(val) { - usePushState = PushStateTree.hasPushState ? val !== false : false; - } - }); - this.usePushState = options.usePushState; - } + let usePushState = false; + Object.defineProperty(this, 'usePushState', { + get() { + return usePushState; + }, + set(val) { + usePushState = this.hasPushState ? val !== false : false; + } + }); + this.usePushState = options.usePushState; // When enabled beautifyLocation will replace the location using history.replaceState // to remove the hash from the URL - let beautifyLocation = true && this.usePushState; + let beautifyLocation = this.usePushState; Object.defineProperty(this, 'beautifyLocation', { get() { return beautifyLocation; @@ -292,7 +154,6 @@ function PushStateTree(options) { // Disabled must be the last thing before options, because it will start the listeners let disabled = true; - let disableMethod; Object.defineProperty(this, 'disabled', { get() { return disabled; @@ -301,8 +162,7 @@ function PushStateTree(options) { value = value === true; if (value != disabled) { disabled = value; - if (disabled) disableMethod(); - else disableMethod = this.startGlobalListeners(); + this.dispatchEvent(new root.CustomEvent(disabled ? 'disabled' : 'enabled')); } } }); @@ -318,13 +178,37 @@ const eventsQueue = []; let holdingDispatch = false; let holdDispatch = false; -let hasPushState = !!(history && history.pushState); - objectMixinProperties(PushStateTree, { // VERSION is defined in the webpack build, it is replaced by package.version VERSION, - isInt, - hasPushState, + plugins: [], + create(options) { + if (!this) { + throw new Error(DEV_ENV && 'Method create requires a context.'); + } + options = options || {}; + options.plugins = Array.isArray(options.plugins) ? options.plugins : []; + + // Proxy all plugins instance properties and methods + proxyLikePrototype(this, PushStateTree.prototype); + + this.plugins = []; + + let plugins = [...options.plugins, ...PushStateTree.plugins]; + + plugins.forEach(plugin => { + + // Expose loaded plugins + this.plugins.push(plugin); + + // Proxy all plugins instance properties and methods + proxyLikePrototype(this, plugin); + + if (plugin.create) { + plugin.create.apply(this, arguments); + } + }); + }, createElement(name) { // When document is available, use it to create and return a HTMLElement if (typeof document !== 'undefined') { @@ -336,76 +220,6 @@ objectMixinProperties(PushStateTree, { prototype: { // VERSION is defined in the webpack build, it is replaced by package.version VERSION, - hasPushState, - - startGlobalListeners() { - // Start the browser global listeners and return a method to stop listening to them - - let beautifyLocation = () => { - // apply pushState for a beautiful URL when beautifyLocation is enable and it's possible to do it - if (this.beautifyLocation - && this.usePushState - && this.isPathValid - && this.path.indexOf('#') != -1 - ) { - - // Execute after to pop_state again - this.replace(this.uri); - return true; - } - }; - - let dispatchListener = event => { - this.path = convertToURI(location.href); - - if (beautifyLocation()) { - event.preventDefault(); - } - }; - this.addEventListener('dispatch', dispatchListener); - - let browserListener = () => { - if (this.path != convertToURI(location.href)) { - this.dispatch(); - } - }; - - root.addEventListener(POP_STATE, browserListener); - root.addEventListener(HASH_CHANGE, browserListener); - - let ieWatch; - let loadListener = () => { - browserListener(); - - if (!isIE() || isIE() > 8) return; - - // Watch for URL changes in the IE - ieWatch = setInterval(browserListener, 50); - }; - - // If the DOM is ready when running the PST, execute loadListeners and ignore others - if (document.readyState == 'complete') { - loadListener(); - } else { - // Modern browsers - document.addEventListener('DOMContentLoaded', browserListener); - // Some IE browsers - root.addEventListener('readystatechange', browserListener); - // Almost all browsers - root.addEventListener('load', loadListener); - } - - return () => { - // Method to stop watching - this.removeEventListener('dispatch', dispatchListener); - root.removeEventListener(POP_STATE, browserListener); - document.removeEventListener('DOMContentLoaded', browserListener); - root.removeEventListener('readystatechange', browserListener); - root.removeEventListener(HASH_CHANGE, browserListener); - root.removeEventListener('load', loadListener); - if (ieWatch) clearInterval(ieWatch); - }; - }, createRule(options) { // Create a pushstreamtree-rule element from a literal object @@ -730,170 +544,5 @@ objectMixinProperties(PushStateTree, { } }); -function preProcessUriBeforeExecuteNativeHistoryMethods(method) { - - // If not pushState or replaceState methods, execute it from history API - if (method !== 'pushState' && method !== 'replaceState') { - this[method] = function () { - history[method].apply(history, arguments); - return this; - }; - return; - } - - this[method] = function () { - // Wrap method - - // remove the method from arguments - let args = Array.prototype.slice.call(arguments); - let uri = args[0] || ''; - if (typeof args[2] === 'string') { - uri = args[2]; - } - - // if has a basePath translate the not relative paths to use the basePath - if (!isExternal(uri)) { - // When not external link, need to normalize the URI - - if (isRelative(uri)) { - // Relative to the uri - var basePath = this.uri[MATCH](/^([^?#]*)\//); - basePath = basePath ? basePath[1] + '/' : ''; - uri = basePath + uri; - } else { - // This isn't relative, will cleanup / and # from the begin and use the remain path - uri = uri[MATCH](/^([#/]*)?(.*)/)[2]; - } - - if (!this.usePushState) { - - // Ignore basePath when using location.hash and resolve relative path and keep - // the current location.pathname, some browsers history API might apply the new pathname - // with the hash content if not explicit - uri = location.pathname + '#' + resolveRelativePath(uri); - } else { - - // Add the basePath to your uri, not allowing to go by pushState outside the basePath - uri = this.basePath + uri; - } - } - - // Ignore state and make the url be the current uri - args[0] = null; - args[2] = uri; - - this.path = this.basePath + uri; - history[method].apply(history, args); - return this; - }; -} - -// Wrap history methods -for (var method in history) { - if (typeof history[method] === 'function') { - preProcessUriBeforeExecuteNativeHistoryMethods.call(PushStateTree.prototype, method); - } -} - -// Add support to pushState on old browsers that doesn't native support it -if (typeof PST_NO_OLD_IE == 'undefined' - && typeof PST_NO_SHIM == 'undefined' - && !PushStateTree.hasPushState - && location -) { - PushStateTree.prototype.pushState = function (uri, ignored, deprecatedUri) { - // Does a shim for pushState when history API doesn't support pushState, - // from version 0.15.x it ignores state and title definition since they are - // never used in any production project so far and seems to make harder to - // developers to use the method since they need to add 2 useless arguments - // before the really necessary one. - // However it keeps compatible with any implementation that already add the - // url as third argument. - if (typeof deprecatedUri == 'string') { - uri = deprecatedUri; - } - if (typeof uri != 'string') { - uri = ''; - } - - // Replace hash url - if (isExternal(uri)) { - // this will redirect the browser, so doesn't matters the rest... - location.href = uri; - } - - // Remove the has if is it present - if (uri[0] === '#') { - uri = uri.slice(1); - } - - if (isRelative(uri)) { - uri = location.hash.slice(1, location.hash.lastIndexOf('/') + 1) + uri; - uri = resolveRelativePath(uri); - } - - // Include the basePath in the uri, if is already in the current router - // basePath, it will apply the assign without refresh, but if the basePath - // is different, it will refresh the browser to work in the current router - // basePath. - // The behavior is the same in browsers that support pushState, however they - // don't refresh when switching between basePath of different routers - this.path = this.basePath + uri; - if (location.pathname == this.basePath) { - location.hash = uri; - } else { - location.assign(uri); - } - - return this; - }; - - PushStateTree.prototype.replaceState = function (uri, ignored, deprecatedUri) { - // Does a shim for replaceState when history API doesn't support pushState, - // from version 0.15.x it ignores state and title definition since they are - // never used in any production project so far and seems to make harder to - // developers to use the method since they need to add 2 useless arguments - // before the really necessary one. - // However it keeps compatible with any implementation that already add the - // url as third argument. - - if (typeof deprecatedUri == 'string') { - uri = deprecatedUri; - } - if (typeof uri != 'string') { - uri = ''; - } - - // Replace the url - if (isExternal(uri)) { - throw new Error('Invalid url replace.'); - } - - if (uri[0] === '#') { - uri = uri.slice(1); - } - - if (isRelative(uri)) { - var relativePos = location.hash.lastIndexOf('/') + 1; - uri = location.hash.slice(1, relativePos) + uri; - uri = resolveRelativePath(uri); - } - - // Always use hash navigation - uri = '#' + uri; - - // Include the basePath in the uri, if is already in the current router - // basePath, it will apply the replace without refresh, but if the basePath - // is different, it will refresh the browser to work in the current router - // basePath. - // The behavior is the same in browsers that support pushState, however they - // don't refresh when switching between basePath of different routers - this.path = this.basePath + uri; - location.replace(uri); - - return this; - }; -} - // Node import support -module.exports = PushStateTree; +export default PushStateTree; diff --git a/src/route.js b/src/route.js new file mode 100644 index 0000000..1b3827e --- /dev/null +++ b/src/route.js @@ -0,0 +1,9 @@ +// Define the PushStateTree route elements + +export function Route() { + +} + +Route.adapter = document; + +export default Route; \ No newline at end of file diff --git a/test/helper/cleanHistoryAPI.js b/test/helper/cleanHistoryAPI.js index a26f59d..1e7fcf4 100644 --- a/test/helper/cleanHistoryAPI.js +++ b/test/helper/cleanHistoryAPI.js @@ -1,16 +1,14 @@ -const PushStateTree = require('../../src/push-state-tree'); +import BrowserHistory from '../../src/plugin/history'; +let globalListeners = []; -export default function cleanHistoryAPI() { +let cache = BrowserHistory.prototype.globalListeners; +BrowserHistory.prototype.globalListeners = function () { + let listeners = cache.apply(this, arguments); + globalListeners.push(listeners); + return listeners; +}; - let enabledInstances = []; - - before(() => { - let cache = PushStateTree.prototype.startGlobalListeners; - PushStateTree.prototype.startGlobalListeners = function () { - enabledInstances.push(this); - return cache.apply(this, arguments); - } - }); +export default function cleanHistoryAPI() { beforeEach(() => { history.pushState(null, null, '/'); @@ -19,8 +17,9 @@ export default function cleanHistoryAPI() { afterEach(() => { // Reset the URI before begin the tests history.pushState(null, null, '/'); - while (enabledInstances.length) { - enabledInstances.shift().disabled = true; + while (globalListeners.length) { + let disableGlobalListener = globalListeners.shift(); + disableGlobalListener(); } }); } \ No newline at end of file diff --git a/test/isInt.js b/test/isInt.js index a6d5d39..2868480 100644 --- a/test/isInt.js +++ b/test/isInt.js @@ -1,50 +1,50 @@ -var PushStateTree = require('../src/push-state-tree'); +import { isInt } from '../src/helpers'; describe('PushStateTree isInt', function() { it('should return true for valid numbers', function() { - expect(PushStateTree.isInt(0)).to.be.true; - expect(PushStateTree.isInt(1)).to.be.true; - expect(PushStateTree.isInt(-1)).to.be.true; + expect(isInt(0)).to.be.true; + expect(isInt(1)).to.be.true; + expect(isInt(-1)).to.be.true; }); it('should return true for valid number in string', function() { - expect(PushStateTree.isInt('0')).to.be.true; - expect(PushStateTree.isInt('1')).to.be.true; - expect(PushStateTree.isInt('2')).to.be.true; + expect(isInt('0')).to.be.true; + expect(isInt('1')).to.be.true; + expect(isInt('2')).to.be.true; }); it('should return false for Boolean values', function() { - expect(PushStateTree.isInt(true)).to.be.false; - expect(PushStateTree.isInt(false)).to.be.false; + expect(isInt(true)).to.be.false; + expect(isInt(false)).to.be.false; }); it('should return false for literal objects', function() { - expect(PushStateTree.isInt({})).to.be.false; + expect(isInt({})).to.be.false; }); it('should return false when no value is specified', function() { - expect(PushStateTree.isInt()).to.be.false; + expect(isInt()).to.be.false; }); it('should return false for invalid strings', function() { - expect(PushStateTree.isInt('1bc')).to.be.false; - expect(PushStateTree.isInt('1.1')).to.be.false; - expect(PushStateTree.isInt('-1.1')).to.be.false; + expect(isInt('1bc')).to.be.false; + expect(isInt('1.1')).to.be.false; + expect(isInt('-1.1')).to.be.false; }); it('should return false for null', function() { - expect(PushStateTree.isInt(null)).to.be.false; + expect(isInt(null)).to.be.false; }); it('should return false for infinity numbers', function() { - expect(PushStateTree.isInt(Number.POSITIVE_INFINITY)).to.be.false; - expect(PushStateTree.isInt(Number.NEGATIVE_INFINITY)).to.be.false; + expect(isInt(Number.POSITIVE_INFINITY)).to.be.false; + expect(isInt(Number.NEGATIVE_INFINITY)).to.be.false; }); it('should return false for invalid numbers', function() { - expect(PushStateTree.isInt(1.1)).to.be.false; - expect(PushStateTree.isInt(-1.1)).to.be.false; + expect(isInt(1.1)).to.be.false; + expect(isInt(-1.1)).to.be.false; }); }); diff --git a/test/route-basePath.js b/test/route-basePath.js index e498c9d..03845d6 100644 --- a/test/route-basePath.js +++ b/test/route-basePath.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; describe('PushStateTree Router basePath', function () { diff --git a/test/route-beutifyLocation.js b/test/route-beutifyLocation.js index 33d0cba..71e55e4 100644 --- a/test/route-beutifyLocation.js +++ b/test/route-beutifyLocation.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; const _ = require('underscore'); diff --git a/test/route-createRules.js b/test/route-createRules.js index 3cb436f..36817f1 100644 --- a/test/route-createRules.js +++ b/test/route-createRules.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; describe('PushStateTree createRule', function() { diff --git a/test/route-force-hash-navigation.js b/test/route-force-hash-navigation.js index e815f29..bab39e4 100644 --- a/test/route-force-hash-navigation.js +++ b/test/route-force-hash-navigation.js @@ -1,7 +1,7 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; -describe('PushStateTree hash-navigation should', function() { +describe('PushStateTree hash-navigation', function() { cleanHistoryAPI(); @@ -17,7 +17,7 @@ describe('PushStateTree hash-navigation should', function() { PushStateTree.hasPushState = cacheVal; }); - it('force hash navigation if browser doesn\'t support pushState', () => { + it(`should force hash navigation if browser doesn't support pushState`, () => { var pst = new PushStateTree({ usePushState: true }); diff --git a/test/route-hash-navigation.js b/test/route-hash-navigation.js index 5dd024f..604805d 100644 --- a/test/route-hash-navigation.js +++ b/test/route-hash-navigation.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; const _ = require('underscore'); diff --git a/test/route-methods.js b/test/route-methods.js index 3e54bed..c32636d 100644 --- a/test/route-methods.js +++ b/test/route-methods.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; describe('PushStateTree methods', function() { diff --git a/test/route.js b/test/route.js index 3d6fae7..8e01a0c 100644 --- a/test/route.js +++ b/test/route.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; describe('PushStateTree should', function() { diff --git a/test/rule-events.js b/test/rule-events.js index 83880bd..62794f1 100644 --- a/test/rule-events.js +++ b/test/rule-events.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; const _ = require('underscore'); diff --git a/test/rule-properties.js b/test/rule-properties.js index 0cc298e..01caaef 100644 --- a/test/rule-properties.js +++ b/test/rule-properties.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; describe('PushStateTree properties', () => { diff --git a/test/rule.js b/test/rule.js index 9c5da78..96cf9aa 100644 --- a/test/rule.js +++ b/test/rule.js @@ -1,4 +1,4 @@ -const PushStateTree = require('../src/push-state-tree'); +const PushStateTree = require('../src/main'); import cleanHistoryAPI from './helper/cleanHistoryAPI'; describe('PushStateTree-rule', function () { diff --git a/test/version.js b/test/version.js index 196ccf6..ed2e81c 100644 --- a/test/version.js +++ b/test/version.js @@ -1,6 +1,6 @@ -const PushStateTree = require('../src/push-state-tree'); -const pkg = require('../package.json'); +import PushStateTree from '../src/push-state-tree'; import cleanHistoryAPI from './helper/cleanHistoryAPI'; +const pkg = require('../package.json'); describe('PushStateTree', function () { diff --git a/webpack.config.js b/webpack.config.js index 24c27e6..cfe7036 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,9 +24,9 @@ const BANNER = `${pkg.title} - v${pkg.version} - ${moment().format('YYYY-MM-DD') Copyright (c) ${moment().format('YYYY')} ${pkg.author.name}; Licensed ${pkg.licenses[0].type}`; let config = { - entry: !PUBLISH ? { 'push-state-tree': './src/push-state-tree' } : { - 'push-state-tree': './src/push-state-tree', - 'push-state-tree.min': './src/push-state-tree' + entry: !PUBLISH ? { 'push-state-tree': './src/main' } : { + 'push-state-tree': './src/main', + 'push-state-tree.min': './src/main' }, output: { path: path.join(BASE_PATH, 'build'), From 54dd6eb460ae74aabe46c85734e937ba8c01ccf2 Mon Sep 17 00:00:00 2001 From: Gabriel Reitz Giannattasio Date: Tue, 26 Apr 2016 10:49:27 -0700 Subject: [PATCH 02/11] Fixed npm test in the new architecture --- karma.conf.js | 37 ++++++++++++++++--------------------- src/main.js | 2 +- test/{ => helpers}/isInt.js | 2 +- webpack.config.js | 8 +++++--- 4 files changed, 23 insertions(+), 26 deletions(-) rename test/{ => helpers}/isInt.js (96%) diff --git a/karma.conf.js b/karma.conf.js index c2689ce..bcee778 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -35,6 +35,13 @@ const WATCH = argv.watch; // so it's disabled by default, more info at https://webpack.github.io/docs/configuration.html#devtool const devtool = argv['fast-build'] ? 'cheap-module-eval-source-map' : 'inline-source-map'; + +// For development server testing, it should not be a library, a devserver.js is including and assign the global +// PushStateTree for the logic in the Demo work +delete webpackConfig.output.library; +delete webpackConfig.output.libraryTarget; +delete webpackConfig.output.umdNamedDefine; + if (WATCH) { let port = argv['dev-port'] || 8080; @@ -47,21 +54,16 @@ if (WATCH) { webpackDevConfig.output.pathinfo = true; webpackDevConfig.resolve.alias = { - 'push-state-tree': path.resolve(__dirname, webpackDevConfig.entry['push-state-tree']) + 'push-state-tree': path.resolve(__dirname, webpackDevConfig.entry['push-state-tree'][0]) }; webpackDevConfig.entry['push-state-tree'] = [ // Add inline webpack-dev-server client `webpack-dev-server/client?http://localhost:${port}/`, // Keep default lib - 'expose?PushStateTree!' + webpackDevConfig.entry['push-state-tree'] + 'expose?PushStateTree!' + webpackDevConfig.entry['push-state-tree'][0] ]; - // For development server testing, it should not be a library, a devserver.js is including and assign the global - // PushStateTree for the logic in the Demo work - delete webpackDevConfig.output.library; - delete webpackDevConfig.output.libraryTarget; - delete webpackDevConfig.output.umdNamedDefine; webpackDevConfig.module.preLoaders = []; var compiler = webpack(webpackDevConfig); @@ -98,11 +100,13 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ - 'test/*.js' + 'test/**/*.js' ], // list of files to exclude - exclude: [], + exclude: [ + 'test/helper/**/*.js' + ], // test results reporter to use reporters: ['progress', 'coverage'], @@ -150,24 +154,15 @@ module.exports = function (config) { exclude: /(node_modules)/, loader: 'json' } - ] - }; - - if (WATCH) { - module.postLoaders = [ + ], + postLoaders: [ { test: /\.js$/, exclude: /(test|node_modules|bower_components|\.shim\.js$|\.json$)/, loader: 'istanbul-instrumenter' } ] - } else { - module.preLoaders.unshift({ - test: /\.js$/, - exclude: /(test|node_modules|bower_components|\.shim\.js$|\.json$)/, - loader: 'istanbul-instrumenter' - }); - } + }; return module; }())), diff --git a/src/main.js b/src/main.js index e28b5b1..a61bbae 100644 --- a/src/main.js +++ b/src/main.js @@ -12,6 +12,6 @@ import PushStateTree from './push-state-tree'; import BrowserHistory from './plugin/history'; -PushStateTree.plugins.push(new BrowserHistory()) +PushStateTree.plugins.push(new BrowserHistory()); module.exports = PushStateTree; \ No newline at end of file diff --git a/test/isInt.js b/test/helpers/isInt.js similarity index 96% rename from test/isInt.js rename to test/helpers/isInt.js index 2868480..8cedcf2 100644 --- a/test/isInt.js +++ b/test/helpers/isInt.js @@ -1,4 +1,4 @@ -import { isInt } from '../src/helpers'; +import { isInt } from '../../src/helpers'; describe('PushStateTree isInt', function() { diff --git a/webpack.config.js b/webpack.config.js index cfe7036..138453e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,9 +24,11 @@ const BANNER = `${pkg.title} - v${pkg.version} - ${moment().format('YYYY-MM-DD') Copyright (c) ${moment().format('YYYY')} ${pkg.author.name}; Licensed ${pkg.licenses[0].type}`; let config = { - entry: !PUBLISH ? { 'push-state-tree': './src/main' } : { - 'push-state-tree': './src/main', - 'push-state-tree.min': './src/main' + entry: PUBLISH ? { + 'push-state-tree': ['./src/main'], + 'push-state-tree.min': ['./src/main'] + } : { + 'push-state-tree': ['./src/main'] }, output: { path: path.join(BASE_PATH, 'build'), From 8bcaa83d44f247ded124135ee43defca15f76b9a Mon Sep 17 00:00:00 2001 From: Gabriel Reitz Giannattasio Date: Fri, 20 May 2016 10:54:00 -0700 Subject: [PATCH 03/11] Add plugins and adapters support --- index.html | 28 +- karma.conf.js | 14 +- src/adapter/browser.js | 170 ++++++++++++ src/adapter/pluginInjector.js | 12 + src/helpers.js | 13 +- src/main.js | 7 +- src/plugin/history.js | 290 +++++++------------- src/push-state-tree.js | 99 +++---- test/{ => history}/route-beutifyLocation.js | 15 +- 9 files changed, 386 insertions(+), 262 deletions(-) create mode 100644 src/adapter/browser.js create mode 100644 src/adapter/pluginInjector.js rename test/{ => history}/route-beutifyLocation.js (89%) diff --git a/index.html b/index.html index 228fd95..b87d990 100644 --- a/index.html +++ b/index.html @@ -39,15 +39,15 @@ if (location.hostname.indexOf('github') === -1){ basePath = ''; } - + // If no internet found (programming on travels...) // Load resources from offline folder if (typeof $ !== 'function' || !$.when){ - + function loadOffline(src, callback) { // Load a file and execute the callback, should be used to // load files in the right order and then boot the system - + var type = src.substr(-3) === 'css' ? 'link' : 'script'; var element = document.createElement(type); var path = location.origin + basePath + '/demo/offline/'; @@ -74,24 +74,26 @@ } else { boot(); } - - function boot(){ + + function boot() { // boot the demo page function load(src){ // Simple script loader based on promises - - var defer = $.Deferred();; + + var defer = $.Deferred(); if (!$.support.leadingWhitespace) { $.getScript(location.protocol + '//' + location.host + src) - .then(defer.resolve, defer.reject); + .then(defer.resolve, defer.reject) + ; } else { $('