rails_admin 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rails_admin might be problematic. Click here for more details.

Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/rails_admin/jquery.migrate.js +3 -0
  3. data/app/assets/javascripts/rails_admin/ra.filter-box.js +1 -4
  4. data/app/assets/javascripts/rails_admin/ra.nested-form-hooks.js +3 -3
  5. data/app/assets/javascripts/rails_admin/ra.widgets.js +1 -1
  6. data/app/assets/javascripts/rails_admin/rails_admin.js +2 -1
  7. data/app/assets/javascripts/rails_admin/ui.js +2 -2
  8. data/app/helpers/rails_admin/form_builder.rb +2 -0
  9. data/lib/rails_admin/extensions/paper_trail/auditing_adapter.rb +1 -1
  10. data/lib/rails_admin/version.rb +2 -2
  11. data/vendor/assets/javascripts/rails_admin/bootstrap-datetimepicker.js +317 -150
  12. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-affix.js +50 -28
  13. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-alert.js +10 -7
  14. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-button.js +35 -20
  15. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-carousel.js +48 -25
  16. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-collapse.js +70 -28
  17. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-dropdown.js +56 -42
  18. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-modal.js +118 -40
  19. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-popover.js +26 -16
  20. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-scrollspy.js +29 -27
  21. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-tab.js +46 -19
  22. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-tooltip.js +280 -60
  23. data/vendor/assets/javascripts/rails_admin/bootstrap/bootstrap-transition.js +5 -5
  24. data/vendor/assets/javascripts/rails_admin/jquery.pjax.js +317 -160
  25. data/vendor/assets/javascripts/rails_admin/moment-with-locales.js +11210 -9290
  26. metadata +3 -3
  27. data/config/initializers/devise_patch.rb +0 -9
@@ -1,8 +1,8 @@
1
1
  /* ========================================================================
2
- * Bootstrap: transition.js v3.2.0
3
- * http://getbootstrap.com/javascript/#transitions
2
+ * Bootstrap: transition.js v3.4.1
3
+ * https://getbootstrap.com/docs/3.4/javascript/#transitions
4
4
  * ========================================================================
5
- * Copyright 2011-2014 Twitter, Inc.
5
+ * Copyright 2011-2019 Twitter, Inc.
6
6
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
7
7
  * ======================================================================== */
8
8
 
