pjax_rails 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -5,7 +5,7 @@ Integrate Chris Wanstrath's [PJAX](https://github.com/defunkt/jquery-pjax) into
5
5
 
6
6
  To activate, add this to your app/assets/javascripts/application.js (or whatever bundle you use):
7
7
 
8
- //=require pjax
8
+ //=require jquery.pjax
9
9
 
10
10
  All links that match `$('a:not([data-remote]):not([data-behavior]):not([data-skip-pjax])')` will then use PJAX.
11
11
 
@@ -29,7 +29,7 @@ The PJAX container has to be marked with data-pjax-container attribute, so for e
29
29
 
30
30
  FIXME: Currently the layout is hardcoded to "application". Need to delegate that to the specific layout of the controller.
31
31
 
32
- Examples for redirect_pjax_to
32
+ Examples for redirect_to
33
33
  -----------------------------
34
34
 
35
35
  class ProjectsController < ApplicationController
@@ -44,19 +44,19 @@ Examples for redirect_pjax_to
44
44
 
45
45
  def create
46
46
  @project = Project.create params[:project]
47
- redirect_pjax_to :show, @project
47
+ redirect_to :show, @project
48
48
  end
49
49
 
50
50
  def update
51
51
  @project.update_attributes params[:project]
52
- redirect_pjax_to :show, @project
52
+ redirect_to :show, @project
53
53
  end
54
54
 
55
55
  def destroy
56
56
  @project.destroy
57
57
 
58
58
  index # set the objects needed for rendering index
59
- redirect_pjax_to :index
59
+ redirect_to :index
60
60
  end
61
61
 
62
62
  private
data/pjax_rails.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'pjax_rails'
3
- s.version = '0.2.0'
3
+ s.version = '0.2.1'
4
4
  s.author = 'David Heinemeier Hansson (PJAX by Chris Wanstrath)'
5
5
  s.email = 'david@loudthinking.com'
6
6
  s.summary = 'PJAX integration for Rails 3.1+'
@@ -24,8 +24,8 @@
24
24
  //
25
25
  // Returns the jQuery object
