pjax_rails 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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+