@@ -10,7 +10,7 @@
10
10
  +function ($) {
11
11
  'use strict';
12
12
 
13
- // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
13
+ // CSS TRANSITION SUPPORT (Shoutout: https://modernizr.com/)
14
14
  // ============================================================
15
15
 
16
16
  function transitionEnd() {
@@ -32,7 +32,7 @@
32
32
  return false // explicit for ie8 ( ._.)
33
33
  }
34
34
 
35
- // http://blog.alexmaccaw.com/css-transitions
35
+ // https://blog.alexmaccaw.com/css-transitions
36
36
  $.fn.emulateTransitionEnd = function (duration) {
37
37
  var called = false
38
38
  var $el = this
@@ -1,6 +1,8 @@
1
- // jquery.pjax.js
2
- // copyright chris wanstrath
3
- // https://github.com/defunkt/jquery-pjax
1
+ /*!
2
+ * Copyright 2012, Chris Wanstrath
3
+ * Released under the MIT License
4
+ * https://github.com/defunkt/jquery-pjax
5
+ */
4
6
 
5
7
  (function($){
6
8
 
@@ -17,9 +19,7 @@
17
19
  // pjax specific options:
18
20
  //
19
21
  //
20
- // container - Where to stick the response body. Usually a String selector.
21
- // $(container).html(xhr.responseBody)
22
- // (default: current jquery context)
22
+ // container - String selector for the element where to place the response body.
23
23
  // push - Whether to pushState the URL. Defaults to true (of course).
24
24
  // replace - Want to use replaceState instead? That's cool.
25
25
  //
@@ -28,11 +28,13 @@
28
28
  //
29
29
  // Returns the jQuery object
30
30
  function fnPjax(selector, container, options) {
31
- var context = this
31
+ options = optionsFor(container, options)
32
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
33
+ var opts = options
34
+ if (!opts.container) {
35
+ opts = $.extend({}, options)
36
+ opts.container = $(this).attr('data-pjax')
37
+ }
36
38
  handleClick(event, opts)
37
39
  })
38
40
  }
@@ -50,16 +52,12 @@ function fnPjax(selector, container, options) {
50
52
  // // is the same as
51
53
  // $(document).pjax('a')
52
54
  //
53
- // $(document).on('click', 'a', function(event) {
54
- // var container = $(this).closest('[data-pjax-container]')
55
- // $.pjax.click(event, container)
56
- // })
57
- //
58
55
  // Returns nothing.
59
56
  function handleClick(event, container, options) {
60
57
  options = optionsFor(container, options)
61
58
 
62
59
  var link = event.currentTarget
60
+ var $link = $(link)
63
61
 
64
62
  if (link.tagName.toUpperCase() !== 'A')
65
63
  throw "$.fn.pjax or $.pjax.click requires an anchor element"
@@ -70,28 +68,32 @@ function handleClick(event, container, options) {
70
68
  return
71
69
 
72
70
  // Ignore cross origin links
73
- if ( location.protocol !== link.protocol || location.host !== link.host )
71
+ if ( location.protocol !== link.protocol || location.hostname !== link.hostname )
74
72
  return
75
73
 
76
- // Ignore anchors on the same page
77
- if (link.hash && link.href.replace(link.hash, '') ===
78
- location.href.replace(location.hash, ''))
74
+ // Ignore case when a hash is being tacked on the current URL
75
+ if ( link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location) )
79
76
  return
80
77
 
81
- // Ignore empty anchor "foo.html#"
82
- if (link.href === location.href + '#')
78
+ // Ignore event with default prevented
79
+ if (event.isDefaultPrevented())
83
80
  return
84
81
 
85
82
  var defaults = {
86
83
  url: link.href,
87
- container: $(link).attr('data-pjax'),
88
- target: link,
89
- fragment: null
84
+ container: $link.attr('data-pjax'),
85
+ target: link
90
86
  }
91
87
 
92
- pjax($.extend({}, defaults, options))
88
+ var opts = $.extend({}, defaults, options)
89
+ var clickEvent = $.Event('pjax:click')
90
+ $link.trigger(clickEvent, [opts])
93
91
 
94
- event.preventDefault()
92
+ if (!clickEvent.isDefaultPrevented()) {
93
+ pjax(opts)
94
+ event.preventDefault()
95
+ $link.trigger('pjax:clicked', [opts])
96
+ }
95
97
  }
96
98
 
97
99
  // Public: pjax on form submit handler
@@ -104,8 +106,7 @@ function handleClick(event, container, options) {
104
106
  // Examples
105
107
  //
106
108
  // $(document).on('submit', 'form', function(event) {
107
- // var container = $(this).closest('[data-pjax-container]')
108
- // $.pjax.submit(event, container)
109
+ // $.pjax.submit(event, '[data-pjax-container]')
109
110
  // })
110
111
  //
111
112
  // Returns nothing.
@@ -113,17 +114,30 @@ function handleSubmit(event, container, options) {
113
114
  options = optionsFor(container, options)
114
115
 
115
116
  var form = event.currentTarget
117
+ var $form = $(form)
116
118
 
117
119
  if (form.tagName.toUpperCase() !== 'FORM')
118
120
  throw "$.pjax.submit requires a form element"
119
121
 
120
122
  var defaults = {
121
- type: form.method,
122
- url: form.action,
123
- data: $(form).serializeArray(),
124
- container: $(form).attr('data-pjax'),
125
- target: form,
126
- fragment: null
123
+ type: ($form.attr('method') || 'GET').toUpperCase(),
124
+ url: $form.attr('action'),
125
+ container: $form.attr('data-pjax'),
126
+ target: form
127
+ }
128
+
129
+ if (defaults.type !== 'GET' && window.FormData !== undefined) {
130
+ defaults.data = new FormData(form)
131
+ defaults.processData = false
132
+ defaults.contentType = false
133
+ } else {
134
+ // Can't handle file uploads, exit
135
+ if ($form.find(':file').length) {
136
+ return
137
+ }
138
+
139
+ // Fallback to manually serializing the fields
140
+ defaults.data = $form.serializeArray()
127
141
  }
128
142
 
129
143
  pjax($.extend({}, defaults, options))
@@ -139,8 +153,7 @@ function handleSubmit(event, container, options) {
139
153
  //
140
154
  // Accepts these extra keys:
141
155
  //
142
- // container - Where to stick the response body.
143
- // $(container).html(xhr.responseBody)
156
+ // container - String selector for where to stick the response body.
144
157
  // push - Whether to pushState the URL. Defaults to true (of course).
145
158
  // replace - Want to use replaceState instead? That's cool.
146
159
  //
@@ -157,21 +170,32 @@ function pjax(options) {
157
170
  options.url = options.url()
158
171
  }
159
172
 
160
- var target = options.target
161
-
162
173
  var hash = parseURL(options.url).hash
163
174
 
164
- var context = options.context = findContainerFor(options.container)
175
+ var containerType = $.type(options.container)
176
+ if (containerType !== 'string') {
177
+ throw "expected string value for 'container' option; got " + containerType
178
+ }
179
+ var context = options.context = $(options.container)
180
+ if (!context.length) {
181
+ throw "the container selector '" + options.container + "' did not match anything"
182
+ }
165
183
 
166
184
  // We want the browser to maintain two separate internal caches: one
167
185
  // for pjax'd partial page loads and one for normal page loads.
168
186
  // Without adding this secret parameter, some browsers will often
169
187
  // confuse the two.
170
188
  if (!options.data) options.data = {}
171
- options.data._pjax = context.selector
189
+ if ($.isArray(options.data)) {
190
+ options.data.push({name: '_pjax', value: options.container})
191
+ } else {
192
+ options.data._pjax = options.container
193
+ }
172
194
 
173
- function fire(type, args) {
174
- var event = $.Event(type, { relatedTarget: target })
195
+ function fire(type, args, props) {
196
+ if (!props) props = {}
197
+ props.relatedTarget = options.target
198
+ var event = $.Event(type, props)
175
199
  context.trigger(event, args)
176
200
  return !event.isDefaultPrevented()
177
201
  }
@@ -186,7 +210,7 @@ function pjax(options) {
186
210
  }
187
211
 
188
212
  xhr.setRequestHeader('X-PJAX', 'true')
189
- xhr.setRequestHeader('X-PJAX-Container', context.selector)
213
+ xhr.setRequestHeader('X-PJAX-Container', options.container)
190
214
 
191
215
  if (!fire('pjax:beforeSend', [xhr, settings]))
192
216
  return false
@@ -201,7 +225,9 @@ function pjax(options) {
201
225
  settings.timeout = 0
202
226
  }
203
227
 
204
- options.requestUrl = parseURL(settings.url).href
228
+ var url = parseURL(settings.url)
229
+ if (hash) url.hash = hash
230
+ options.requestUrl = stripInternalParams(url)
205
231
  }
206
232
 
207
233
  options.complete = function(xhr, textStatus) {
@@ -223,8 +249,31 @@ function pjax(options) {
223
249
  }
224
250
 
225
251
  options.success = function(data, status, xhr) {
252
+ var previousState = pjax.state
253
+
254
+ // If $.pjax.defaults.version is a function, invoke it first.
255
+ // Otherwise it can be a static string.
256
+ var currentVersion = (typeof $.pjax.defaults.version === 'function') ?
257
+ $.pjax.defaults.version() :
258
+ $.pjax.defaults.version
259
+
260
+ var latestVersion = xhr.getResponseHeader('X-PJAX-Version')
261
+
226
262
  var container = extractContainer(data, xhr, options)
227
263
 
264
+ var url = parseURL(container.url)
265
+ if (hash) {
266
+ url.hash = hash
267
+ container.url = url.href
268
+ }
269
+
270
+ // If there is a layout version mismatch, hard load the new url
271
+ if (currentVersion && latestVersion && currentVersion !== latestVersion) {
272
+ locationReplace(container.url)
273
+ return
274
+ }
275
+
276
+ // If the new response is missing a body, hard load the page
228
277
  if (!container.contents) {
229
278
  locationReplace(container.url)
230
279
  return
@@ -234,7 +283,7 @@ function pjax(options) {
234
283
  id: options.id || uniqueId(),
235
284
  url: container.url,
236
285
  title: container.title,
237
- container: context.selector,
286
+ container: options.container,
238
287
  fragment: options.fragment,
239
288
  timeout: options.timeout
240
289
  }
@@ -243,35 +292,47 @@ function pjax(options) {
243
292
  window.history.replaceState(pjax.state, container.title, container.url)
244
293
  }
245
294
 
295
+ // Only blur the focus if the focused element is within the container.
296
+ var blurFocus = $.contains(options.container, document.activeElement)
297
+
298
+ // Clear out any focused controls before inserting new page contents.
299
+ if (blurFocus) {
300
+ try {
301
+ document.activeElement.blur()
302
+ } catch (e) { }
303
+ }
304
+
246
305
  if (container.title) document.title = container.title
306
+
307
+ fire('pjax:beforeReplace', [container.contents, options], {
308
+ state: pjax.state,
309
+ previousState: previousState
310
+ })
247
311
  context.html(container.contents)
248
312
 
249
- // Scroll to top by default
250
- if (typeof options.scrollTo === 'number')
251
- $(window).scrollTop(options.scrollTo)
252
-
253
- // Google Analytics support
254
- if ( (options.replace || options.push) && window._gaq )
255
- _gaq.push(['_trackPageview'])
256
-
257
- // If the URL has a hash in it, make sure the browser
258
- // knows to navigate to the hash.
259
- if ( hash !== '' ) {
260
- // Avoid using simple hash set here. Will add another history
261
- // entry. Replace the url with replaceState and scroll to target
262
- // by hand.
263
- //
264
- // window.location.hash = hash
265
- var url = parseURL(container.url)
266
- url.hash = hash
313
+ // FF bug: Won't autofocus fields that are inserted via JS.
314
+ // This behavior is incorrect. So if theres no current focus, autofocus
315
+ // the last field.
316
+ //
317
+ // http://www.w3.org/html/wg/drafts/html/master/forms.html
318
+ var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0]
319
+ if (autofocusEl && document.activeElement !== autofocusEl) {
320
+ autofocusEl.focus()
321
+ }
322
+
323
+ executeScriptTags(container.scripts)
267
324
 
268
- pjax.state.url = url.href
269
- window.history.replaceState(pjax.state, container.title, url.href)
325
+ var scrollTo = options.scrollTo
270
326
 
271
- var target = $(url.hash)
272
- if (target.length) $(window).scrollTop(target.offset().top)
327
+ // Ensure browser scrolls to the element referenced by the URL anchor
328
+ if (hash) {
329
+ var name = decodeURIComponent(hash.slice(1))
330
+ var target = document.getElementById(name) || document.getElementsByName(name)[0]
331
+ if (target) scrollTo = $(target).offset().top
273
332
  }
274
333
 
334
+ if (typeof scrollTo == 'number') $(window).scrollTop(scrollTo)
335
+
275
336
  fire('pjax:success', [data, status, xhr, options])
276
337
  }
277
338
 
@@ -285,7 +346,7 @@ function pjax(options) {
285
346
  id: uniqueId(),
286
347
  url: window.location.href,
287
348
  title: document.title,
288
- container: context.selector,
349
+ container: options.container,
289
350
  fragment: options.fragment,
290
351
  timeout: options.timeout
291
352
  }
@@ -293,11 +354,7 @@ function pjax(options) {
293
354
  }
294
355
 
295
356
  // Cancel the current request if we're already pjaxing
296
- var xhr = pjax.xhr
297
- if ( xhr && xhr.readyState < 4) {
298
- xhr.onreadystatechange = $.noop
299
- xhr.abort()
300
- }
357
+ abortXHR(pjax.xhr)
301
358
 
302
359
  pjax.options = options
303
360
  var xhr = pjax.xhr = $.ajax(options)
@@ -305,9 +362,9 @@ function pjax(options) {
305
362
  if (xhr.readyState > 0) {
306
363
  if (options.push && !options.replace) {
307
364
  // Cache current container element before replacing it
308
- cachePush(pjax.state.id, context.clone().contents())
365
+ cachePush(pjax.state.id, [options.container, cloneContents(context)])
309
366
 
310
- window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
367
+ window.history.pushState(null, "", options.requestUrl)
311
368
  }
312
369
 
313
370
  fire('pjax:start', [xhr, options])
@@ -338,35 +395,66 @@ function pjaxReload(container, options) {
338
395
  //
339
396
  // Returns nothing.
340
397
  function locationReplace(url) {
341
- window.history.replaceState(null, "", "#")
398
+ window.history.replaceState(null, "", pjax.state.url)
342
399
  window.location.replace(url)
343
400
  }
344
401
 
402
+
403
+ var initialPop = true
404
+ var initialURL = window.location.href
405
+ var initialState = window.history.state
406
+
407
+ // Initialize $.pjax.state if possible
408
+ // Happens when reloading a page and coming forward from a different
409
+ // session history.
410
+ if (initialState && initialState.container) {
411
+ pjax.state = initialState
412
+ }
413
+
414
+ // Non-webkit browsers don't fire an initial popstate event
415
+ if ('state' in window.history) {
416
+ initialPop = false
417
+ }
418
+
345
419
  // popstate handler takes care of the back and forward buttons
346
420
  //
347
421
  // You probably shouldn't use pjax on pages with other pushState
348
422
  // stuff yet.
349
423
  function onPjaxPopstate(event) {
424
+
425
+ // Hitting back or forward should override any pending PJAX request.
426
+ if (!initialPop) {
427
+ abortXHR(pjax.xhr)
428
+ }
429
+
430
+ var previousState = pjax.state
350
431
  var state = event.state
432
+ var direction
351
433
 
352
434
  if (state && state.container) {
353
- var container = $(state.container)
354
- if (container.length) {
355
- var contents = cacheMapping[state.id]
435
+ // When coming forward from a separate history session, will get an
436
+ // initial pop with a state we are already at. Skip reloading the current
437
+ // page.
438
+ if (initialPop && initialURL == state.url) return
439
+
440
+ if (previousState) {
441
+ // If popping back to the same state, just skip.
442
+ // Could be clicking back from hashchange rather than a pushState.
443
+ if (previousState.id === state.id) return
444
+
445
+ // Since state IDs always increase, we can deduce the navigation direction
446
+ direction = previousState.id < state.id ? 'forward' : 'back'
447
+ }
356
448
 
357
- if (pjax.state) {
358
- // Since state ids always increase, we can deduce the history
359
- // direction from the previous state.
360
- var direction = pjax.state.id < state.id ? 'forward' : 'back'
449
+ var cache = cacheMapping[state.id] || []
450
+ var containerSelector = cache[0] || state.container
451
+ var container = $(containerSelector), contents = cache[1]
361
452
 
453
+ if (container.length) {
454
+ if (previousState) {
362
455
  // Cache current container before replacement and inform the
363
456
  // cache which direction the history shifted.
364
- cachePop(direction, pjax.state.id, container.clone().contents())
365
- } else {
366
- // Page was reloaded but we have an existing history entry.
367
- // Set it to our initial state.
368
- pjax.state = state;
369
- return;
457
+ cachePop(direction, previousState.id, [containerSelector, cloneContents(container)])
370
458
  }
371
459
 
372
460
  var popstateEvent = $.Event('pjax:popstate', {
@@ -378,7 +466,7 @@ function onPjaxPopstate(event) {
378
466
  var options = {
379
467
  id: state.id,
380
468
  url: state.url,
381
- container: container,
469
+ container: containerSelector,
382
470
  push: false,
383
471
  fragment: state.fragment,
384
472
  timeout: state.timeout,
@@ -388,9 +476,14 @@ function onPjaxPopstate(event) {
388
476
  if (contents) {
389
477
  container.trigger('pjax:start', [null, options])
390
478
 
479
+ pjax.state = state
391
480
  if (state.title) document.title = state.title
481
+ var beforeReplaceEvent = $.Event('pjax:beforeReplace', {
482
+ state: state,
483
+ previousState: previousState
484
+ })
485
+ container.trigger(beforeReplaceEvent, [contents, options])
392
486
  container.html(contents)
393
- pjax.state = state
394
487
 
395
488
  container.trigger('pjax:end', [null, options])
396
489
  } else {
@@ -404,6 +497,7 @@ function onPjaxPopstate(event) {
404
497
  locationReplace(location.href)
405
498
  }
406
499
  }
500
+ initialPop = false
407
501
  }
408
502
 
409
503
  // Fallback version of main pjax function for browsers that don't
@@ -434,8 +528,13 @@ function fallbackPjax(options) {
434
528
  var pair = value.split('=')
435
529
  form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]}))
436
530
  })
531
+ } else if ($.isArray(data)) {
532
+ $.each(data, function(index, value) {
533
+ form.append($('<input>', {type: 'hidden', name: value.name, value: value.value}))
534
+ })
437
535
  } else if (typeof data === 'object') {
438
- for (var key in data)
536
+ var key
537
+ for (key in data)
439
538
  form.append($('<input>', {type: 'hidden', name: key, value: data[key]}))
440
539
  }
441
540
 
@@ -443,6 +542,15 @@ function fallbackPjax(options) {
443
542
  form.submit()
444
543
  }
445
544
 
545
+ // Internal: Abort an XmlHttpRequest if it hasn't been completed,
546
+ // also removing its event handlers.
547
+ function abortXHR(xhr) {
548
+ if ( xhr && xhr.readyState < 4) {
549
+ xhr.onreadystatechange = $.noop
550
+ xhr.abort()
551
+ }
552
+ }
553
+
446
554
  // Internal: Generate unique id for state object.
447
555
  //
448
556
  // Use a timestamp instead of a counter since ids should still be
@@ -453,16 +561,22 @@ function uniqueId() {
453
561
  return (new Date).getTime()
454
562
  }
455
563
 
456
- // Internal: Strips _pjax param from url
457
- //
458
- // url - String
564
+ function cloneContents(container) {
565
+ var cloned = container.clone()
566
+ // Unmark script tags as already being eval'd so they can get executed again
567
+ // when restored from cache. HAXX: Uses jQuery internal method.
568
+ cloned.find('script').each(function(){
569
+ if (!this.src) jQuery._data(this, 'globalEval', false)
570
+ })
571
+ return cloned.contents()
572
+ }
573
+
574
+ // Internal: Strip internal query params from parsed URL.
459
575
  //
460
- // Returns String.
461
- function stripPjaxParam(url) {
462
- return url
463
- .replace(/\?_pjax=[^&]+&?/, '?')
464
- .replace(/_pjax=[^&]+&?/, '')
465
- .replace(/[\?&]$/, '')
576
+ // Returns sanitized url.href String.
577
+ function stripInternalParams(url) {
578
+ url.search = url.search.replace(/([?&])(_pjax|_)=[^&]*/g, '')
579
+ return url.href.replace(/\?($|#)/, '$1')
466
580
  }
467
581
 
468
582
  // Internal: Parse URL components and returns a Locationish object.
@@ -476,6 +590,16 @@ function parseURL(url) {
476
590
  return a
477
591
  }
478
592
 
593
+ // Internal: Return the `href` component of given URL object with the hash
594
+ // portion removed.
595
+ //
596
+ // location - Location or HTMLAnchorElement
597
+ //
598
+ // Returns String
599
+ function stripHash(location) {
600
+ return location.href.replace(/#.*/, '')
601
+ }
602
+
479
603
  // Internal: Build options Object for arguments.
480
604
  //
481
605
  // For convenience the first parameter can be either the container or
@@ -494,44 +618,14 @@ function parseURL(url) {
494
618
  //
495
619
  // Returns options Object.
496
620
  function optionsFor(container, options) {
497
- // Both container and options
498
- if ( container && options )
621
+ if (container && options) {
622
+ options = $.extend({}, options)
499
623
  options.container = container
500
-
501
- // First argument is options Object
502
- else if ( $.isPlainObject(container) )
503
- options = container
504
-
505
- // Only container
506
- else
507
- options = {container: container}
508
-
509
- // Find and validate container
510
- if (options.container)
511
- options.container = findContainerFor(options.container)
512
-
513
- return options
514
- }
515
-
516
- // Internal: Find container element for a variety of inputs.
517
- //
518
- // Because we can't persist elements using the history API, we must be
519
- // able to find a String selector that will consistently find the Element.
520
- //
521
- // container - A selector String, jQuery object, or DOM Element.
522
- //
523
- // Returns a jQuery object whose context is `document` and has a selector.
524
- function findContainerFor(container) {
525
- container = $(container)
526
-
527
- if ( !container.length ) {
528
- throw "no pjax container for " + container.selector
529
- } else if ( container.selector !== '' && container.context === document ) {
624
+ return options
625
+ } else if ($.isPlainObject(container)) {
530
626
  return container
531
- } else if ( container.attr('id') ) {
532
- return $('#' + container.attr('id'))
533
627
  } else {
534
- throw "cant get selector for pjax container!"
628
+ return {container: container}
535
629
  }
536
630
  }
537
631
 
@@ -545,7 +639,11 @@ function findContainerFor(container) {
545
639
  //
546
640
  // Returns a jQuery object.
547
641
  function findAll(elems, selector) {
548
- return elems.filter(selector).add(elems.find(selector));
642
+ return elems.filter(selector).add(elems.find(selector))
643
+ }
644
+
645
+ function parseHTML(html) {
646
+ return $.parseHTML(html, document, true)
549
647
  }
550
648
 
551
649
  // Internal: Extracts container and metadata from response.
@@ -560,18 +658,19 @@ function findAll(elems, selector) {
560
658
  //
561
659
  // Returns an Object with url, title, and contents keys.
562
660
  function extractContainer(data, xhr, options) {
563
- var obj = {}
661
+ var obj = {}, fullDocument = /<html/i.test(data)
564
662
 
565
663
  // Prefer X-PJAX-URL header if it was set, otherwise fallback to
566
664
  // using the original requested url.
567
- obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl)
665
+ var serverUrl = xhr.getResponseHeader('X-PJAX-URL')
666
+ obj.url = serverUrl ? stripInternalParams(parseURL(serverUrl)) : options.requestUrl
568
667
 
569
668
  // Attempt to parse response html into elements
570
- if (/<html/i.test(data)) {
571
- var $head = $(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0])
572
- var $body = $(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])
669
+ if (fullDocument) {
670
+ var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0]))
671
+ var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0]))
573
672
  } else {
574
- var $head = $body = $(data)
673
+ var $head = $body = $(parseHTML(data))
575
674
  }
576
675
 
577
676
  // If response data is empty, return fast
@@ -592,7 +691,7 @@ function extractContainer(data, xhr, options) {
592
691
  }
593
692
 
594
693
  if ($fragment.length) {
595
- obj.contents = $fragment.contents()
694
+ obj.contents = options.fragment === 'body' ? $fragment : $fragment.contents()
596
695
 
597
696
  // If there's no title, look for data-title and title attributes
598
697
  // on the fragment
@@ -600,17 +699,21 @@ function extractContainer(data, xhr, options) {
600
699
  obj.title = $fragment.attr('title') || $fragment.data('title')
601
700
  }
602
701
 
603
- } else if (!/<html/i.test(data)) {
702
+ } else if (!fullDocument) {
604
703
  obj.contents = $body
605
704
  }
606
705
 
607
706
  // Clean up any <title> tags
608
707
  if (obj.contents) {
609
708
  // Remove any parent title elements
610
- obj.contents = obj.contents.not('title')
709
+ obj.contents = obj.contents.not(function() { return $(this).is('title') })
611
710
 
612
- // Then scrub any titles from their descendents
711
+ // Then scrub any titles from their descendants
613
712
  obj.contents.find('title').remove()
713
+
714
+ // Gather all script[src] elements
715
+ obj.scripts = findAll(obj.contents, 'script[src]').remove()
716
+ obj.contents = obj.contents.not(obj.scripts)
614
717
  }
615
718
 
616
719
  // Trim any whitespace off the title
@@ -619,6 +722,34 @@ function extractContainer(data, xhr, options) {
619
722
  return obj
620
723
  }
621
724
 
725
+ // Load an execute scripts using standard script request.
726
+ //
727
+ // Avoids jQuery's traditional $.getScript which does a XHR request and
728
+ // globalEval.
729
+ //
730
+ // scripts - jQuery object of script Elements
731
+ //
732
+ // Returns nothing.
733
+ function executeScriptTags(scripts) {
734
+ if (!scripts) return
735
+
736
+ var existingScripts = $('script[src]')
737
+
738
+ scripts.each(function() {
739
+ var src = this.src
740
+ var matchedScripts = existingScripts.filter(function() {
741
+ return this.src === src
742
+ })
743
+ if (matchedScripts.length) return
744
+
745
+ var script = document.createElement('script')
746
+ var type = $(this).attr('type')
747
+ if (type) script.type = type
748
+ script.src = $(this).attr('src')
749
+ document.head.appendChild(script)
750
+ })
751
+ }
752
+
622
753
  // Internal: History DOM caching class.
623
754
  var cacheMapping = {}
624
755
  var cacheForwardStack = []
@@ -636,14 +767,11 @@ function cachePush(id, value) {
636
767
  cacheMapping[id] = value
637
768
  cacheBackStack.push(id)
638
769
 
639
- // Remove all entires in forward history stack after pushing
640
- // a new page.
641
- while (cacheForwardStack.length)
642
- delete cacheMapping[cacheForwardStack.shift()]
770
+ // Remove all entries in forward history stack after pushing a new page.
771
+ trimCacheStack(cacheForwardStack, 0)
643
772
 
644
773
  // Trim back history stack to max cache length.
645
- while (cacheBackStack.length > pjax.defaults.maxCacheLength)
646
- delete cacheMapping[cacheBackStack.shift()]
774
+ trimCacheStack(cacheBackStack, pjax.defaults.maxCacheLength)
647
775
  }
648
776
 
649
777
  // Shifts cache from directional history cache. Should be
@@ -670,6 +798,31 @@ function cachePop(direction, id, value) {
670
798
  pushStack.push(id)
671
799
  if (id = popStack.pop())
672
800
  delete cacheMapping[id]
801
+
802
+ // Trim whichever stack we just pushed to to max cache length.
803
+ trimCacheStack(pushStack, pjax.defaults.maxCacheLength)
804
+ }
805
+
806
+ // Trim a cache stack (either cacheBackStack or cacheForwardStack) to be no
807
+ // longer than the specified length, deleting cached DOM elements as necessary.
808
+ //
809
+ // stack - Array of state IDs
810
+ // length - Maximum length to trim to
811
+ //
812
+ // Returns nothing.
813
+ function trimCacheStack(stack, length) {
814
+ while (stack.length > length)
815
+ delete cacheMapping[stack.shift()]
816
+ }
817
+
818
+ // Public: Find version identifier for the initial page load.
819
+ //
820
+ // Returns String version or undefined.
821
+ function findVersion() {
822
+ return $('meta').filter(function() {
823
+ var name = $(this).attr('http-equiv')
824
+ return name && name.toUpperCase() === 'X-PJAX-VERSION'
825
+ }).attr('content')
673
826
  }
674
827
 
675
828
  // Install pjax functions on $.pjax to enable pushState behavior.
@@ -696,9 +849,10 @@ function enable() {
696
849
  type: 'GET',
697
850
  dataType: 'html',
698
851
  scrollTo: 0,
699
- maxCacheLength: 20
852
+ maxCacheLength: 20,
853
+ version: findVersion
700
854
  }
701
- $(window).bind('popstate.pjax', onPjaxPopstate)
855
+ $(window).on('popstate.pjax', onPjaxPopstate)
702
856
  }
703
857
 
704
858
  // Disable pushState behavior.
@@ -721,21 +875,24 @@ function disable() {
721
875
  $.pjax.submit = $.noop
722
876
  $.pjax.reload = function() { window.location.reload() }
723
877
 
724
- $(window).unbind('popstate.pjax', onPjaxPopstate)
878
+ $(window).off('popstate.pjax', onPjaxPopstate)
725
879
  }
726
880
 
727
881
 
728
882
  // Add the state property to jQuery's event object so we can use it in
729
883
  // $(window).bind('popstate')
730
- if ( $.inArray('state', $.event.props) < 0 )
884
+ if ($.event.props && $.inArray('state', $.event.props) < 0) {
731
885
  $.event.props.push('state')
886
+ } else if (!('state' in $.Event.prototype)) {
887
+ $.event.addProp('state')
888
+ }
732
889
 
733
- // Is pjax supported by this browser?
890
+ // Is pjax supported by this browser?
734
891
  $.support.pjax =
735
892
  window.history && window.history.pushState && window.history.replaceState &&
736
893
  // pushState isn't reliable on iOS until 5.
737
- !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
894
+ !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
738
895
 
739
896
  $.support.pjax ? enable() : disable()
740
897
 
741
- })(jQuery);
898
+ })(jQuery)