|
1 /* |
|
2 YUI 3.10.3 (build 2fb5187) |
|
3 Copyright 2013 Yahoo! Inc. All rights reserved. |
|
4 Licensed under the BSD License. |
|
5 http://yuilibrary.com/license/ |
|
6 */ |
|
7 |
|
8 YUI.add('pjax-base', function (Y, NAME) { |
|
9 |
|
10 /** |
|
11 `Y.Router` extension that provides the core plumbing for enhanced navigation |
|
12 implemented using the pjax technique (HTML5 pushState + Ajax). |
|
13 |
|
14 @module pjax |
|
15 @submodule pjax-base |
|
16 @since 3.5.0 |
|
17 **/ |
|
18 |
|
19 var win = Y.config.win, |
|
20 |
|
21 // The CSS class name used to filter link clicks from only the links which |
|
22 // the pjax enhanced navigation should be used. |
|
23 CLASS_PJAX = Y.ClassNameManager.getClassName('pjax'), |
|
24 |
|
25 /** |
|
26 Fired when navigating to a URL via Pjax. |
|
27 |
|
28 When the `navigate()` method is called or a pjax link is clicked, this event |
|
29 will be fired if the browser supports HTML5 history _and_ the router has a |
|
30 route handler for the specified URL. |
|
31 |
|
32 This is a useful event to listen to for adding a visual loading indicator |
|
33 while the route handlers are busy handling the URL change. |
|
34 |
|
35 @event navigate |
|
36 @param {String} url The URL that the router will dispatch to its route |
|
37 handlers in order to fulfill the enhanced navigation "request". |
|
38 @param {Boolean} [force=false] Whether the enhanced navigation should occur |
|
39 even in browsers without HTML5 history. |
|
40 @param {String} [hash] The hash-fragment (including "#") of the `url`. This |
|
41 will be present when the `url` differs from the current URL only by its |
|
42 hash and `navigateOnHash` has been set to `true`. |
|
43 @param {Event} [originEvent] The event that caused the navigation. Usually |
|
44 this would be a click event from a "pjax" anchor element. |
|
45 @param {Boolean} [replace] Whether or not the current history entry will be |
|
46 replaced, or a new entry will be created. Will default to `true` if the |
|
47 specified `url` is the same as the current URL. |
|
48 @since 3.5.0 |
|
49 **/ |
|
50 EVT_NAVIGATE = 'navigate'; |
|
51 |
|
52 /** |
|
53 `Y.Router` extension that provides the core plumbing for enhanced navigation |
|
54 implemented using the pjax technique (HTML5 `pushState` + Ajax). |
|
55 |
|
56 This makes it easy to enhance the navigation between the URLs of an application |
|
57 in HTML5 history capable browsers by delegating to the router to fulfill the |
|
58 "request" and seamlessly falling-back to using standard full-page reloads in |
|
59 older, less-capable browsers. |
|
60 |
|
61 The `PjaxBase` class isn't useful on its own, but can be mixed into a |
|
62 `Router`-based class to add Pjax functionality to that Router. For a pre-made |
|
63 standalone Pjax router, see the `Pjax` class. |
|
64 |
|
65 var MyRouter = Y.Base.create('myRouter', Y.Router, [Y.PjaxBase], { |
|
66 // ... |
|
67 }); |
|
68 |
|
69 @class PjaxBase |
|
70 @extensionfor Router |
|
71 @since 3.5.0 |
|
72 **/ |
|
73 function PjaxBase() {} |
|
74 |
|
75 PjaxBase.prototype = { |
|
76 // -- Protected Properties ------------------------------------------------- |
|
77 |
|
78 /** |
|
79 Holds the delegated pjax-link click handler. |
|
80 |
|
81 @property _pjaxEvents |
|
82 @type EventHandle |
|
83 @protected |
|
84 @since 3.5.0 |
|
85 **/ |
|
86 |
|
87 /** |
|
88 Regex used to break up a URL string around the URL's path. |
|
89 |
|
90 Subpattern captures: |
|
91 |
|
92 1. Origin, everything before the URL's path-part. |
|
93 2. The URL's path-part. |
|
94 3. Suffix, everything after the URL's path-part. |
|
95 |
|
96 @property _regexURL |
|
97 @type RegExp |
|
98 @protected |
|
99 @since 3.5.0 |
|
100 **/ |
|
101 _regexURL: /^((?:[^\/#?:]+:\/\/|\/\/)[^\/]*)?([^?#]*)(\?[^#]*)?(#.*)?$/, |
|
102 |
|
103 // -- Lifecycle Methods ---------------------------------------------------- |
|
104 initializer: function () { |
|
105 this.publish(EVT_NAVIGATE, {defaultFn: this._defNavigateFn}); |
|
106 |
|
107 // Pjax is all about progressively enhancing the navigation between |
|
108 // "pages", so by default we only want to handle and route link clicks |
|
109 // in HTML5 `pushState`-compatible browsers. |
|
110 if (this.get('html5')) { |
|
111 this._pjaxBindUI(); |
|
112 } |
|
113 }, |
|
114 |
|
115 destructor: function () { |
|
116 if (this._pjaxEvents) { |
|
117 this._pjaxEvents.detach(); |
|
118 } |
|
119 }, |
|
120 |
|
121 // -- Public Methods ------------------------------------------------------- |
|
122 |
|
123 /** |
|
124 Navigates to the specified URL if there is a route handler that matches. In |
|
125 browsers capable of using HTML5 history, the navigation will be enhanced by |
|
126 firing the `navigate` event and having the router handle the "request". |
|
127 Non-HTML5 browsers will navigate to the new URL via manipulation of |
|
128 `window.location`. |
|
129 |
|
130 When there is a route handler for the specified URL and it is being |
|
131 navigated to, this method will return `true`, otherwise it will return |
|
132 `false`. |
|
133 |
|
134 **Note:** The specified URL _must_ be of the same origin as the current URL, |
|
135 otherwise an error will be logged and navigation will not occur. This is |
|
136 intended as both a security constraint and a purposely imposed limitation as |
|
137 it does not make sense to tell the router to navigate to a URL on a |
|
138 different scheme, host, or port. |
|
139 |
|
140 @method navigate |
|
141 @param {String} url The URL to navigate to. This must be of the same origin |
|
142 as the current URL. |
|
143 @param {Object} [options] Additional options to configure the navigation. |
|
144 These are mixed into the `navigate` event facade. |
|
145 @param {Boolean} [options.replace] Whether or not the current history |
|
146 entry will be replaced, or a new entry will be created. Will default |
|
147 to `true` if the specified `url` is the same as the current URL. |
|
148 @param {Boolean} [options.force=false] Whether the enhanced navigation |
|
149 should occur even in browsers without HTML5 history. |
|
150 @return {Boolean} `true` if the URL was navigated to, `false` otherwise. |
|
151 @since 3.5.0 |
|
152 **/ |
|
153 navigate: function (url, options) { |
|
154 // The `_navigate()` method expects fully-resolved URLs. |
|
155 url = this._resolveURL(url); |
|
156 |
|
157 if (this._navigate(url, options)) { |
|
158 return true; |
|
159 } |
|
160 |
|
161 if (!this._hasSameOrigin(url)) { |
|
162 Y.error('Security error: The new URL must be of the same origin as the current URL.'); |
|
163 } |
|
164 |
|
165 return false; |
|
166 }, |
|
167 |
|
168 // -- Protected Methods ---------------------------------------------------- |
|
169 |
|
170 /** |
|
171 Utility method to test whether a specified link/anchor node's `href` is of |
|
172 the same origin as the page's current location. |
|
173 |
|
174 This normalize browser inconsistencies with how the `port` is reported for |
|
175 anchor elements (IE reports a value for the default port, e.g. "80"). |
|
176 |
|
177 @method _isLinkSameOrigin |
|
178 @param {Node} link The anchor element to test whether its `href` is of the |
|
179 same origin as the page's current location. |
|
180 @return {Boolean} Whether or not the link's `href` is of the same origin as |
|
181 the page's current location. |
|
182 @protected |
|
183 @since 3.6.0 |
|
184 **/ |
|
185 _isLinkSameOrigin: function (link) { |
|
186 var location = Y.getLocation(), |
|
187 protocol = location.protocol, |
|
188 hostname = location.hostname, |
|
189 port = parseInt(location.port, 10) || null, |
|
190 linkPort; |
|
191 |
|
192 // Link must have the same `protocol` and `hostname` as the page's |
|
193 // currrent location. |
|
194 if (link.get('protocol') !== protocol || |
|
195 link.get('hostname') !== hostname) { |
|
196 |
|
197 return false; |
|
198 } |
|
199 |
|
200 linkPort = parseInt(link.get('port'), 10) || null; |
|
201 |
|
202 // Normalize ports. In most cases browsers use an empty string when the |
|
203 // port is the default port, but IE does weird things with anchor |
|
204 // elements, so to be sure, this will re-assign the default ports before |
|
205 // they are compared. |
|
206 if (protocol === 'http:') { |
|
207 port || (port = 80); |
|
208 linkPort || (linkPort = 80); |
|
209 } else if (protocol === 'https:') { |
|
210 port || (port = 443); |
|
211 linkPort || (linkPort = 443); |
|
212 } |
|
213 |
|
214 // Finally, to be from the same origin, the link's `port` must match the |
|
215 // page's current `port`. |
|
216 return linkPort === port; |
|
217 }, |
|
218 |
|
219 /** |
|
220 Underlying implementation for `navigate()`. |
|
221 |
|
222 @method _navigate |
|
223 @param {String} url The fully-resolved URL that the router should dispatch |
|
224 to its route handlers to fulfill the enhanced navigation "request", or use |
|
225 to update `window.location` in non-HTML5 history capable browsers. |
|
226 @param {Object} [options] Additional options to configure the navigation. |
|
227 These are mixed into the `navigate` event facade. |
|
228 @param {Boolean} [options.replace] Whether or not the current history |
|
229 entry will be replaced, or a new entry will be created. Will default |
|
230 to `true` if the specified `url` is the same as the current URL. |
|
231 @param {Boolean} [options.force=false] Whether the enhanced navigation |
|
232 should occur even in browsers without HTML5 history. |
|
233 @return {Boolean} `true` if the URL was navigated to, `false` otherwise. |
|
234 @protected |
|
235 @since 3.5.0 |
|
236 **/ |
|
237 _navigate: function (url, options) { |
|
238 url = this._upgradeURL(url); |
|
239 |
|
240 // Navigation can only be enhanced if there is a route-handler. |
|
241 if (!this.hasRoute(url)) { |
|
242 return false; |
|
243 } |
|
244 |
|
245 // Make a copy of `options` before modifying it. |
|
246 options = Y.merge(options, {url: url}); |
|
247 |
|
248 var currentURL = this._getURL(), |
|
249 hash, hashlessURL; |
|
250 |
|
251 // Captures the `url`'s hash and returns a URL without that hash. |
|
252 hashlessURL = url.replace(/(#.*)$/, function (u, h, i) { |
|
253 hash = h; |
|
254 return u.substring(i); |
|
255 }); |
|
256 |
|
257 if (hash && hashlessURL === currentURL.replace(/#.*$/, '')) { |
|
258 // When the specified `url` and current URL only differ by the hash, |
|
259 // the browser should handle this in-page navigation normally. |
|
260 if (!this.get('navigateOnHash')) { |
|
261 return false; |
|
262 } |
|
263 |
|
264 options.hash = hash; |
|
265 } |
|
266 |
|
267 // When navigating to the same URL as the current URL, behave like a |
|
268 // browser and replace the history entry instead of creating a new one. |
|
269 'replace' in options || (options.replace = url === currentURL); |
|
270 |
|
271 // The `navigate` event will only fire and therefore enhance the |
|
272 // navigation to the new URL in HTML5 history enabled browsers or when |
|
273 // forced. Otherwise it will fallback to assigning or replacing the URL |
|
274 // on `window.location`. |
|
275 if (this.get('html5') || options.force) { |
|
276 this.fire(EVT_NAVIGATE, options); |
|
277 } else if (win) { |
|
278 if (options.replace) { |
|
279 win.location.replace(url); |
|
280 } else { |
|
281 win.location = url; |
|
282 } |
|
283 } |
|
284 |
|
285 return true; |
|
286 }, |
|
287 |
|
288 /** |
|
289 Binds the delegation of link-click events that match the `linkSelector` to |
|
290 the `_onLinkClick()` handler. |
|
291 |
|
292 By default this method will only be called if the browser is capable of |
|
293 using HTML5 history. |
|
294 |
|
295 @method _pjaxBindUI |
|
296 @protected |
|
297 @since 3.5.0 |
|
298 **/ |
|
299 _pjaxBindUI: function () { |
|
300 // Only bind link if we haven't already. |
|
301 if (!this._pjaxEvents) { |
|
302 this._pjaxEvents = Y.one('body').delegate('click', |
|
303 this._onLinkClick, this.get('linkSelector'), this); |
|
304 } |
|
305 }, |
|
306 |
|
307 // -- Protected Event Handlers --------------------------------------------- |
|
308 |
|
309 /** |
|
310 Default handler for the `navigate` event. |
|
311 |
|
312 Adds a new history entry or replaces the current entry for the specified URL |
|
313 and will scroll the page to the top if configured to do so. |
|
314 |
|
315 @method _defNavigateFn |
|
316 @param {EventFacade} e |
|
317 @protected |
|
318 @since 3.5.0 |
|
319 **/ |
|
320 _defNavigateFn: function (e) { |
|
321 this[e.replace ? 'replace' : 'save'](e.url); |
|
322 |
|
323 if (win && this.get('scrollToTop')) { |
|
324 // Scroll to the top of the page. The timeout ensures that the |
|
325 // scroll happens after navigation begins, so that the current |
|
326 // scroll position will be restored if the user clicks the back |
|
327 // button. |
|
328 setTimeout(function () { |
|
329 win.scroll(0, 0); |
|
330 }, 1); |
|
331 } |
|
332 }, |
|
333 |
|
334 /** |
|
335 Handler for delegated link-click events which match the `linkSelector`. |
|
336 |
|
337 This will attempt to enhance the navigation to the link element's `href` by |
|
338 passing the URL to the `_navigate()` method. When the navigation is being |
|
339 enhanced, the default action is prevented. |
|
340 |
|
341 If the user clicks a link with the middle/right mouse buttons, or is holding |
|
342 down the Ctrl or Command keys, this method's behavior is not applied and |
|
343 allows the native behavior to occur. Similarly, if the router is not capable |
|
344 or handling the URL because no route-handlers match, the link click will |
|
345 behave natively. |
|
346 |
|
347 @method _onLinkClick |
|
348 @param {EventFacade} e |
|
349 @protected |
|
350 @since 3.5.0 |
|
351 **/ |
|
352 _onLinkClick: function (e) { |
|
353 var link, url, navigated; |
|
354 |
|
355 // Allow the native behavior on middle/right-click, or when Ctrl or |
|
356 // Command are pressed. |
|
357 if (e.button !== 1 || e.ctrlKey || e.metaKey) { return; } |
|
358 |
|
359 link = e.currentTarget; |
|
360 |
|
361 // Only allow anchor elements because we need access to its `protocol`, |
|
362 // `host`, and `href` attributes. |
|
363 if (link.get('tagName').toUpperCase() !== 'A') { |
|
364 Y.log('pjax link-click navigation requires an anchor element.', 'warn', 'PjaxBase'); |
|
365 return; |
|
366 } |
|
367 |
|
368 // Same origin check to prevent trying to navigate to URLs from other |
|
369 // sites or things like mailto links. |
|
370 if (!this._isLinkSameOrigin(link)) { |
|
371 return; |
|
372 } |
|
373 |
|
374 // All browsers fully resolve an anchor's `href` property. |
|
375 url = link.get('href'); |
|
376 |
|
377 // Try and navigate to the URL via the router, and prevent the default |
|
378 // link-click action if we do. |
|
379 if (url) { |
|
380 navigated = this._navigate(url, {originEvent: e}); |
|
381 |
|
382 if (navigated) { |
|
383 e.preventDefault(); |
|
384 } |
|
385 } |
|
386 } |
|
387 }; |
|
388 |
|
389 PjaxBase.ATTRS = { |
|
390 /** |
|
391 CSS selector string used to filter link click events so that only the links |
|
392 which match it will have the enhanced navigation behavior of Pjax applied. |
|
393 |
|
394 When a link is clicked and that link matches this selector, Pjax will |
|
395 attempt to dispatch to any route handlers matching the link's `href` URL. If |
|
396 HTML5 history is not supported or if no route handlers match, the link click |
|
397 will be handled by the browser just like any old link. |
|
398 |
|
399 @attribute linkSelector |
|
400 @type String|Function |
|
401 @default "a.yui3-pjax" |
|
402 @initOnly |
|
403 @since 3.5.0 |
|
404 **/ |
|
405 linkSelector: { |
|
406 value : 'a.' + CLASS_PJAX, |
|
407 writeOnce: 'initOnly' |
|
408 }, |
|
409 |
|
410 /** |
|
411 Whether navigating to a hash-fragment identifier on the current page should |
|
412 be enhanced and cause the `navigate` event to fire. |
|
413 |
|
414 By default Pjax allows the browser to perform its default action when a user |
|
415 is navigating within a page by clicking in-page links |
|
416 (e.g. `<a href="#top">Top of page</a>`) and does not attempt to interfere or |
|
417 enhance in-page navigation. |
|
418 |
|
419 @attribute navigateOnHash |
|
420 @type Boolean |
|
421 @default false |
|
422 @since 3.5.0 |
|
423 **/ |
|
424 navigateOnHash: { |
|
425 value: false |
|
426 }, |
|
427 |
|
428 /** |
|
429 Whether the page should be scrolled to the top after navigating to a URL. |
|
430 |
|
431 When the user clicks the browser's back button, the previous scroll position |
|
432 will be maintained. |
|
433 |
|
434 @attribute scrollToTop |
|
435 @type Boolean |
|
436 @default true |
|
437 @since 3.5.0 |
|
438 **/ |
|
439 scrollToTop: { |
|
440 value: true |
|
441 } |
|
442 }; |
|
443 |
|
444 Y.PjaxBase = PjaxBase; |
|
445 |
|
446 |
|
447 }, '3.10.3', {"requires": ["classnamemanager", "node-event-delegate", "router"]}); |