26
26
  $.fn.pjax = function( container, options ) {
27
- return this.live('click', function(event){
28
- return handleClick(event, container, options)
27
+ return this.live('click.pjax', function(event){
28
+ handleClick(event, container, options)
29
29
  })
30
30
  }
31
31
 
@@ -58,7 +58,7 @@ function handleClick(event, container, options) {
58
58
 
59
59
  // Middle click, cmd click, and ctrl click should open
60
60
  // links in a new tab as normal.
61
- if ( event.which > 1 || event.metaKey )
61
+ if ( event.which > 1 || event.metaKey || event.ctrlKey )
62
62
  return
63
63
 
64
64
  // Ignore cross origin links
@@ -81,30 +81,6 @@ function handleClick(event, container, options) {
81
81
  $.pjax($.extend({}, defaults, options))
82
82
 
83
83
  event.preventDefault()
84
- return false
85
- }
86
-
87
- // Internal: Strips _pjax param from url
88
- //
89
- // url - String
90
- //
91
- // Returns String.
92
- function stripPjaxParam(url) {
93
- return url
94
- .replace(/\?_pjax=[^&]+&?/, '?')
95
- .replace(/_pjax=[^&]+&?/, '')
96
- .replace(/[\?&]$/, '')
97
- }
98
-
99
- // Internal: Parse URL components and returns a Locationish object.
100
- //
101
- // url - String URL
102
- //
103
- // Returns HTMLAnchorElement that acts like Location.
104
- function parseURL(url) {
105
- var a = document.createElement('a')
106
- a.href = url
107
- return a
108
84
  }
109
85
 
110
86
 
@@ -139,8 +115,7 @@ var pjax = $.pjax = function( options ) {
139
115
  // DEPRECATED: use options.target
140
116
  if (!target && options.clickedElement) target = options.clickedElement[0]
141
117
 
142
- var url = options.url
143
- var hash = parseURL(url).hash
118
+ var hash = parseURL(options.url).hash
144
119
 
145
120
  // DEPRECATED: Save references to original event callbacks. However,
146
121
  // listening for custom pjax:* events is prefered.
@@ -167,8 +142,6 @@ var pjax = $.pjax = function( options ) {
167
142
  var timeoutTimer
168
143
 
169
144
  options.beforeSend = function(xhr, settings) {
170
- url = stripPjaxParam(settings.url)
171
-
172
145
  if (settings.timeout > 0) {
173
146
  timeoutTimer = setTimeout(function() {
174
147
  if (fire('pjax:timeout', [xhr, options]))
@@ -190,11 +163,8 @@ var pjax = $.pjax = function( options ) {
190
163
  if (result === false) return false
191
164
  }
192
165
 
193
- if (!fire('pjax:beforeSend', [xhr, settings])) return false
194
-
195
- fire('pjax:start', [xhr, options])
196
- // start.pjax is deprecated
197
- fire('start.pjax', [xhr, options])
166
+ if (!fire('pjax:beforeSend', [xhr, settings]))
167
+ return false
198
168
  }
199
169
 
200
170
  options.complete = function(xhr, textStatus) {
@@ -212,73 +182,44 @@ var pjax = $.pjax = function( options ) {
212
182
  }
213
183
 
214
184
  options.error = function(xhr, textStatus, errorThrown) {
215
- var respUrl = xhr.getResponseHeader('X-PJAX-URL')
216
- if (respUrl) url = stripPjaxParam(respUrl)
185
+ var container = extractContainer("", xhr, options)
217
186
 
218
187
  // DEPRECATED: Invoke original `error` handler
219
188
  if (oldError) oldError.apply(this, arguments)
220
189
 
221
190
  var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options])
222
191
  if (textStatus !== 'abort' && allowed)
223
- window.location = url
192
+ window.location = container.url
224
193
  }
225
194
 
226
195
  options.success = function(data, status, xhr) {
227
- var respUrl = xhr.getResponseHeader('X-PJAX-URL')
228
- if (respUrl) url = stripPjaxParam(respUrl)
229
-
230
- var title, oldTitle = document.title
231
-
232
- if ( options.fragment ) {
233
- // If they specified a fragment, look for it in the response
234
- // and pull it out.
235
- var html = $('<html>').html(data)
236
- var $fragment = html.find(options.fragment)
237
- if ( $fragment.length ) {
238
- this.html($fragment.contents())
239
-
240
- // If there's a <title> tag in the response, use it as
241
- // the page's title. Otherwise, look for data-title and title attributes.
242
- title = html.find('title').text() || $fragment.attr('title') || $fragment.data('title')
243
- } else {
244
- return window.location = url
245
- }
246
- } else {
247
- // If we got no data or an entire web page, go directly
248
- // to the page and let normal error handling happen.
249
- if ( !$.trim(data) || /<html/i.test(data) )
250
- return window.location = url
196
+ var container = extractContainer(data, xhr, options)
251
197
 
252
- this.html(data)
253
-
254
- // If there's a <title> tag in the response, use it as
255
- // the page's title.
256
- title = this.find('title').remove().text()
198
+ if (!container.contents) {
199
+ window.location = container.url
200
+ return
257
201
  }
258
202
 
259
- if ( title ) document.title = $.trim(title)
260
-
261
- var state = {
262
- url: url,
263
- pjax: this.selector,
203
+ pjax.state = {
204
+ id: options.id || uniqueId(),
205
+ url: container.url,
206
+ title: container.title,
207
+ container: context.selector,
264
208
  fragment: options.fragment,
265
209
  timeout: options.timeout
266
210
  }
267
211
 
268
- if ( options.replace ) {
269
- pjax.active = true
270
- window.history.replaceState(state, document.title, url)
271
- } else if ( options.push ) {
272
- // this extra replaceState before first push ensures good back
273
- // button behavior
274
- if ( !pjax.active ) {
275
- window.history.replaceState($.extend({}, state, {url:null}), oldTitle)
276
- pjax.active = true
277
- }
278
-
279
- window.history.pushState(state, document.title, url)
212
+ if (options.push || options.replace) {
213
+ window.history.replaceState(pjax.state, container.title, container.url)
280
214
  }
281
215
 
216
+ if (container.title) document.title = container.title
217
+ context.html(container.contents)
218
+
219
+ // Scroll to top by default
220
+ if (typeof options.scrollTo === 'number')
221
+ $(window).scrollTop(options.scrollTo)
222
+
282
223
  // Google Analytics support
283
224
  if ( (options.replace || options.push) && window._gaq )
284
225
  _gaq.push(['_trackPageview'])
@@ -296,6 +237,22 @@ var pjax = $.pjax = function( options ) {
296
237
  }
297
238
 
298
239
 
240
+ // Initialize pjax.state for the initial page load. Assume we're
241
+ // using the container and options of the link we're loading for the
242
+ // back button to the initial page. This ensures good back button
243
+ // behavior.
244
+ if (!pjax.state) {
245
+ pjax.state = {
246
+ id: uniqueId(),
247
+ url: window.location.href,
248
+ title: document.title,
249
+ container: context.selector,
250
+ fragment: options.fragment,
251
+ timeout: options.timeout
252
+ }
253
+ window.history.replaceState(pjax.state, document.title)
254
+ }
255
+
299
256
  // Cancel the current request if we're already pjaxing
300
257
  var xhr = pjax.xhr
301
258
  if ( xhr && xhr.readyState < 4) {
@@ -304,13 +261,63 @@ var pjax = $.pjax = function( options ) {
304
261
  }
305
262
 
306
263
  pjax.options = options
307
- pjax.xhr = $.ajax(options)
308
- $(document).trigger('pjax', [pjax.xhr, options])
264
+ var xhr = pjax.xhr = $.ajax(options)
265
+
266
+ if (xhr.readyState > 0) {
267
+ // pjax event is deprecated
268
+ $(document).trigger('pjax', [xhr, options])
269
+
270
+ if (options.push && !options.replace) {
271
+ // Cache current container element before replacing it
272
+ containerCache.push(pjax.state.id, context.clone(true, true).contents())
273
+
274
+ window.history.pushState(null, "", options.url)
275
+ }
276
+
277
+ fire('pjax:start', [xhr, options])
278
+ // start.pjax is deprecated
279
+ fire('start.pjax', [xhr, options])
280
+
281
+ fire('pjax:send', [xhr, options])
282
+ }
309
283
 
310
284
  return pjax.xhr
311
285
  }
312
286
 
313
287
 
288
+ // Internal: Generate unique id for state object.
289
+ //
290
+ // Use a timestamp instead of a counter since ids should still be
291
+ // unique across page loads.
292
+ //
293
+ // Returns Number.
294
+ function uniqueId() {
295
+ return (new Date).getTime()
296
+ }
297
+
298
+ // Internal: Strips _pjax param from url
299
+ //
300
+ // url - String
301
+ //
302
+ // Returns String.
303
+ function stripPjaxParam(url) {
304
+ return url
305
+ .replace(/\?_pjax=[^&]+&?/, '?')
306
+ .replace(/_pjax=[^&]+&?/, '')
307
+ .replace(/[\?&]$/, '')
308
+ }
309
+
310
+ // Internal: Parse URL components and returns a Locationish object.
311
+ //
312
+ // url - String URL
313
+ //
314
+ // Returns HTMLAnchorElement that acts like Location.
315
+ function parseURL(url) {
316
+ var a = document.createElement('a')
317
+ a.href = url
318
+ return a
319
+ }
320
+
314
321
  // Internal: Build options Object for arguments.
315
322
  //
316
323
  // For convenience the first parameter can be either the container or
@@ -370,15 +377,181 @@ function findContainerFor(container) {
370
377
  }
371
378
  }
372
379
 
380
+ // Internal: Filter and find all elements matching the selector.
381
+ //
382
+ // Where $.fn.find only matches descendants, findAll will test all the
383
+ // top level elements in the jQuery object as well.
384
+ //
385
+ // elems - jQuery object of Elements
386
+ // selector - String selector to match
387
+ //
388
+ // Returns a jQuery object.
389
+ function findAll(elems, selector) {
390
+ var results = $()
391
+ elems.each(function() {
392
+ if ($(this).is(selector))
393
+ results = results.add(this)
394
+ results = results.add(selector, this)
395
+ })
396
+ return results
397
+ }
398
+
399
+ // Internal: Extracts container and metadata from response.
400
+ //
401
+ // 1. Extracts X-PJAX-URL header if set
402
+ // 2. Extracts inline <title> tags
403
+ // 3. Builds response Element and extracts fragment if set
404
+ //
405
+ // data - String response data
406
+ // xhr - XHR response
407
+ // options - pjax options Object
408
+ //
409
+ // Returns an Object with url, title, and contents keys.
410
+ function extractContainer(data, xhr, options) {
411
+ var obj = {}
412
+
413
+ // Prefer X-PJAX-URL header if it was set, otherwise fallback to
414
+ // using the original requested url.
415
+ obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.url)
416
+
417
+ // Attempt to parse response html into elements
418
+ var $data = $(data)
419
+
420
+ // If response data is empty, return fast
421
+ if ($data.length === 0)
422
+ return obj
423
+
424
+ // If there's a <title> tag in the response, use it as
425
+ // the page's title.
426
+ obj.title = findAll($data, 'title').last().text()
427
+
428
+ if (options.fragment) {
429
+ // If they specified a fragment, look for it in the response
430
+ // and pull it out.
431
+ var $fragment = findAll($data, options.fragment).first()
432
+
433
+ if ($fragment.length) {
434
+ obj.contents = $fragment.contents()
435
+
436
+ // If there's no title, look for data-title and title attributes
437
+ // on the fragment
438
+ if (!obj.title)
439
+ obj.title = $fragment.attr('title') || $fragment.data('title')
440
+ }
441
+
442
+ } else if (!/<html/i.test(data)) {
443
+ obj.contents = $data
444
+ }
445
+
446
+ // Clean up any <title> tags
447
+ if (obj.contents) {
448
+ // Remove any parent title elements
449
+ obj.contents = obj.contents.not('title')
450
+
451
+ // Then scrub any titles from their descendents
452
+ obj.contents.find('title').remove()
453
+ }
454
+
455
+ // Trim any whitespace off the title
456
+ if (obj.title) obj.title = $.trim(obj.title)
457
+
458
+ return obj
459
+ }
460
+
461
+ // Public: Reload current page with pjax.
462
+ //
463
+ // Returns whatever $.pjax returns.
464
+ pjax.reload = function(container, options) {
465
+ var defaults = {
466
+ url: window.location.href,
467
+ push: false,
468
+ replace: true,
469
+ scrollTo: false
470
+ }
471
+
472
+ return $.pjax($.extend(defaults, optionsFor(container, options)))
473
+ }
474
+
373
475
 
374
476
  pjax.defaults = {
375
477
  timeout: 650,
376
478
  push: true,
377
479
  replace: false,
378
480
  type: 'GET',
379
- dataType: 'html'
481
+ dataType: 'html',
482
+ scrollTo: 0,
483
+ maxCacheLength: 20
380
484
  }
381
485
 
486
+ // Internal: History DOM caching class.
487
+ function Cache() {
488
+ this.mapping = {}
489
+ this.forwardStack = []
490
+ this.backStack = []
491
+ }
492
+ // Push previous state id and container contents into the history
493
+ // cache. Should be called in conjunction with `pushState` to save the
494
+ // previous container contents.
495
+ //
496
+ // id - State ID Number
497
+ // value - DOM Element to cache
498
+ //
499
+ // Returns nothing.
500
+ Cache.prototype.push = function(id, value) {
501
+ this.mapping[id] = value
502
+ this.backStack.push(id)
503
+
504
+ // Remove all entires in forward history stack after pushing
505
+ // a new page.
506
+ while (this.forwardStack.length)
507
+ delete this.mapping[this.forwardStack.shift()]
508
+
509
+ // Trim back history stack to max cache length.
510
+ while (this.backStack.length > pjax.defaults.maxCacheLength)
511
+ delete this.mapping[this.backStack.shift()]
512
+ }
513
+ // Retrieve cached DOM Element for state id.
514
+ //
515
+ // id - State ID Number
516
+ //
517
+ // Returns DOM Element(s) or undefined if cache miss.
518
+ Cache.prototype.get = function(id) {
519
+ return this.mapping[id]
520
+ }
521
+ // Shifts cache from forward history cache to back stack. Should be
522
+ // called on `popstate` with the previous state id and container
523
+ // contents.
524
+ //
525
+ // id - State ID Number
526
+ // value - DOM Element to cache
527
+ //
528
+ // Returns nothing.
529
+ Cache.prototype.forward = function(id, value) {
530
+ this.mapping[id] = value
531
+ this.backStack.push(id)
532
+
533
+ if (id = this.forwardStack.pop())
534
+ delete this.mapping[id]
535
+ }
536
+ // Shifts cache from back history cache to forward stack. Should be
537
+ // called on `popstate` with the previous state id and container
538
+ // contents.
539
+ //
540
+ // id - State ID Number
541
+ // value - DOM Element to cache
542
+ //
543
+ // Returns nothing.
544
+ Cache.prototype.back = function(id, value) {
545
+ this.mapping[id] = value
546
+ this.forwardStack.push(id)
547
+
548
+ if (id = this.backStack.pop())
549
+ delete this.mapping[id]
550
+ }
551
+
552
+ var containerCache = new Cache
553
+
554
+
382
555
  // Export $.pjax.click
383
556
  pjax.click = handleClick
384
557
 
@@ -400,18 +573,61 @@ $(window).bind('popstate', function(event){
400
573
 
401
574
  var state = event.state
402
575
 
403
- if ( state && state.pjax ) {
404
- var container = state.pjax
405
- if ( $(container+'').length )
406
- $.pjax({
407
- url: state.url || location.href,
408
- fragment: state.fragment,
576
+ if (state && state.container) {
577
+ var container = $(state.container)
578
+ if (container.length) {
579
+ var contents = containerCache.get(state.id)
580
+
581
+ if (pjax.state) {
582
+ // Since state ids always increase, we can deduce the history
583
+ // direction from the previous state.
584
+ var direction = pjax.state.id < state.id ? 'forward' : 'back'
585
+
586
+ // Cache current container before replacement and inform the
587
+ // cache which direction the history shifted.
588
+ containerCache[direction](pjax.state.id, container.clone(true, true).contents())
589
+ }
590
+
591
+ var popstateEvent = $.Event('pjax:popstate', {
592
+ state: state,
593
+ direction: direction
594
+ })
595
+ container.trigger(popstateEvent)
596
+
597
+ var options = {
598
+ id: state.id,
599
+ url: state.url,
409
600
  container: container,
410
601
  push: false,
411
- timeout: state.timeout
412
- })
413
- else
602
+ fragment: state.fragment,
603
+ timeout: state.timeout,
604
+ scrollTo: false
605
+ }
606
+
607
+ if (contents) {
608
+ // pjax event is deprecated
609
+ $(document).trigger('pjax', [null, options])
610
+ container.trigger('pjax:start', [null, options])
611
+ // end.pjax event is deprecated
612
+ container.trigger('start.pjax', [null, options])
613
+
614
+ if (state.title) document.title = state.title
615
+ container.html(contents)
616
+ pjax.state = state
617
+
618
+ container.trigger('pjax:end', [null, options])
619
+ // end.pjax event is deprecated
620
+ container.trigger('end.pjax', [null, options])
621
+ } else {
622
+ $.pjax(options)
623
+ }
624
+
625
+ // Force reflow/relayout before the browser tries to restore the
626
+ // scroll position.
627
+ container[0].offsetHeight
628
+ } else {
414
629
  window.location = location.href
630
+ }
415
631
  }
416
632
  })
417
633
 
@@ -428,7 +644,6 @@ $.support.pjax =
428
644
  // pushState isn't reliable on iOS until 5.
429
645
  && !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
430
646
 
431
-
432
647
  // Fall back to normalcy for older browsers.
433
648
  if ( !$.support.pjax ) {
434
649
  $.pjax = function( options ) {
@@ -464,6 +679,7 @@ if ( !$.support.pjax ) {
464
679
  form.submit()
465
680
  }
466
681
  $.pjax.click = $.noop
682
+ $.pjax.reload = window.location.reload
467
683
  $.fn.pjax = function() { return this }
468
684
  }
469
685
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pjax_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-02 00:00:00.000000000 Z
12
+ date: 2012-05-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: jquery-rails
16
- requirement: &70172476533540 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,12 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70172476533540
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
25
30
  description:
26
31
  email: david@loudthinking.com
27
32
  executables: []
@@ -56,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
61
  version: '0'
57
62
  requirements: []
58
63
  rubyforge_project:
59
- rubygems_version: 1.8.11
64
+ rubygems_version: 1.8.23
60
65
  signing_key:
61
66
  specification_version: 3
62
67
  summary: PJAX integration for Rails 3.1+