diff --git a/doc/ns.history.md b/doc/ns.history.md new file mode 100644 index 00000000..1119261c --- /dev/null +++ b/doc/ns.history.md @@ -0,0 +1,33 @@ +# ns.History + +В noscript для смены URL в адресной строке используется HTML5 History API, который не поддерживается [в IE раньше 10](http://caniuse.com/#feat=history). + +## Polyfill для IE + +В качестве полифилла можно использовать [devote/HTML5-History-API](https://github.com/devote/HTML5-History-API). Скрипт предоставляет стандартизированное API и будет использовать смену хеш-фрагмента URL для навигации. + + /notes/141 -> /#/notes/141 + +Кроме подключения самого скрипта на страницу нужно проделать небольшую работу: + +1. Организовать редирект до старта приложения: + +```js +// Тут может произойти смена URL и перезагрузка, поэтому какие-нибудь +// модели до редиректа запрашивать бессмысленно. +window.history.redirect(); + +ns.init(); +``` + +2. Переопределить вычисление текущего URL приложения: + +```js +var history = window.history; + +if (history.emulate) { + ns.page.getCurrentUrl = function() { + return history.location.pathname + history.location.search; + }; +} +``` diff --git a/src/ns.action.js b/src/ns.action.js index 9a9506c2..869a0916 100644 --- a/src/ns.action.js +++ b/src/ns.action.js @@ -198,8 +198,8 @@ ns.action._process = function(e) { return true; } - // если host ссылки не равен нашему хосту - if (target.host != window.location.host) { + // если hostname ссылки не равен нашему хосту + if (target.hostname != window.location.hostname) { return true; } diff --git a/src/ns.history.js b/src/ns.history.js index 1ac3c5a1..0a3d2b39 100644 --- a/src/ns.history.js +++ b/src/ns.history.js @@ -1,104 +1,44 @@ +(function() { + /** * Объект для работы с историей и адресной строкой браузера. - * - * Методы: - * - * `ns.history.pushState(url)` - * Меняет URL в адресной строке, запоминая его в истории браузера. - * `ns.history.replaceState(url)` - * Меняет URL в адресной строке, заменяя текущую запись в истории браузера. - * `ns.history.adapt()` - * Адаптирует хешовые URL для браузеров, поддерживающих HTML5 History API, - * выполняя трансформации типа `/#/message/123/` в `/message/123/`. - * `ns.history.legacy` - * {Boolean} - * Флаг использования HTML5 History API или отката на `hashchange`. + * @namespace */ -ns.history = (function(window, ns) { - - var history = window.history; - var loc = window.location; - - var legacy = !(history.pushState && history.replaceState); - - // API для браузеров с поддержкой HTML5 History API. - if (!legacy) { - return { - pushState: function(url, title) { - history.pushState(null, title || ns.page.title(url), url); - }, - replaceState: function(url, title) { - history.replaceState(null, title || ns.page.title(url), url); - }, - - // Редирект с хешового урла на его полноценный аналог. - adapt: function() { - var hash = loc.hash.substr(1); - - if (hash.length) { - - var hashRoute = ns.router(hash); - if (hashRoute.page !== ns.R.NOT_FOUND && hashRoute.page !== ns.R.REDIRECT) { - - // TODO: При добавлении способа задания корневого урла заменить - // слеш на нужное свойство/метод. - if (loc.pathname === '/' || ns.router(loc.pathname).page === ns.R.NOT_FOUND) { - // Если в хеше уже есть какие-то query параметры, - // текущие надо к ним прибавить. - var search = hash.indexOf('?') !== -1 ? loc.search.replace(/^\?/, '&') : loc.search; - this.replaceState(hash + search); - } - } - } +ns.history = {}; - window.addEventListener('popstate', function(e) { - // прибиваем событие, чтобы не дергалась адресная строка - e.preventDefault(); - e.stopPropagation(); +ns.history.init = function() { + $(window).on('popstate', function(e) { + // прибиваем событие, чтобы не дергалась адресная строка + e.preventDefault(); + e.stopPropagation(); - ns.page.go('', true); - }, false); + ns.history.onpopstate(e); + }); +}; - // Здесь `ns.page.go` можно не вызывать, потому что после `ns.init` - // `ns.page.go` все равно вызывается. - }, - legacy: false - }; - - // Откат на `hashchange`. - } else { - // Редирект с текущего полноценного урла на его хешовый аналог. - // - // TODO: При добавлении способа задания корневого урла заменить - // слеш на нужное свойство/метод. - if ((loc.pathname + loc.search) !== '/') { - return loc.replace('/#' + loc.pathname + loc.search); - } - - // Заставляет `hashchange` игнорировать смену URL через вызов legacy - // `pushState` или `replaceState`. - var ignore = false; +ns.history.pushState = function(url, title) { + if (isFunction(window.history.pushState)) { + window.history.pushState(null, title || ns.page.title(url), url); + } +}; - window.addEventListener('hashchange', function() { - if (!ignore) { - ns.page.go(); - } +ns.history.replaceState = function(url, title) { + if (isFunction(window.history.replaceState)) { + window.history.replaceState(null, title || ns.page.title(url), url); + } +}; - ignore = false; - }, false); +ns.history.onpopstate = function() { + ns.page.go('', true); +}; - return { - pushState: function(url) { - ignore = true; - loc.hash = url; - }, - replaceState: function(url) { - ignore = true; - loc.replace(loc.pathname + loc.search + '#' + url); - }, - adapt: no.nop, - legacy: true - }; - } +/** + * Проверяет, является ли переданный объект функцией. + * @param {Function} fn + * @return {Boolean} + */ +function isFunction(fn) { + return 'function' == typeof fn; +} -}(window, ns)); +})(); diff --git a/src/ns.js b/src/ns.js index 0e57b7be..1ff37f6e 100644 --- a/src/ns.js +++ b/src/ns.js @@ -112,9 +112,8 @@ ns.tmpl = function(json, mode, module) { ns.init = function() { ns.action.init(); ns.router.init(); + ns.history.init(); ns.initMainView(); - - ns.history.adapt(); }; /** diff --git a/src/ns.page.js b/src/ns.page.js index 9793bd49..64baf92d 100644 --- a/src/ns.page.js +++ b/src/ns.page.js @@ -18,7 +18,7 @@ ns.page._lastUrl = ''; /** * Осуществляем переход по ссылке. - * @param {String} [url=location.pathname + location.search] + * @param {String} [url=ns.page.getCurrentUrl()] * @param {Boolean} [preventAddingToHistory=false] Не добавлять урл в историю браузера. * @return {no.Promise} */ @@ -29,9 +29,7 @@ ns.page.go = function(url, preventAddingToHistory) { return no.Promise.rejected('transaction'); } - var loc = window.location; - - url = url || (ns.history.legacy ? loc.hash.substr(1) : (loc.pathname + loc.search)); + url = url || ns.page.getCurrentUrl(); // возможность заблокировать переход if (!ns.page.block.check(url)) { @@ -165,6 +163,13 @@ ns.page.getDefaultUrl = function() { return ns.router.url('/'); }; +/** + * Calculates current application url, fed as default value for `ns.page.go`. + */ +ns.page.getCurrentUrl = function() { + return window.location.pathname + window.location.search; +}; + /** * Object to work with application history. * @namespace diff --git a/test/index.html b/test/index.html index a107ce5d..45b2194b 100644 --- a/test/index.html +++ b/test/index.html @@ -39,17 +39,17 @@ + - - + + - @@ -69,7 +69,6 @@ - diff --git a/test/spec/ns.history.js b/test/spec/ns.history.js deleted file mode 100644 index 2a96278f..00000000 --- a/test/spec/ns.history.js +++ /dev/null @@ -1,143 +0,0 @@ -describe('ns.history', function() { - - // PhantomJS не умеет работать с URL - if (/PhantomJS\//.test(navigator.userAgent)) { - return; - } - - var original = window.location.pathname + window.location.search; - - afterEach(function() { - window.history.replaceState(null, 'test', original); - }); - - describe('API', function() { - - it('ns.history should be defined', function() { - expect(ns.history).to.be.an('object'); - }); - - it('ns.history.pushState should be defined', function() { - expect(ns.history.pushState).to.be.a('function'); - }); - - it('ns.history.replaceState should be defined', function() { - expect(ns.history.replaceState).to.be.a('function'); - }); - - it('ns.history.adapt should be defined', function() { - expect(ns.history.adapt).to.be.a('function'); - }); - - it('ns.history.legacy should have a specific value', function() { - expect(ns.history.legacy).to.be.a('boolean'); - }); - - }); - - describe('behavior in modern browsers', function() { - - var url = '/some/address/with%20spaces/'; - - it('ns.history.legacy should be false', function() { - expect(ns.history.legacy).to.be(false); - }); - - it(!!window['phantom'] + ' 121212ns.history.pushState should change the address bar URL to the one passed in', function() { - ns.history.pushState(url); - - expect(window.location.pathname + window.location.search).to.be(url); - }); - - it('ns.history.pushState should not trigger popstate event', function(done) { - var triggered = false; - var onpopstate = function() { triggered = true; }; - - window.addEventListener('popstate', onpopstate, false); - - ns.history.pushState(url); - - setTimeout(function() { - expect(triggered).to.be(false); - window.removeEventListener('popstate', onpopstate); - done(); - }, 0); - }); - - }); - - describe('adapting hashed URLs to their original versions', function() { - - beforeEach(function() { - ns.router.routes = { - route: { - '/message/{message-id:int}': 'layout', - '/photo/{photo-id:int}': 'layout', - '/': 'layout' - } - }; - ns.router.init(); - - // заменяем ns.page.go на пустую функцию, нам там нечего проверять - sinon.stub(ns.page, 'go', no.nop); - }); - - afterEach(function() { - ns.page.go.restore(); - }); - - it('should happen when the hash matches any of the defined routes', function(done) { - - window.history.pushState(null, 'test', '/#/message/123/'); - - ns.history.adapt(); - - setTimeout(function() { - expect(window.location.pathname + window.location.search).to.be('/message/123/'); - done(); - }, 0); - - }); - - it('should keep the query params', function(done) { - - window.history.pushState(null, 'test', '/?debug=true&append=%25a%25#/message/123/?param=1'); - - ns.history.adapt(); - - setTimeout(function() { - expect(window.location.pathname + window.location.search).to.be('/message/123/?param=1&debug=true&append=%25a%25'); - done(); - }, 0); - - }); - - it('should not happen when the hash is a random non-route string', function(done) { - - window.history.pushState(null, 'test', '/?debug=true#top'); - - ns.history.adapt(); - - setTimeout(function() { - expect(window.location.pathname + window.location.search + window.location.hash).to.be('/?debug=true#top'); - done(); - }, 0); - - }); - - it('should not happen when both pathname and hash match routes', function(done) { - - window.history.pushState(null, 'test', '/message/123/#/photo/321/'); - - ns.history.adapt(); - - setTimeout(function() { - expect(window.location.pathname + window.location.search).to.be('/message/123/'); - done(); - }, 0); - - }); - - }); - -});