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