| @@ -0,0 +1,838 @@ |
| @@ -0,0 +1,838 @@ |
| |
1
| +// jquery.pjax.js |
| |
2
| +// copyright chris wanstrath |
| |
3
| +// https://github.com/defunkt/jquery-pjax |
| |
4
| + |
| |
5
| +(function($){ |
| |
6
| + |
| |
7
| +// When called on a container with a selector, fetches the href with |
| |
8
| +// ajax into the container or with the data-pjax attribute on the link |
| |
9
| +// itself. |
| |
10
| +// |
| |
11
| +// Tries to make sure the back button and ctrl+click work the way |
| |
12
| +// you'd expect. |
| |
13
| +// |
| |
14
| +// Exported as $.fn.pjax |
| |
15
| +// |
| |
16
| +// Accepts a jQuery ajax options object that may include these |
| |
17
| +// pjax specific options: |
| |
18
| +// |
| |
19
| +// |
| |
20
| +// container - Where to stick the response body. Usually a String selector. |
| |
21
| +// $(container).html(xhr.responseBody) |
| |
22
| +// (default: current jquery context) |
| |
23
| +// push - Whether to pushState the URL. Defaults to true (of course). |
| |
24
| +// replace - Want to use replaceState instead? That's cool. |
| |
25
| +// |
| |
26
| +// For convenience the second parameter can be either the container or |
| |
27
| +// the options object. |
| |
28
| +// |
| |
29
| +// Returns the jQuery object |
| |
30
| +function fnPjax(selector, container, options) { |
| |
31
| + var context = this |
| |
32
| + return this.on('click.pjax', selector, function(event) { |
| |
33
| + var opts = $.extend({}, optionsFor(container, options)) |
| |
34
| + if (!opts.container) |
| |
35
| + opts.container = $(this).attr('data-pjax') || context |
| |
36
| + handleClick(event, opts) |
| |
37
| + }) |
| |
38
| +} |
| |
39
| + |
| |
40
| +// Public: pjax on click handler |
| |
41
| +// |
| |
42
| +// Exported as $.pjax.click. |
| |
43
| +// |
| |
44
| +// event - "click" jQuery.Event |
| |
45
| +// options - pjax options |
| |
46
| +// |
| |
47
| +// Examples |
| |
48
| +// |
| |
49
| +// $(document).on('click', 'a', $.pjax.click) |
| |
50
| +// // is the same as |
| |
51
| +// $(document).pjax('a') |
| |
52
| +// |
| |
53
| +// $(document).on('click', 'a', function(event) { |
| |
54
| +// var container = $(this).closest('[data-pjax-container]') |
| |
55
| +// $.pjax.click(event, container) |
| |
56
| +// }) |
| |
57
| +// |
| |
58
| +// Returns nothing. |
| |
59
| +function handleClick(event, container, options) { |
| |
60
| + options = optionsFor(container, options) |
| |
61
| + |
| |
62
| + var link = event.currentTarget |
| |
63
| + |
| |
64
| + if (link.tagName.toUpperCase() !== 'A') |
| |
65
| + throw "$.fn.pjax or $.pjax.click requires an anchor element" |
| |
66
| + |
| |
67
| + // Middle click, cmd click, and ctrl click should open |
| |
68
| + // links in a new tab as normal. |
| |
69
| + if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) |
| |
70
| + return |
| |
71
| + |
| |
72
| + // Ignore cross origin links |
| |
73
| + if ( location.protocol !== link.protocol || location.hostname !== link.hostname ) |
| |
74
| + return |
| |
75
| + |
| |
76
| + // Ignore anchors on the same page |
| |
77
| + if (link.hash && link.href.replace(link.hash, '') === |
| |
78
| + location.href.replace(location.hash, '')) |
| |
79
| + return |
| |
80
| + |
| |
81
| + // Ignore empty anchor "foo.html#" |
| |
82
| + if (link.href === location.href + '#') |
| |
83
| + return |
| |
84
| + |
| |
85
| + var defaults = { |
| |
86
| + url: link.href, |
| |
87
| + container: $(link).attr('data-pjax'), |
| |
88
| + target: link |
| |
89
| + } |
| |
90
| + |
| |
91
| + var opts = $.extend({}, defaults, options) |
| |
92
| + var clickEvent = $.Event('pjax:click') |
| |
93
| + $(link).trigger(clickEvent, [opts]) |
| |
94
| + |
| |
95
| + if (!clickEvent.isDefaultPrevented()) { |
| |
96
| + pjax(opts) |
| |
97
| + event.preventDefault() |
| |
98
| + } |
| |
99
| +} |
| |
100
| + |
| |
101
| +// Public: pjax on form submit handler |
| |
102
| +// |
| |
103
| +// Exported as $.pjax.submit |
| |
104
| +// |
| |
105
| +// event - "click" jQuery.Event |
| |
106
| +// options - pjax options |
| |
107
| +// |
| |
108
| +// Examples |
| |
109
| +// |
| |
110
| +// $(document).on('submit', 'form', function(event) { |
| |
111
| +// var container = $(this).closest('[data-pjax-container]') |
| |
112
| +// $.pjax.submit(event, container) |
| |
113
| +// }) |
| |
114
| +// |
| |
115
| +// Returns nothing. |
| |
116
| +function handleSubmit(event, container, options) { |
| |
117
| + options = optionsFor(container, options) |
| |
118
| + |
| |
119
| + var form = event.currentTarget |
| |
120
| + |
| |
121
| + if (form.tagName.toUpperCase() !== 'FORM') |
| |
122
| + throw "$.pjax.submit requires a form element" |
| |
123
| + |
| |
124
| + var defaults = { |
| |
125
| + type: form.method.toUpperCase(), |
| |
126
| + url: form.action, |
| |
127
| + data: $(form).serializeArray(), |
| |
128
| + container: $(form).attr('data-pjax'), |
| |
129
| + target: form |
| |
130
| + } |
| |
131
| + |
| |
132
| + pjax($.extend({}, defaults, options)) |
| |
133
| + |
| |
134
| + event.preventDefault() |
| |
135
| +} |
| |
136
| + |
| |
137
| +// Loads a URL with ajax, puts the response body inside a container, |
| |
138
| +// then pushState()'s the loaded URL. |
| |
139
| +// |
| |
140
| +// Works just like $.ajax in that it accepts a jQuery ajax |
| |
141
| +// settings object (with keys like url, type, data, etc). |
| |
142
| +// |
| |
143
| +// Accepts these extra keys: |
| |
144
| +// |
| |
145
| +// container - Where to stick the response body. |
| |
146
| +// $(container).html(xhr.responseBody) |
| |
147
| +// push - Whether to pushState the URL. Defaults to true (of course). |
| |
148
| +// replace - Want to use replaceState instead? That's cool. |
| |
149
| +// |
| |
150
| +// Use it just like $.ajax: |
| |
151
| +// |
| |
152
| +// var xhr = $.pjax({ url: this.href, container: '#main' }) |
| |
153
| +// console.log( xhr.readyState ) |
| |
154
| +// |
| |
155
| +// Returns whatever $.ajax returns. |
| |
156
| +function pjax(options) { |
| |
157
| + options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) |
| |
158
| + |
| |
159
| + if ($.isFunction(options.url)) { |
| |
160
| + options.url = options.url() |
| |
161
| + } |
| |
162
| + |
| |
163
| + var target = options.target |
| |
164
| + |
| |
165
| + var hash = parseURL(options.url).hash |
| |
166
| + |
| |
167
| + var context = options.context = findContainerFor(options.container) |
| |
168
| + |
| |
169
| + // We want the browser to maintain two separate internal caches: one |
| |
170
| + // for pjax'd partial page loads and one for normal page loads. |
| |
171
| + // Without adding this secret parameter, some browsers will often |
| |
172
| + // confuse the two. |
| |
173
| + if (!options.data) options.data = {} |
| |
174
| + options.data._pjax = context.selector |
| |
175
| + |
| |
176
| + function fire(type, args) { |
| |
177
| + var event = $.Event(type, { relatedTarget: target }) |
| |
178
| + context.trigger(event, args) |
| |
179
| + return !event.isDefaultPrevented() |
| |
180
| + } |
| |
181
| + |
| |
182
| + var timeoutTimer |
| |
183
| + |
| |
184
| + options.beforeSend = function(xhr, settings) { |
| |
185
| + // No timeout for non-GET requests |
| |
186
| + // Its not safe to request the resource again with a fallback method. |
| |
187
| + if (settings.type !== 'GET') { |
| |
188
| + settings.timeout = 0 |
| |
189
| + } |
| |
190
| + |
| |
191
| + xhr.setRequestHeader('X-PJAX', 'true') |
| |
192
| + xhr.setRequestHeader('X-PJAX-Container', context.selector) |
| |
193
| + |
| |
194
| + if (!fire('pjax:beforeSend', [xhr, settings])) |
| |
195
| + return false |
| |
196
| + |
| |
197
| + if (settings.timeout > 0) { |
| |
198
| + timeoutTimer = setTimeout(function() { |
| |
199
| + if (fire('pjax:timeout', [xhr, options])) |
| |
200
| + xhr.abort('timeout') |
| |
201
| + }, settings.timeout) |
| |
202
| + |
| |
203
| + // Clear timeout setting so jquerys internal timeout isn't invoked |
| |
204
| + settings.timeout = 0 |
| |
205
| + } |
| |
206
| + |
| |
207
| + options.requestUrl = parseURL(settings.url).href |
| |
208
| + } |
| |
209
| + |
| |
210
| + options.complete = function(xhr, textStatus) { |
| |
211
| + if (timeoutTimer) |
| |
212
| + clearTimeout(timeoutTimer) |
| |
213
| + |
| |
214
| + fire('pjax:complete', [xhr, textStatus, options]) |
| |
215
| + |
| |
216
| + fire('pjax:end', [xhr, options]) |
| |
217
| + } |
| |
218
| + |
| |
219
| + options.error = function(xhr, textStatus, errorThrown) { |
| |
220
| + var container = extractContainer("", xhr, options) |
| |
221
| + |
| |
222
| + var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) |
| |
223
| + if (options.type == 'GET' && textStatus !== 'abort' && allowed) { |
| |
224
| + locationReplace(container.url) |
| |
225
| + } |
| |
226
| + } |
| |
227
| + |
| |
228
| + options.success = function(data, status, xhr) { |
| |
229
| + // If $.pjax.defaults.version is a function, invoke it first. |
| |
230
| + // Otherwise it can be a static string. |
| |
231
| + var currentVersion = (typeof $.pjax.defaults.version === 'function') ? |
| |
232
| + $.pjax.defaults.version() : |
| |
233
| + $.pjax.defaults.version |
| |
234
| + |
| |
235
| + var latestVersion = xhr.getResponseHeader('X-PJAX-Version') |
| |
236
| + |
| |
237
| + var container = extractContainer(data, xhr, options) |
| |
238
| + |
| |
239
| + // If there is a layout version mismatch, hard load the new url |
| |
240
| + if (currentVersion && latestVersion && currentVersion !== latestVersion) { |
| |
241
| + locationReplace(container.url) |
| |
242
| + return |
| |
243
| + } |
| |
244
| + |
| |
245
| + // If the new response is missing a body, hard load the page |
| |
246
| + if (!container.contents) { |
| |
247
| + locationReplace(container.url) |
| |
248
| + return |
| |
249
| + } |
| |
250
| + |
| |
251
| + pjax.state = { |
| |
252
| + id: options.id || uniqueId(), |
| |
253
| + url: container.url, |
| |
254
| + title: container.title, |
| |
255
| + container: context.selector, |
| |
256
| + fragment: options.fragment, |
| |
257
| + timeout: options.timeout |
| |
258
| + } |
| |
259
| + |
| |
260
| + if (options.push || options.replace) { |
| |
261
| + window.history.replaceState(pjax.state, container.title, container.url) |
| |
262
| + } |
| |
263
| + |
| |
264
| + // Clear out any focused controls before inserting new page contents. |
| |
265
| + document.activeElement.blur() |
| |
266
| + |
| |
267
| + if (container.title) document.title = container.title |
| |
268
| + context.html(container.contents) |
| |
269
| + |
| |
270
| + // FF bug: Won't autofocus fields that are inserted via JS. |
| |
271
| + // This behavior is incorrect. So if theres no current focus, autofocus |
| |
272
| + // the last field. |
| |
273
| + // |
| |
274
| + // http://www.w3.org/html/wg/drafts/html/master/forms.html |
| |
275
| + var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0] |
| |
276
| + if (autofocusEl && document.activeElement !== autofocusEl) { |
| |
277
| + autofocusEl.focus(); |
| |
278
| + } |
| |
279
| + |
| |
280
| + executeScriptTags(container.scripts) |
| |
281
| + |
| |
282
| + // Scroll to top by default |
| |
283
| + if (typeof options.scrollTo === 'number') |
| |
284
| + $(window).scrollTop(options.scrollTo) |
| |
285
| + |
| |
286
| + // If the URL has a hash in it, make sure the browser |
| |
287
| + // knows to navigate to the hash. |
| |
288
| + if ( hash !== '' ) { |
| |
289
| + // Avoid using simple hash set here. Will add another history |
| |
290
| + // entry. Replace the url with replaceState and scroll to target |
| |
291
| + // by hand. |
| |
292
| + // |
| |
293
| + // window.location.hash = hash |
| |
294
| + var url = parseURL(container.url) |
| |
295
| + url.hash = hash |
| |
296
| + |
| |
297
| + pjax.state.url = url.href |
| |
298
| + window.history.replaceState(pjax.state, container.title, url.href) |
| |
299
| + |
| |
300
| + var target = $(url.hash) |
| |
301
| + if (target.length) $(window).scrollTop(target.offset().top) |
| |
302
| + } |
| |
303
| + |
| |
304
| + fire('pjax:success', [data, status, xhr, options]) |
| |
305
| + } |
| |
306
| + |
| |
307
| + |
| |
308
| + // Initialize pjax.state for the initial page load. Assume we're |
| |
309
| + // using the container and options of the link we're loading for the |
| |
310
| + // back button to the initial page. This ensures good back button |
| |
311
| + // behavior. |
| |
312
| + if (!pjax.state) { |
| |
313
| + pjax.state = { |
| |
314
| + id: uniqueId(), |
| |
315
| + url: window.location.href, |
| |
316
| + title: document.title, |
| |
317
| + container: context.selector, |
| |
318
| + fragment: options.fragment, |
| |
319
| + timeout: options.timeout |
| |
320
| + } |
| |
321
| + window.history.replaceState(pjax.state, document.title) |
| |
322
| + } |
| |
323
| + |
| |
324
| + // Cancel the current request if we're already pjaxing |
| |
325
| + var xhr = pjax.xhr |
| |
326
| + if ( xhr && xhr.readyState < 4) { |
| |
327
| + xhr.onreadystatechange = $.noop |
| |
328
| + xhr.abort() |
| |
329
| + } |
| |
330
| + |
| |
331
| + pjax.options = options |
| |
332
| + var xhr = pjax.xhr = $.ajax(options) |
| |
333
| + |
| |
334
| + if (xhr.readyState > 0) { |
| |
335
| + if (options.push && !options.replace) { |
| |
336
| + // Cache current container element before replacing it |
| |
337
| + cachePush(pjax.state.id, context.clone().contents()) |
| |
338
| + |
| |
339
| + window.history.pushState(null, "", stripPjaxParam(options.requestUrl)) |
| |
340
| + } |
| |
341
| + |
| |
342
| + fire('pjax:start', [xhr, options]) |
| |
343
| + fire('pjax:send', [xhr, options]) |
| |
344
| + } |
| |
345
| + |
| |
346
| + return pjax.xhr |
| |
347
| +} |
| |
348
| + |
| |
349
| +// Public: Reload current page with pjax. |
| |
350
| +// |
| |
351
| +// Returns whatever $.pjax returns. |
| |
352
| +function pjaxReload(container, options) { |
| |
353
| + var defaults = { |
| |
354
| + url: window.location.href, |
| |
355
| + push: false, |
| |
356
| + replace: true, |
| |
357
| + scrollTo: false |
| |
358
| + } |
| |
359
| + |
| |
360
| + return pjax($.extend(defaults, optionsFor(container, options))) |
| |
361
| +} |
| |
362
| + |
| |
363
| +// Internal: Hard replace current state with url. |
| |
364
| +// |
| |
365
| +// Work for around WebKit |
| |
366
| +// https://bugs.webkit.org/show_bug.cgi?id=93506 |
| |
367
| +// |
| |
368
| +// Returns nothing. |
| |
369
| +function locationReplace(url) { |
| |
370
| + window.history.replaceState(null, "", "#") |
| |
371
| + window.location.replace(url) |
| |
372
| +} |
| |
373
| + |
| |
374
| + |
| |
375
| +var initialPop = true |
| |
376
| +var initialURL = window.location.href |
| |
377
| +var initialState = window.history.state |
| |
378
| + |
| |
379
| +// Initialize $.pjax.state if possible |
| |
380
| +// Happens when reloading a page and coming forward from a different |
| |
381
| +// session history. |
| |
382
| +if (initialState && initialState.container) { |
| |
383
| + pjax.state = initialState |
| |
384
| +} |
| |
385
| + |
| |
386
| +// Non-webkit browsers don't fire an initial popstate event |
| |
387
| +if ('state' in window.history) { |
| |
388
| + initialPop = false |
| |
389
| +} |
| |
390
| + |
| |
391
| +// popstate handler takes care of the back and forward buttons |
| |
392
| +// |
| |
393
| +// You probably shouldn't use pjax on pages with other pushState |
| |
394
| +// stuff yet. |
| |
395
| +function onPjaxPopstate(event) { |
| |
396
| + var state = event.state |
| |
397
| + |
| |
398
| + if (state && state.container) { |
| |
399
| + // When coming forward from a separate history session, will get an |
| |
400
| + // initial pop with a state we are already at. Skip reloading the current |
| |
401
| + // page. |
| |
402
| + if (initialPop && initialURL == state.url) return |
| |
403
| + |
| |
404
| + // If popping back to the same state, just skip. |
| |
405
| + // Could be clicking back from hashchange rather than a pushState. |
| |
406
| + if (pjax.state.id === state.id) return |
| |
407
| + |
| |
408
| + var container = $(state.container) |
| |
409
| + if (container.length) { |
| |
410
| + var direction, contents = cacheMapping[state.id] |
| |
411
| + |
| |
412
| + if (pjax.state) { |
| |
413
| + // Since state ids always increase, we can deduce the history |
| |
414
| + // direction from the previous state. |
| |
415
| + direction = pjax.state.id < state.id ? 'forward' : 'back' |
| |
416
| + |
| |
417
| + // Cache current container before replacement and inform the |
| |
418
| + // cache which direction the history shifted. |
| |
419
| + cachePop(direction, pjax.state.id, container.clone().contents()) |
| |
420
| + } |
| |
421
| + |
| |
422
| + var popstateEvent = $.Event('pjax:popstate', { |
| |
423
| + state: state, |
| |
424
| + direction: direction |
| |
425
| + }) |
| |
426
| + container.trigger(popstateEvent) |
| |
427
| + |
| |
428
| + var options = { |
| |
429
| + id: state.id, |
| |
430
| + url: state.url, |
| |
431
| + container: container, |
| |
432
| + push: false, |
| |
433
| + fragment: state.fragment, |
| |
434
| + timeout: state.timeout, |
| |
435
| + scrollTo: false |
| |
436
| + } |
| |
437
| + |
| |
438
| + if (contents) { |
| |
439
| + container.trigger('pjax:start', [null, options]) |
| |
440
| + |
| |
441
| + if (state.title) document.title = state.title |
| |
442
| + container.html(contents) |
| |
443
| + pjax.state = state |
| |
444
| + |
| |
445
| + container.trigger('pjax:end', [null, options]) |
| |
446
| + } else { |
| |
447
| + pjax(options) |
| |
448
| + } |
| |
449
| + |
| |
450
| + // Force reflow/relayout before the browser tries to restore the |
| |
451
| + // scroll position. |
| |
452
| + container[0].offsetHeight |
| |
453
| + } else { |
| |
454
| + locationReplace(location.href) |
| |
455
| + } |
| |
456
| + } |
| |
457
| + initialPop = false |
| |
458
| +} |
| |
459
| + |
| |
460
| +// Fallback version of main pjax function for browsers that don't |
| |
461
| +// support pushState. |
| |
462
| +// |
| |
463
| +// Returns nothing since it retriggers a hard form submission. |
| |
464
| +function fallbackPjax(options) { |
| |
465
| + var url = $.isFunction(options.url) ? options.url() : options.url, |
| |
466
| + method = options.type ? options.type.toUpperCase() : 'GET' |
| |
467
| + |
| |
468
| + var form = $('<form>', { |
| |
469
| + method: method === 'GET' ? 'GET' : 'POST', |
| |
470
| + action: url, |
| |
471
| + style: 'display:none' |
| |
472
| + }) |
| |
473
| + |
| |
474
| + if (method !== 'GET' && method !== 'POST') { |
| |
475
| + form.append($('<input>', { |
| |
476
| + type: 'hidden', |
| |
477
| + name: '_method', |
| |
478
| + value: method.toLowerCase() |
| |
479
| + })) |
| |
480
| + } |
| |
481
| + |
| |
482
| + var data = options.data |
| |
483
| + if (typeof data === 'string') { |
| |
484
| + $.each(data.split('&'), function(index, value) { |
| |
485
| + var pair = value.split('=') |
| |
486
| + form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]})) |
| |
487
| + }) |
| |
488
| + } else if (typeof data === 'object') { |
| |
489
| + for (key in data) |
| |
490
| + form.append($('<input>', {type: 'hidden', name: key, value: data[key]})) |
| |
491
| + } |
| |
492
| + |
| |
493
| + $(document.body).append(form) |
| |
494
| + form.submit() |
| |
495
| +} |
| |
496
| + |
| |
497
| +// Internal: Generate unique id for state object. |
| |
498
| +// |
| |
499
| +// Use a timestamp instead of a counter since ids should still be |
| |
500
| +// unique across page loads. |
| |
501
| +// |
| |
502
| +// Returns Number. |
| |
503
| +function uniqueId() { |
| |
504
| + return (new Date).getTime() |
| |
505
| +} |
| |
506
| + |
| |
507
| +// Internal: Strips _pjax param from url |
| |
508
| +// |
| |
509
| +// url - String |
| |
510
| +// |
| |
511
| +// Returns String. |
| |
512
| +function stripPjaxParam(url) { |
| |
513
| + return url |
| |
514
| + .replace(/\?_pjax=[^&]+&?/, '?') |
| |
515
| + .replace(/_pjax=[^&]+&?/, '') |
| |
516
| + .replace(/[\?&]$/, '') |
| |
517
| +} |
| |
518
| + |
| |
519
| +// Internal: Parse URL components and returns a Locationish object. |
| |
520
| +// |
| |
521
| +// url - String URL |
| |
522
| +// |
| |
523
| +// Returns HTMLAnchorElement that acts like Location. |
| |
524
| +function parseURL(url) { |
| |
525
| + var a = document.createElement('a') |
| |
526
| + a.href = url |
| |
527
| + return a |
| |
528
| +} |
| |
529
| + |
| |
530
| +// Internal: Build options Object for arguments. |
| |
531
| +// |
| |
532
| +// For convenience the first parameter can be either the container or |
| |
533
| +// the options object. |
| |
534
| +// |
| |
535
| +// Examples |
| |
536
| +// |
| |
537
| +// optionsFor('#container') |
| |
538
| +// // => {container: '#container'} |
| |
539
| +// |
| |
540
| +// optionsFor('#container', {push: true}) |
| |
541
| +// // => {container: '#container', push: true} |
| |
542
| +// |
| |
543
| +// optionsFor({container: '#container', push: true}) |
| |
544
| +// // => {container: '#container', push: true} |
| |
545
| +// |
| |
546
| +// Returns options Object. |
| |
547
| +function optionsFor(container, options) { |
| |
548
| + // Both container and options |
| |
549
| + if ( container && options ) |
| |
550
| + options.container = container |
| |
551
| + |
| |
552
| + // First argument is options Object |
| |
553
| + else if ( $.isPlainObject(container) ) |
| |
554
| + options = container |
| |
555
| + |
| |
556
| + // Only container |
| |
557
| + else |
| |
558
| + options = {container: container} |
| |
559
| + |
| |
560
| + // Find and validate container |
| |
561
| + if (options.container) |
| |
562
| + options.container = findContainerFor(options.container) |
| |
563
| + |
| |
564
| + return options |
| |
565
| +} |
| |
566
| + |
| |
567
| +// Internal: Find container element for a variety of inputs. |
| |
568
| +// |
| |
569
| +// Because we can't persist elements using the history API, we must be |
| |
570
| +// able to find a String selector that will consistently find the Element. |
| |
571
| +// |
| |
572
| +// container - A selector String, jQuery object, or DOM Element. |
| |
573
| +// |
| |
574
| +// Returns a jQuery object whose context is `document` and has a selector. |
| |
575
| +function findContainerFor(container) { |
| |
576
| + container = $(container) |
| |
577
| + |
| |
578
| + if ( !container.length ) { |
| |
579
| + throw "no pjax container for " + container.selector |
| |
580
| + } else if ( container.selector !== '' && container.context === document ) { |
| |
581
| + return container |
| |
582
| + } else if ( container.attr('id') ) { |
| |
583
| + return $('#' + container.attr('id')) |
| |
584
| + } else { |
| |
585
| + throw "cant get selector for pjax container!" |
| |
586
| + } |
| |
587
| +} |
| |
588
| + |
| |
589
| +// Internal: Filter and find all elements matching the selector. |
| |
590
| +// |
| |
591
| +// Where $.fn.find only matches descendants, findAll will test all the |
| |
592
| +// top level elements in the jQuery object as well. |
| |
593
| +// |
| |
594
| +// elems - jQuery object of Elements |
| |
595
| +// selector - String selector to match |
| |
596
| +// |
| |
597
| +// Returns a jQuery object. |
| |
598
| +function findAll(elems, selector) { |
| |
599
| + return elems.filter(selector).add(elems.find(selector)); |
| |
600
| +} |
| |
601
| + |
| |
602
| +function parseHTML(html) { |
| |
603
| + return $.parseHTML(html, document, true) |
| |
604
| +} |
| |
605
| + |
| |
606
| +// Internal: Extracts container and metadata from response. |
| |
607
| +// |
| |
608
| +// 1. Extracts X-PJAX-URL header if set |
| |
609
| +// 2. Extracts inline <title> tags |
| |
610
| +// 3. Builds response Element and extracts fragment if set |
| |
611
| +// |
| |
612
| +// data - String response data |
| |
613
| +// xhr - XHR response |
| |
614
| +// options - pjax options Object |
| |
615
| +// |
| |
616
| +// Returns an Object with url, title, and contents keys. |
| |
617
| +function extractContainer(data, xhr, options) { |
| |
618
| + var obj = {} |
| |
619
| + |
| |
620
| + // Prefer X-PJAX-URL header if it was set, otherwise fallback to |
| |
621
| + // using the original requested url. |
| |
622
| + obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl) |
| |
623
| + |
| |
624
| + // Attempt to parse response html into elements |
| |
625
| + if (/<html/i.test(data)) { |
| |
626
| + var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0])) |
| |
627
| + var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])) |
| |
628
| + } else { |
| |
629
| + var $head = $body = $(parseHTML(data)) |
| |
630
| + } |
| |
631
| + |
| |
632
| + // If response data is empty, return fast |
| |
633
| + if ($body.length === 0) |
| |
634
| + return obj |
| |
635
| + |
| |
636
| + // If there's a <title> tag in the header, use it as |
| |
637
| + // the page's title. |
| |
638
| + obj.title = findAll($head, 'title').last().text() |
| |
639
| + |
| |
640
| + if (options.fragment) { |
| |
641
| + // If they specified a fragment, look for it in the response |
| |
642
| + // and pull it out. |
| |
643
| + if (options.fragment === 'body') { |
| |
644
| + var $fragment = $body |
| |
645
| + } else { |
| |
646
| + var $fragment = findAll($body, options.fragment).first() |
| |
647
| + } |
| |
648
| + |
| |
649
| + if ($fragment.length) { |
| |
650
| + obj.contents = $fragment.contents() |
| |
651
| + |
| |
652
| + // If there's no title, look for data-title and title attributes |
| |
653
| + // on the fragment |
| |
654
| + if (!obj.title) |
| |
655
| + obj.title = $fragment.attr('title') || $fragment.data('title') |
| |
656
| + } |
| |
657
| + |
| |
658
| + } else if (!/<html/i.test(data)) { |
| |
659
| + obj.contents = $body |
| |
660
| + } |
| |
661
| + |
| |
662
| + // Clean up any <title> tags |
| |
663
| + if (obj.contents) { |
| |
664
| + // Remove any parent title elements |
| |
665
| + obj.contents = obj.contents.not(function() { return $(this).is('title') }) |
| |
666
| + |
| |
667
| + // Then scrub any titles from their descendants |
| |
668
| + obj.contents.find('title').remove() |
| |
669
| + |
| |
670
| + // Gather all script[src] elements |
| |
671
| + //obj.scripts = findAll(obj.contents, 'script[src]').remove() |
| |
672
| + obj.contents = obj.contents.not(obj.scripts) |
| |
673
| + } |
| |
674
| + |
| |
675
| + // Trim any whitespace off the title |
| |
676
| + if (obj.title) obj.title = $.trim(obj.title) |
| |
677
| + |
| |
678
| + return obj |
| |
679
| +} |
| |
680
| + |
| |
681
| +// Load an execute scripts using standard script request. |
| |
682
| +// |
| |
683
| +// Avoids jQuery's traditional $.getScript which does a XHR request and |
| |
684
| +// globalEval. |
| |
685
| +// |
| |
686
| +// scripts - jQuery object of script Elements |
| |
687
| +// |
| |
688
| +// Returns nothing. |
| |
689
| +function executeScriptTags(scripts) { |
| |
690
| + if (!scripts) return |
| |
691
| + |
| |
692
| + var existingScripts = $('script[src]') |
| |
693
| + |
| |
694
| + scripts.each(function() { |
| |
695
| + var src = this.src |
| |
696
| + var matchedScripts = existingScripts.filter(function() { |
| |
697
| + return this.src === src |
| |
698
| + }) |
| |
699
| + if (matchedScripts.length) return |
| |
700
| + |
| |
701
| + var script = document.createElement('script') |
| |
702
| + script.type = $(this).attr('type') |
| |
703
| + script.src = $(this).attr('src') |
| |
704
| + document.head.appendChild(script) |
| |
705
| + }) |
| |
706
| +} |
| |
707
| + |
| |
708
| +// Internal: History DOM caching class. |
| |
709
| +var cacheMapping = {} |
| |
710
| +var cacheForwardStack = [] |
| |
711
| +var cacheBackStack = [] |
| |
712
| + |
| |
713
| +// Push previous state id and container contents into the history |
| |
714
| +// cache. Should be called in conjunction with `pushState` to save the |
| |
715
| +// previous container contents. |
| |
716
| +// |
| |
717
| +// id - State ID Number |
| |
718
| +// value - DOM Element to cache |
| |
719
| +// |
| |
720
| +// Returns nothing. |
| |
721
| +function cachePush(id, value) { |
| |
722
| + cacheMapping[id] = value |
| |
723
| + cacheBackStack.push(id) |
| |
724
| + |
| |
725
| + // Remove all entires in forward history stack after pushing |
| |
726
| + // a new page. |
| |
727
| + while (cacheForwardStack.length) |
| |
728
| + delete cacheMapping[cacheForwardStack.shift()] |
| |
729
| + |
| |
730
| + // Trim back history stack to max cache length. |
| |
731
| + while (cacheBackStack.length > pjax.defaults.maxCacheLength) |
| |
732
| + delete cacheMapping[cacheBackStack.shift()] |
| |
733
| +} |
| |
734
| + |
| |
735
| +// Shifts cache from directional history cache. Should be |
| |
736
| +// called on `popstate` with the previous state id and container |
| |
737
| +// contents. |
| |
738
| +// |
| |
739
| +// direction - "forward" or "back" String |
| |
740
| +// id - State ID Number |
| |
741
| +// value - DOM Element to cache |
| |
742
| +// |
| |
743
| +// Returns nothing. |
| |
744
| +function cachePop(direction, id, value) { |
| |
745
| + var pushStack, popStack |
| |
746
| + cacheMapping[id] = value |
| |
747
| + |
| |
748
| + if (direction === 'forward') { |
| |
749
| + pushStack = cacheBackStack |
| |
750
| + popStack = cacheForwardStack |
| |
751
| + } else { |
| |
752
| + pushStack = cacheForwardStack |
| |
753
| + popStack = cacheBackStack |
| |
754
| + } |
| |
755
| + |
| |
756
| + pushStack.push(id) |
| |
757
| + if (id = popStack.pop()) |
| |
758
| + delete cacheMapping[id] |
| |
759
| +} |
| |
760
| + |
| |
761
| +// Public: Find version identifier for the initial page load. |
| |
762
| +// |
| |
763
| +// Returns String version or undefined. |
| |
764
| +function findVersion() { |
| |
765
| + return $('meta').filter(function() { |
| |
766
| + var name = $(this).attr('http-equiv') |
| |
767
| + return name && name.toUpperCase() === 'X-PJAX-VERSION' |
| |
768
| + }).attr('content') |
| |
769
| +} |
| |
770
| + |
| |
771
| +// Install pjax functions on $.pjax to enable pushState behavior. |
| |
772
| +// |
| |
773
| +// Does nothing if already enabled. |
| |
774
| +// |
| |
775
| +// Examples |
| |
776
| +// |
| |
777
| +// $.pjax.enable() |
| |
778
| +// |
| |
779
| +// Returns nothing. |
| |
780
| +function enable() { |
| |
781
| + $.fn.pjax = fnPjax |
| |
782
| + $.pjax = pjax |
| |
783
| + $.pjax.enable = $.noop |
| |
784
| + $.pjax.disable = disable |
| |
785
| + $.pjax.click = handleClick |
| |
786
| + $.pjax.submit = handleSubmit |
| |
787
| + $.pjax.reload = pjaxReload |
| |
788
| + $.pjax.defaults = { |
| |
789
| + timeout: 650, |
| |
790
| + push: true, |
| |
791
| + replace: false, |
| |
792
| + type: 'GET', |
| |
793
| + dataType: 'html', |
| |
794
| + scrollTo: 0, |
| |
795
| + maxCacheLength: 20, |
| |
796
| + version: findVersion |
| |
797
| + } |
| |
798
| + $(window).on('popstate.pjax', onPjaxPopstate) |
| |
799
| +} |
| |
800
| + |
| |
801
| +// Disable pushState behavior. |
| |
802
| +// |
| |
803
| +// This is the case when a browser doesn't support pushState. It is |
| |
804
| +// sometimes useful to disable pushState for debugging on a modern |
| |
805
| +// browser. |
| |
806
| +// |
| |
807
| +// Examples |
| |
808
| +// |
| |
809
| +// $.pjax.disable() |
| |
810
| +// |
| |
811
| +// Returns nothing. |
| |
812
| +function disable() { |
| |
813
| + $.fn.pjax = function() { return this } |
| |
814
| + $.pjax = fallbackPjax |
| |
815
| + $.pjax.enable = enable |
| |
816
| + $.pjax.disable = $.noop |
| |
817
| + $.pjax.click = $.noop |
| |
818
| + $.pjax.submit = $.noop |
| |
819
| + $.pjax.reload = function() { window.location.reload() } |
| |
820
| + |
| |
821
| + $(window).off('popstate.pjax', onPjaxPopstate) |
| |
822
| +} |
| |
823
| + |
| |
824
| + |
| |
825
| +// Add the state property to jQuery's event object so we can use it in |
| |
826
| +// $(window).bind('popstate') |
| |
827
| +if ( $.inArray('state', $.event.props) < 0 ) |
| |
828
| + $.event.props.push('state') |
| |
829
| + |
| |
830
| +// Is pjax supported by this browser? |
| |
831
| +$.support.pjax = |
| |
832
| + window.history && window.history.pushState && window.history.replaceState && |
| |
833
| + // pushState isn't reliable on iOS until 5. |
| |
834
| + !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) |
| |
835
| + |
| |
836
| +$.support.pjax ? enable() : disable() |
| |
837
| + |
| |
838
| +})(jQuery); |