3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
7 YUI.add('pjax-base', function(Y) {
10 `Y.Router` extension that provides the core plumbing for enhanced navigation
11 implemented using the pjax technique (HTML5 pushState + Ajax).
18 var win = Y.config.win,
22 // The CSS class name used to filter link clicks from only the links which
23 // the pjax enhanced navigation should be used.
24 CLASS_PJAX = Y.ClassNameManager.getClassName('pjax'),
27 Fired when navigating to a URL via Pjax.
29 When the `navigate()` method is called or a pjax link is clicked, this event
30 will be fired if the browser supports HTML5 history _and_ the router has a
31 route handler for the specified URL.
33 This is a useful event to listen to for adding a visual loading indicator
34 while the route handlers are busy handling the URL change.
37 @param {String} url The URL that the router will dispatch to its route
38 handlers in order to fulfill the enhanced navigation "request".
39 @param {Boolean} [force=false] Whether the enhanced navigation should occur
40 even in browsers without HTML5 history.
41 @param {String} [hash] The hash-fragment (including "#") of the `url`. This
42 will be present when the `url` differs from the current URL only by its
43 hash and `navigateOnHash` has ben set to `true`.
44 @param {Event} [originEvent] The event that caused the navigation. Usually
45 this would be a click event from a "pjax" anchor element.
46 @param {Boolean} [replace] Whether or not the current history entry will be
47 replaced, or a new entry will be created. Will default to `true` if the
48 specified `url` is the same as the current URL.
51 EVT_NAVIGATE = 'navigate';
54 `Y.Router` extension that provides the core plumbing for enhanced navigation
55 implemented using the pjax technique (HTML5 `pushState` + Ajax).
57 This makes it easy to enhance the navigation between the URLs of an application
58 in HTML5 history capable browsers by delegating to the router to fulfill the
59 "request" and seamlessly falling-back to using standard full-page reloads in
60 older, less-capable browsers.
62 The `PjaxBase` class isn't useful on its own, but can be mixed into a
63 `Router`-based class to add Pjax functionality to that Router. For a pre-made
64 standalone Pjax router, see the `Pjax` class.
66 var MyRouter = Y.Base.create('myRouter', Y.Router, [Y.PjaxBase], {
74 function PjaxBase() {}
76 PjaxBase.prototype = {
77 // -- Protected Properties -------------------------------------------------
80 Holds the delegated pjax-link click handler.
89 Regex used to break up a URL string around the URL's path.
93 1. Origin, everything before the URL's path-part.
94 2. The URL's path-part.
95 3. Suffix, everything after the URL's path-part.
102 _regexURL: /^((?:[^\/#?:]+:\/\/|\/\/)[^\/]*)?([^?#]*)(\?[^#]*)?(#.*)?$/,
104 // -- Lifecycle Methods ----------------------------------------------------
105 initializer: function () {
106 this.publish(EVT_NAVIGATE, {defaultFn: this._defNavigateFn});
108 // Pjax is all about progressively enhancing the navigation between
109 // "pages", so by default we only want to handle and route link clicks
110 // in HTML5 `pushState`-compatible browsers.
111 if (this.get('html5')) {
116 destructor: function () {
117 this._pjaxEvents && this._pjaxEvents.detach();
120 // -- Public Methods -------------------------------------------------------
123 Navigates to the specified URL if there is a route handler that matches. In
124 browsers capable of using HTML5 history, the navigation will be enhanced by
125 firing the `navigate` event and having the router handle the "request".
126 Non-HTML5 browsers will navigate to the new URL via manipulation of
129 When there is a route handler for the specified URL and it is being
130 navigated to, this method will return `true`, otherwise it will return
133 **Note:** The specified URL _must_ be of the same origin as the current URL,
134 otherwise an error will be logged and navigation will not occur. This is
135 intended as both a security constraint and a purposely imposed limitation as
136 it does not make sense to tell the router to navigate to a URL on a
137 different scheme, host, or port.
140 @param {String} url The URL to navigate to. This must be of the same origin
142 @param {Object} [options] Additional options to configure the navigation.
143 These are mixed into the `navigate` event facade.
144 @param {Boolean} [options.replace] Whether or not the current history
145 entry will be replaced, or a new entry will be created. Will default
146 to `true` if the specified `url` is the same as the current URL.
147 @param {Boolean} [options.force=false] Whether the enhanced navigation
148 should occur even in browsers without HTML5 history.
149 @return {Boolean} `true` if the URL was navigated to, `false` otherwise.
152 navigate: function (url, options) {
153 // The `_navigate()` method expects fully-resolved URLs.
154 url = this._resolveURL(url);
156 if (this._navigate(url, options)) {
160 if (!this._hasSameOrigin(url)) {
161 Y.error('Security error: The new URL must be of the same origin as the current URL.');
167 // -- Protected Methods ----------------------------------------------------
170 Returns the current path root after popping off the last path segment,
171 making it useful for resolving other URL paths against.
173 The path root will always begin and end with a '/'.
176 @return {String} The URL's path root.
180 _getRoot: function () {
182 path = Y.getLocation().pathname,
185 if (path.charAt(path.length - 1) === slash) {
189 segments = path.split(slash);
192 return segments.join(slash) + slash;
196 Underlying implementation for `navigate()`.
199 @param {String} url The fully-resolved URL that the router should dispatch
200 to its route handlers to fulfill the enhanced navigation "request", or use
201 to update `window.location` in non-HTML5 history capable browsers.
202 @param {Object} [options] Additional options to configure the navigation.
203 These are mixed into the `navigate` event facade.
204 @param {Boolean} [options.replace] Whether or not the current history
205 entry will be replaced, or a new entry will be created. Will default
206 to `true` if the specified `url` is the same as the current URL.
207 @param {Boolean} [options.force=false] Whether the enhanced navigation
208 should occur even in browsers without HTML5 history.
209 @return {Boolean} `true` if the URL was navigated to, `false` otherwise.
213 _navigate: function (url, options) {
214 // Navigation can only be enhanced if there is a route-handler.
215 if (!this.hasRoute(url)) {
219 options || (options = {});
222 var currentURL = this._getURL(),
225 // Captures the `url`'s hash and returns a URL without that hash.
226 hashlessURL = url.replace(/(#.*)$/, function (u, h, i) {
228 return u.substring(i);
231 if (hash && hashlessURL === currentURL.replace(/#.*$/, '')) {
232 // When the specified `url` and current URL only differ by the hash,
233 // the browser should handle this in-page navigation normally.
234 if (!this.get('navigateOnHash')) {
241 // When navigating to the same URL as the current URL, behave like a
242 // browser and replace the history entry instead of creating a new one.
243 Lang.isValue(options.replace) || (options.replace = url === currentURL);
245 // The `navigate` event will only fire and therefore enhance the
246 // navigation to the new URL in HTML5 history enabled browsers or when
247 // forced. Otherwise it will fallback to assigning or replacing the URL
248 // on `window.location`.
249 if (this.get('html5') || options.force) {
250 this.fire(EVT_NAVIGATE, options);
252 if (options.replace) {
253 win && win.location.replace(url);
255 win && (win.location = url);
263 Returns a normalized path, ridding it of any '..' segments and properly
264 handling leading and trailing slashes.
266 @method _normalizePath
267 @param {String} path URL path to normalize.
268 @return {String} Normalized path.
272 _normalizePath: function (path) {
275 i, len, normalized, segments, segment, stack;
277 if (!path || path === slash) {
281 segments = path.split(slash);
284 for (i = 0, len = segments.length; i < len; ++i) {
285 segment = segments[i];
287 if (segment === dots) {
289 } else if (segment) {
294 normalized = slash + stack.join(slash);
296 // Append trailing slash if necessary.
297 if (normalized !== slash && path.charAt(path.length - 1) === slash) {
305 Binds the delegation of link-click events that match the `linkSelector` to
306 the `_onLinkClick()` handler.
308 By default this method will only be called if the browser is capable of
315 _pjaxBindUI: function () {
316 // Only bind link if we haven't already.
317 if (!this._pjaxEvents) {
318 this._pjaxEvents = Y.one('body').delegate('click',
319 this._onLinkClick, this.get('linkSelector'), this);
324 Returns the normalized result of resolving the `path` against the current
325 path. Falsy values for `path` will return just the current path.
328 @param {String} path URL path to resolve.
329 @return {String} Resolved path.
333 _resolvePath: function (path) {
335 return this._getPath();
338 // Path is host-relative and assumed to be resolved and normalized,
339 // meaning silly paths like: '/foo/../bar/' will be returned as-is.
340 if (path.charAt(0) === '/') {
341 return this._normalizePath(path);
344 return this._normalizePath(this._getRoot() + path);
348 Resolves the specified URL against the current URL.
350 This method resolves URLs like a browser does and will always return an
351 absolute URL. When the specified URL is already absolute, it is assumed to
352 be fully resolved and is simply returned as is. Scheme-relative URLs are
353 prefixed with the current protocol. Relative URLs are giving the current
354 URL's origin and are resolved and normalized against the current path root.
357 @param {String} url URL to resolve.
358 @return {String} Resolved URL.
362 _resolveURL: function (url) {
363 var parts = url && url.match(this._regexURL),
364 origin, path, query, hash, resolved;
367 return this._getURL();
375 // Absolute and scheme-relative URLs are assumed to be fully-resolved.
377 // Prepend the current scheme for scheme-relative URLs.
378 if (origin.indexOf('//') === 0) {
379 origin = Y.getLocation().protocol + origin;
382 return origin + (path || '/') + (query || '') + (hash || '');
385 // Will default to the current origin and current path.
386 resolved = this._getOrigin() + this._resolvePath(path);
388 // A path or query for the specified URL trumps the current URL's.
390 return resolved + (query || '') + (hash || '');
393 query = this._getQuery();
395 return resolved + (query ? ('?' + query) : '') + (hash || '');
398 // -- Protected Event Handlers ---------------------------------------------
401 Default handler for the `navigate` event.
403 Adds a new history entry or replaces the current entry for the specified URL
404 and will scroll the page to the top if configured to do so.
406 @method _defNavigateFn
407 @param {EventFacade} e
411 _defNavigateFn: function (e) {
412 this[e.replace ? 'replace' : 'save'](e.url);
414 if (win && this.get('scrollToTop')) {
415 // Scroll to the top of the page. The timeout ensures that the
416 // scroll happens after navigation begins, so that the current
417 // scroll position will be restored if the user clicks the back
419 setTimeout(function () {
426 Handler for delegated link-click events which match the `linkSelector`.
428 This will attempt to enhance the navigation to the link element's `href` by
429 passing the URL to the `_navigate()` method. When the navigation is being
430 enhanced, the default action is prevented.
432 If the user clicks a link with the middle/right mouse buttons, or is holding
433 down the Ctrl or Command keys, this method's behavior is not applied and
434 allows the native behavior to occur. Similarly, if the router is not capable
435 or handling the URL because no route-handlers match, the link click will
439 @param {EventFacade} e
443 _onLinkClick: function (e) {
446 // Allow the native behavior on middle/right-click, or when Ctrl or
447 // Command are pressed.
448 if (e.button !== 1 || e.ctrlKey || e.metaKey) { return; }
450 // All browsers fully resolve an anchor's `href` property.
451 url = e.currentTarget.get('href');
453 // Try and navigate to the URL via the router, and prevent the default
454 // link-click action if we do.
455 url && this._navigate(url, {originEvent: e}) && e.preventDefault();
461 CSS selector string used to filter link click events so that only the links
462 which match it will have the enhanced navigation behavior of Pjax applied.
464 When a link is clicked and that link matches this selector, Pjax will
465 attempt to dispatch to any route handlers matching the link's `href` URL. If
466 HTML5 history is not supported or if no route handlers match, the link click
467 will be handled by the browser just like any old link.
469 @attribute linkSelector
470 @type String|Function
476 value : 'a.' + CLASS_PJAX,
477 writeOnce: 'initOnly'
481 Whether navigating to a hash-fragment identifier on the current page should
482 be enhanced and cause the `navigate` event to fire.
484 By default Pjax allows the browser to perform its default action when a user
485 is navigating within a page by clicking in-page links
486 (e.g. `<a href="#top">Top of page</a>`) and does not attempt to interfere or
487 enhance in-page navigation.
489 @attribute navigateOnHash
499 Whether the page should be scrolled to the top after navigating to a URL.
501 When the user clicks the browser's back button, the previous scroll position
504 @attribute scrollToTop
514 Y.PjaxBase = PjaxBase;
517 }, '3.5.0' ,{requires:['classnamemanager', 'node-event-delegate', 'router']});