├── .gitignore
├── .travis.yml
├── gem
├── pagesjs.rb
└── pagesjs.gemspec
├── package.json
├── ChangeLog
├── test
├── mocha.js
├── integration.html
└── pages_spec.coffee
├── Cakefile
├── LICENSE
├── README.md
└── lib
└── pages.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *~
3 |
4 | node_modules
5 |
6 | build
7 | pkg
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "0.10"
5 |
--------------------------------------------------------------------------------
/gem/pagesjs.rb:
--------------------------------------------------------------------------------
1 | # Used only for Ruby on Rails gem to tell, that gem contain `lib/assets` with
2 | # pages.js file.
3 | module PagesJs
4 | module Rails
5 | class Engine < ::Rails::Engine
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pages.js",
3 | "version": "0.1.0",
4 | "dependencies": {
5 | "jquery-browser": "1.9.1-1"
6 | },
7 | "devDependencies": {
8 | "coffee-script": "1.6.2",
9 | "mocha": "1.9.0",
10 | "chai": "1.5.0",
11 | "sinon": "1.6.0",
12 | "sinon-chai": "2.3.1",
13 | "fs-extra": "0.6.0",
14 | "glob": "3.1.21",
15 | "uglify-js": "2.2.5",
16 | "jsdom": "0.2.19",
17 | "location": "0.0.1",
18 | "jquery": "1.8.3",
19 | "chai-jquery": "1.1.1"
20 | },
21 | "scripts": {
22 | "test": "cake test"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/gem/pagesjs.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | Gem::Specification.new do |s|
4 | s.name = 'pagesjs'
5 | s.version = VERSION
6 | s.platform = Gem::Platform::RUBY
7 | s.authors = ['Andrey "A.I." Sitnik']
8 | s.email = ['andrey@sitnik.ru']
9 | s.homepage = 'https://github.com/ai/pages.js'
10 | s.summary = 'Pages.js - is a framework for History pushState.'
11 | s.description = 'Pages.js allow you to manage pages JS code and ' +
12 | 'forget about low-level History API.'
13 |
14 | s.add_dependency 'sprockets', '>= 2'
15 |
16 | s.files = ['lib/assets/javascripts/pages.js', 'lib/pagesjs.rb',
17 | 'LICENSE', 'README.md', 'ChangeLog']
18 | s.extra_rdoc_files = ['LICENSE', 'README.md', 'ChangeLog']
19 | s.require_path = 'lib'
20 | end
21 |
--------------------------------------------------------------------------------
/ChangeLog:
--------------------------------------------------------------------------------
1 | == 0.1 (Ignacy Hryniewiecki)
2 | * Add API to redefine work with URL.
3 | * Rename Pages.pagesSelector to Pages.selector.
4 | * Simpler API in Pages.animating.
5 | * Abort previous page loading, when start to load new one.
6 | * Decrease files size (by compressible code and UnglifyJS 2).
7 |
8 | == 0.0.5 (Pyotr Kakhovsky)
9 | * Add data.from with URL changing source.
10 | * Pages.open() will change URL if it necessary.
11 | * Don’t track location hash changes.
12 | * Don’t run animation, when page wasn’t be changed.
13 | * Use lib/assets path in gem instead of vendor/assets.
14 |
15 | == 0.0.4 (Mikhail Bestuzhev-Ryumin)
16 | * Allow to set several page options for one page.
17 | * Set empty jQuery object to Pages.current on load.
18 | * Remove non-ASCII symbols from gemspec.
19 | * Print testing URL in test task.
20 |
21 | == 0.0.3 (Sergey Muravyov-Apostol)
22 | * Allow to add listener for all new content.
23 | * Allow to set animation page options in JS.
24 | * Cache page options to tag data.
25 | * Fix gemspec issue with Bundler.
26 |
27 | == 0.0.2 (Kondraty Ryleyev)
28 | * Fix Pages.js disabling in onready event.
29 |
30 | == 0.0.1 (Pavel Pestel)
31 | * Initial release.
32 |
--------------------------------------------------------------------------------
/test/mocha.js:
--------------------------------------------------------------------------------
1 | $ = jQuery = require('jquery');
2 |
3 | chai = require('chai');
4 | sinon = require('sinon');
5 | sinonChai = require('sinon-chai');
6 | chaiJquery = require('chai-jquery');
7 | chai.should();
8 | chai.use(sinonChai);
9 | chai.use(chaiJquery);
10 |
11 | jsdom = require('jsdom')
12 | window = jsdom.jsdom().createWindow();
13 | document = window.document;
14 | location = require('location');
15 | history = window.history = {
16 | pushState: function() {}
17 | };
18 | document.implementation.createHTMLDocument = function(html, url) {
19 | return jsdom.html(html);
20 | };
21 |
22 | // Hack to fix Wrong Document error somewhere between node-jquery and jsdom
23 | var core = require('jsdom/lib/jsdom/level1/core').dom.level1.core;
24 | var originInsertBefore = core.Node.prototype.insertBefore;
25 | core.Node.prototype.insertBefore = function(newChild, refChild) {
26 | newChild._ownerDocument = this._ownerDocument;
27 | return originInsertBefore.apply(this, arguments);
28 | };
29 | var originSetNamedItem = core.NamedNodeMap.prototype.setNamedItem;
30 | core.NamedNodeMap.prototype.setNamedItem = function(arg) {
31 | if ( arg ) {
32 | arg._ownerDocument = this._ownerDocument;
33 | }
34 | return originSetNamedItem.apply(this, arguments);
35 | };
36 |
--------------------------------------------------------------------------------
/test/integration.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ')
313 | ok = sinon.spy()
314 | callback = ($, $$, page) -> ok() if $('.a', page).length
315 | Pages.add(callback)
316 | Pages._enlive(body)
317 |
318 | ok.should.have.been.calledOnce
319 |
320 | describe '._callbackArgs()', ->
321 |
322 | it 'should return arguments for callback', ->
323 | html '
' +
324 | '
'
325 | args = Pages._callbackArgs(find('.a'))
326 |
327 | args[0].should.eql(jQuery)
328 | args[1]('a').should.be('.a a')
329 | args[2].should.be('.a')
330 |
331 | describe '._callback()', ->
332 |
333 | it 'should run all callbacks', ->
334 | a = open: sinon.spy(), close: sinon.spy()
335 | b = open: sinon.spy()
336 | div = $('
').data(pages: [a, b])
337 | sinon.stub(Pages, '_callbackArgs', -> [1, 2, 3])
338 |
339 | Pages._callback(div, 'open')
340 |
341 | a.open.should.have.been.calledOnce
342 | b.open.should.have.been.calledOnce
343 | a.close.should.not.have.been.called
344 |
345 | Pages._callbackArgs.should.have.been.calledWith(div)
346 | a.open.should.have.been.calledWith(1, 2, 3)
347 | a.open.should.have.been.calledOn(div)
348 |
349 | describe '._setCurrent()', ->
350 |
351 | it 'should call open and close events', ->
352 | html '
' +
353 | '
'
354 | Pages.add('.a', open: sinon.spy(), close: sinon.spy())
355 | Pages.add('.b', open: sinon.spy(), close: sinon.spy())
356 | Pages._enlive($(Pages._doc))
357 | Pages.current = $('')
358 |
359 | Pages._setCurrent(find('.a'))
360 | Pages.current.should.eql(find('.a'))
361 | Pages._pages[0].open.should.have.been.calledOnce
362 | Pages._pages[1].open.should.not.have.been.called
363 | Pages._pages[0].close.should.not.have.been.called
364 | Pages._pages[1].close.should.not.have.been.called
365 |
366 | Pages._setCurrent(find('.b'))
367 | Pages.current.should.eql(find('.b'))
368 | Pages._pages[0].open.should.have.been.calledOnce
369 | Pages._pages[1].open.should.have.been.calledOnce
370 | Pages._pages[0].close.should.have.been.calledOnce
371 | Pages._pages[1].close.should.not.have.been.called
372 |
373 | describe '._openLink()', ->
374 |
375 | beforeEach -> sinon.stub(Pages, 'open')
376 | afterEach -> location.hash = ''
377 |
378 | it 'should open url by link', ->
379 | html '
'
380 | Pages._openLink(find('a')).should.be.false
381 | Pages.open.should.have.been.calledWith '/a',
382 | a: 1
383 | from: 'link'
384 | link: find('a')
385 |
386 | it 'should not open external url by link', ->
387 | html '
'
388 | Pages._openLink(find('a')).should.be.true
389 | Pages.open.should.not.have.been.called
390 |
391 | it 'should not open url by disabled link', ->
392 | html '
'
393 | Pages._openLink(find('a')).should.be.true
394 | Pages.open.should.not.have.been.called
395 |
396 | it 'should not call Pages.open without hash', ->
397 | html '
'
398 | Pages._openLink(find('a')).should.be.false
399 | Pages.open.should.have.been.calledWith '/a', from: 'link', link: find('a')
400 |
401 | describe '._openPage()', ->
402 |
403 | it 'should should animated change pages', ->
404 | html '
' +
405 | '
'
407 | a = find('.a')
408 | b = find('.b')
409 | b.data(d: 'D')
410 | Pages.current = a
411 |
412 | sinon.stub(Pages, 'title')
413 |
414 | animationArgs = []
415 | Pages.animations.test = {
416 | animate: -> animationArgs = arguments
417 | }
418 | sinon.spy(Pages.animations.test, 'animate')
419 | Pages.animation = 'test'
420 |
421 | Pages.animating.waiting = true
422 | Pages._openPage(b, { a: 'a', b: 'b' })
423 | Pages.animations.test.animate.should.not.have.been.called
424 |
425 | Pages.animating.end()
426 | Pages.title.should.have.been.calledWith('B')
427 | Pages.animations.test.animate.should.have.been.called
428 | Pages.current.should.be('.a')
429 | Pages.animating.waiting.should.be.true
430 |
431 | animationArgs[0].should.be('.a')
432 | animationArgs[1].should.be('.b')
433 | animationArgs[3].should.
434 | eql({ url: '/b', title: 'B', a: 'a', b: 'b', c: 'C', d: 'D' })
435 |
436 | animationArgs[2]()
437 | Pages.current.should.be('.b')
438 | Pages.animating.waiting.should.be.false
439 |
440 | it 'should not change title to undefined', ->
441 | html '
'
442 | Pages.animations.a = { animate: sinon.spy() }
443 | Pages.animation = 'a'
444 |
445 | sinon.stub(Pages, 'title')
446 | Pages._openPage(find('article'))
447 |
448 | Pages.title.should.not.have.been.called
449 |
450 | it 'should take animation from page', ->
451 | html '
'
452 | Pages.animations.a = { animate: sinon.spy() }
453 | Pages._openPage(find('article'))
454 | Pages.animations.a.animate.should.been.called
455 |
456 | it 'should take animation from link first', ->
457 | html '
'
458 | Pages.animations.a = { animate: sinon.spy() }
459 | Pages.animations.b = { animate: sinon.spy() }
460 | Pages._openPage(find('article'), { pageAnimation: 'b' })
461 |
462 | Pages.animations.a.animate.should.not.been.called
463 | Pages.animations.b.animate.should.been.called
464 |
465 | it 'should choose animation dynamically', ->
466 | Pages.add '.a', animation: -> 'a'
467 | html '
'
468 | Pages.current = $('')
469 | Pages.animations.a = { animate: sinon.spy() }
470 |
471 | Pages._openPage(find('.a'))
472 | Pages.animations.a.animate.should.been.called
473 |
474 | it 'should not show animation when page will not be changed', ->
475 | html '
'
476 | Pages.current = find('.a')
477 |
478 | Pages.animations.test = { animate: sinon.spy() }
479 | Pages.animation = 'test'
480 | sinon.stub(Pages, '_setCurrent')
481 |
482 | Pages._openPage(find('.a'))
483 |
484 | Pages.animations.test.animate.should.not.been.called
485 | Pages._setCurrent.should.been.called
486 |
487 | describe '._loadPages()', ->
488 |
489 | it 'should load new page', ->
490 | html '
'
491 | Pages.current = find('.b')
492 | sinon.stub Pages, 'load', (url, data, callback) ->
493 | callback('
')
494 | callback = sinon.spy()
495 | Pages.add('.a', sinon.spy())
496 |
497 | Pages._loadPages('/a', { }, callback)
498 |
499 | find('.a').should.be.exists
500 | callback.should.have.been.called
501 |
502 | Pages.load.should.have.been.called
503 | Pages._pages[0].load.should.have.been.called
504 | find('.b').next().should.be('.a')
505 |
506 | it 'should load new page without current one', ->
507 | html '
'
508 | Pages.current = $('')
509 | sinon.stub Pages, 'load', (url, data, callback) ->
510 | callback('
')
511 | Pages._loadPages('/a', { }, ->)
512 | find('.a').prev().should.be('div')
513 |
514 | it 'should tell to body that it load page', ->
515 | html ''
516 | body = find('body')
517 | loading = sinon.spy()
518 | loaded = sinon.spy()
519 | body.on('page-loading', loading).on('page-loaded', loaded)
520 | load = ->
521 | sinon.stub Pages, 'load', (url, data, callback) -> load = callback
522 |
523 | Pages._loadPages('/a', { a: 1 }, -> )
524 |
525 | body.should.have.class('page-loading')
526 | loading.should.have.been.called
527 | loaded.should.not.have.been.called
528 |
529 | load()
530 | body.should.not.have.class('page-loading')
531 | loaded.should.have.been.called
532 |
533 | it 'should tell to link that it load page', ->
534 | html '
'
535 | a = find('a')
536 | loading = sinon.spy()
537 | loaded = sinon.spy()
538 | a.on('page-loading', loading).on('page-loaded', loaded)
539 | load = ->
540 | sinon.stub Pages, 'load', (url, data, callback) -> load = callback
541 |
542 | Pages._loadPages('/a', { link: a }, -> )
543 |
544 | a.should.have.class('page-loading')
545 | loading.should.have.been.called
546 | loaded.should.not.have.been.called
547 |
548 | load()
549 | a.should.not.have.class('page-loading')
550 | loaded.should.have.been.called
551 |
552 | it 'should abort previous loading', ->
553 | aborted = []
554 | sinon.stub Pages, 'load', (url, data, callback) ->
555 | { url: url, abort: -> aborted.push(url) }
556 |
557 | Pages._loadPages('/a', { }, -> )
558 | Pages._loading.url.should.eql('/a')
559 | aborted.should.eql([])
560 |
561 | Pages._loadPages('/b', { }, -> )
562 | Pages._loading.url.should.eql('/b')
563 | aborted.should.eql(['/a'])
564 |
--------------------------------------------------------------------------------
/lib/pages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2012 Andrey “A.I.” Sitnik
,
3 | * sponsored by Evil Martians.
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU Lesser General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU Lesser General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Lesser General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | ;(function($, undefined) {
20 | "use strict";
21 |
22 | // Pages.js is a framework for History pushState. It allow you to manage
23 | // pages JS code and forget about low-level APIs.
24 | //
25 | // You need to:
26 | // 1. Wrap your page content (without layout, header and footer) into
27 | // `…`
28 | // and set page URL to `data-url` and title to `data-title` attributes.
29 | // 2. Change your server code, to respond AJAX requests with only wrapped
30 | // page content without layout. Just check `HTTP_X_REQUESTED_WITH`
31 | // HTTP header to equal `"XMLHttpRequest"`.
32 | // 3. Load jQuery before Pages.js.
33 | var self = window.Pages = {
34 |
35 | // Is history management disabled.
36 | disabled: false,
37 |
38 | // jQuery node for current page.
39 | //
40 | // if ( Pages.current.is('.comments-page') ) { … }
41 | current: $(),
42 |
43 | // Selector to find page blocks. Use to find loaded pages and detect
44 | // current one.
45 | //
46 | // Be default it is `article.page` (`article` tag with `page` class).
47 | //
48 | // Pages.selector = '.page';
49 | selector: 'article.page',
50 |
51 | // Current animation name. For some page or link you can set
52 | // custom animation by `data-page-animation` attribute.
53 | //
54 | // Pages.animation = 'myOwnAnimation';
55 | animation: 'fade',
56 |
57 | // Available animation to set in `Pages.animation`. See definitions below.
58 | //
59 | // You can add your own animation. Animation object must contain `animate`
60 | // function with 4 arguments:
61 | // * jQuery-node of current page.
62 | // * jQuery-node of next page.
63 | // * callback, that you must call, when animation will be end.
64 | // * data object with merged link and page datas
65 | //
66 | // Pages.animations.slideUpDown = {
67 | // animate: function (current, next, done, data) {
68 | // var duration = data.duration || 600;
69 | // current.slideUp(duration);
70 | // next.slideDown(duration, done);
71 | // }
72 | // };
73 | // Pages.animation = 'slideUpDown';
74 | animations: {
75 | // The simplest “animation”. Just immediately hide/show pages
76 | // by CSS display property.
77 | immediately: {
78 | animate: function(prev, next, done, data) {
79 | prev.hide();
80 | next.show();
81 | done();
82 | }
83 | },
84 |
85 | // Simple fade in/out animation.
86 | fade: {
87 | // Animation duration in milliseconds.
88 | duration: 300,
89 |
90 | animate: function(prev, next, done, data) {
91 | var half = this.duration / 2;
92 | prev.fadeOut(half, function() {
93 | next.fadeIn(half, done);
94 | });
95 | }
96 | },
97 |
98 | },
99 |
100 | // Add description for page with `selector`. Allow options:
101 | // * `load`: `function ($, $$, page)` which is called, when page is loaded
102 | // (contained in document or loaded after by AJAX).
103 | // Good place to add events handlers to HTML tags.
104 | // * `open`: `function ($, $$, page)` which is called, when page become
105 | // to be visible (called on document ready and when URL is changed).
106 | // * `close`: `function ($, $$, page)` which is called, when page become
107 | // to be hidden (URL changed and another page become to be open).
108 | // * `animation`: `function (prev)` to return animation, depend on
109 | // previous page. For simple solution use `data-page-animation` attribute
110 | // in page or link tags.
111 | //
112 | // Pages.add('.comments-page', {
113 | // load: function($, $$, page) {
114 | // $$('.add').click(function() {
115 | // postNewComment();
116 | // });
117 | // },
118 | // open: function($, $$, page) {
119 | // page.enableAutoUpdate();
120 | // },
121 | // close: function($, $$, page) {
122 | // page.disableAutoUpdate();
123 | // }
124 | // });
125 | //
126 | // Callbacks get three arguments:
127 | // * `$`: jQuery.
128 | // * `$$`: jQuery finder only in page (a little bit faster, than $ and
129 | // more safely). For example `$$('a')` is equal to
130 | // `$('a', page)`.
131 | // * `page`: jQuery-nodes of pages with this selectors.
132 | //
133 | // You can pass `load` as second argument without another options:
134 | //
135 | // Pages.add('.comments-page', function($, $$, page) {
136 | // $$('.pagination').ajaxPagination();
137 | // });
138 | //
139 | // You can set callback to be runned on every content added to DOM
140 | // (for example, to bind events for controls common for all pages).
141 | // Just miss `selector`:
142 | //
143 | // Pages.add(function($, $$, content) {
144 | // $$('[rel=submit]').click(function() {
145 | // $(this).closest('form').submit();
146 | // });
147 | // });
148 | add: function(selector, options) {
149 | if ( options == undefined ) {
150 | self._liveCallbacks.push(selector);
151 | } else {
152 | if ( typeof(options) == 'function' ) {
153 | options = { load: options };
154 | }
155 | options.selector = selector;
156 | self._pages.push(options);
157 | }
158 | },
159 |
160 | // Run Pages.js. It’s called automatically on document ready.
161 | // It’s used for tests.
162 | init: function() {
163 | self._enlive($(self._doc));
164 | var current = self._findCurrent();
165 | if ( current.length ) {
166 | self._setCurrent(current);
167 | }
168 | },
169 |
170 | // Return true if browser support History pushState.
171 | //
172 | // if ( !Pages.isSupported() ) {
173 | // $('.old-browser-notice').show();
174 | // }
175 | //
176 | // If you rewrite `setURL` and other URL methods, you maybe need
177 | // to override this method too:
178 | //
179 | // Pages.isSupported = function() {
180 | // return true;
181 | // };
182 | isSupported: function() {
183 | return !!(window.history && history.pushState);
184 | },
185 |
186 | // Start session history management. It’s called automatically on
187 | // document ready. You can use it manually after `Pages.disable` calling.
188 | enable: function() {
189 | if ( self._events || !self.isSupported() ) {
190 | return false;
191 | }
192 | self.disabled = false;
193 | self._events = true;
194 |
195 | self.watchURL(function() {
196 | if ( self._lastUrl != self.getURL() ) {
197 | self.open(self.getURL(), { from: 'popstate' });
198 | }
199 | });
200 |
201 | $(self._doc).on('click.pages', 'a', function() {
202 | return self._openLink($(this));
203 | });
204 |
205 | return true;
206 | },
207 |
208 | // Disable session history management. Pages.js will load by default browser
209 | // way without AJAX and animations.
210 | disable: function() {
211 | $(self._doc).off('click.pages', 'a');
212 | self.unwatchURL();
213 | self.disabled = true;
214 | self._events = false;
215 | },
216 |
217 | // Show page by `url` with overrided page `data`. Return true if page is
218 | // already loaded and false if Pages.js request it from server.
219 | //
220 | // setTimeout(function() {
221 | // Pages.open('/gameover');
222 | // }, 5000);
223 | open: function(url, data) {
224 | if ( self.getURL() != url ) {
225 | self.setURL(url);
226 | }
227 |
228 | if ( data == undefined ) {
229 | data = { };
230 | }
231 | if ( data.from == undefined ) {
232 | data.from = 'js';
233 | }
234 | data.url = url;
235 | self._lastUrl = url;
236 |
237 | var page = self.page(url);
238 | if ( page.length ) {
239 | self._openPage(page, data);
240 | return true;
241 | } else {
242 | self._loadPages(url, data, function(nodes) {
243 | nodes.hide();
244 | page = self.page(url, nodes);
245 | if ( page.length ) {
246 | self._openPage(page, data);
247 | }
248 | });
249 | return false;
250 | }
251 | },
252 |
253 | // Find loaded page by URL in `data-url` attribute.
254 | // It use `Pages.selector` to detect pages tags.
255 | //
256 | // if ( Pages.page('/comments').length ) {
257 | // // Comment page is loaded to DOM
258 | // }
259 | page: function(url, base) {
260 | if ( !base ) {
261 | base = $(self._doc);
262 | }
263 | var selector = self.selector + '[data-url="' + url + '"]';
264 | return base.filter(selector).add(base.find(selector));
265 | },
266 |
267 | // Load page by url. It simple use `jQuery.get`, but allow you
268 | // to override it.
269 | //
270 | // Argument `data` contain page data from link, you can use it
271 | // in override method.
272 | //
273 | // Page.load = function(url, data, callbacks) {
274 | // $.post(url, { password: data.password }, callback);
275 | // };
276 | //
277 | // Or you can override `load` method, to determine, that page is loaded
278 | // from AJAX (but `HTTP_X_REQUESTED_WITH` is better way):
279 | //
280 | // Page.load = function(url, data, callbacks) {
281 | // return $.get(url + '?no_layout=1', callback);
282 | // };
283 | //
284 | // It must return some AJAX request object or ID to use it in `stopLoading`.
285 | // If you use non-jQuery AJAX, you need also override `stopLoading`.
286 | load: function(url, data, callback) {
287 | return $.get(url, callback);
288 | },
289 |
290 | // Stop current AJAX page loading. It just abort jQuery AJAX, but allow you
291 | // to override it.
292 | //
293 | // It will get as argument, what `load` was return.
294 | //
295 | // You need to override this method, only if your `load` override, don’t
296 | // return jQuery jqXHR object (`$.get` and `$.post` return correct jqXHR).
297 | stopLoading: function(loading) {
298 | loading.abort();
299 | },
300 |
301 | // Change document title. It is internal method, used by `Pages.open`,
302 | // you can override it for some special cases.
303 | //
304 | // Pages.title = function(title) {
305 | // document.title = title + ' - ' companyName;
306 | // };
307 | title: function(title) {
308 | document.title = title;
309 | },
310 |
311 | // Preload pages from `url`. URL can contain several pages, and can be
312 | // different from pages URL.
313 | //
314 | // Pages.preload('/posts/all');
315 | preload: function(url) {
316 | self._loadPages(url, { }, function(nodes) {
317 | nodes.hide();
318 | })
319 | },
320 |
321 | // Change document URL to `url`. By default it will use
322 | // `history.pushState`, but you can change it to support
323 | // your history library or some hacks.
324 | //
325 | // Pages.setURL = function(url) {
326 | // location.hash = url;
327 | // };
328 | setURL: function(url) {
329 | history.pushState({ }, '', url);
330 | },
331 |
332 | // Get current page URL. By default it will use `location.pathname`,
333 | // but you can change it to support your history library or some hacks.
334 | //
335 | // Pages.getURL = function() {
336 | // return location.hash;
337 | // };
338 | getURL: function() {
339 | return location.pathname;
340 | },
341 |
342 | // Set `callback` to watch for current page URL changes.
343 | // By default it will bind to `popstate` event, but you can change it
344 | // to support your history library or some hacks.
345 | //
346 | // Pages.watchURL = function(callback) {
347 | // $(window).on('hashchange.pages', callback);
348 | // };
349 | watchURL: function(callback) {
350 | $(window).on('popstate.pages', callback);
351 | },
352 |
353 | // Disable listening, which was set by `watchURL`.
354 | // By default it will unbind from `popstate` event, but you can change it
355 | // to support your history library or some hacks.
356 | //
357 | // Pages.unwatchURL = function() {
358 | // $(window).off('hashchange.pages');
359 | // };
360 | unwatchURL: function() {
361 | $(window).off('popstate.pages');
362 | },
363 |
364 | // Internal API to wait previous animation, before start new one.
365 | //
366 | // Pages.animating.run(function(done) {
367 | // // Animation code
368 | // done();
369 | // });
370 | animating: {
371 |
372 | // List of callbacks, that wait end of current animation.
373 | _waiters: [],
374 |
375 | // True if some animation is played now.
376 | waiting: false,
377 |
378 | // If there isn’t animation now, `callback` will be executed now.
379 | // Else `callback` will wait, until previous animation will call `end`.
380 | wait: function(callback) {
381 | if ( this.waiting ) {
382 | this._waiters.push(callback);
383 | } else {
384 | callback();
385 | }
386 | },
387 |
388 | // Mark, that current animation is ended.
389 | end: function() {
390 | this.waiting = false;
391 | var waiter;
392 | while ( waiter = this._waiters.pop() ) {
393 | waiter();
394 | }
395 | },
396 |
397 | // Wait for previous animation end, execute `callback` and run another
398 | // animations, only after `callback` will execute it first argument.
399 | //
400 | // Pages.animating.run(function(done) {
401 | // // Animation code
402 | // done();
403 | // });
404 | run: function(callback) {
405 | var animating = this;
406 | animating.wait(function() {
407 | animating.waiting = true;
408 | callback(function() {
409 | animating.end();
410 | });
411 | });
412 | }
413 |
414 | },
415 |
416 | // Link to current `window.document`. It is used for tests.
417 | _doc: document,
418 |
419 | // Arra of added pages options.
420 | _pages: [],
421 |
422 | // Prevent double load page on double click.
423 | _lastUrl: null,
424 |
425 | // Event listeners are already binded.
426 | _events: false,
427 |
428 | // Callbacks was setted by `Pages.live`.
429 | _liveCallbacks: [],
430 |
431 | // Find first current page.
432 | _findCurrent: function() {
433 | return $(self.selector + ':visible:first', self._doc)
434 | },
435 |
436 | // AJAX request, if some page is loading now.
437 | _loading: null,
438 |
439 | // Find pages in new content, set caches and trigger load event on them.
440 | _enlive: function(nodes) {
441 | var args = self._callbackArgs(nodes)
442 | for (var i = 0; i < self._liveCallbacks.length; i++) {
443 | self._liveCallbacks[i].apply(nodes, args);
444 | }
445 |
446 | var page, pages;
447 | for (var i = 0; i < self._pages.length; i++) {
448 | page = self._pages[i];
449 | var divs = nodes.filter(page.selector);
450 | divs = divs.add( nodes.find(page.selector) );
451 | if ( divs.length ) {
452 | pages = divs.data('pages')
453 | if ( pages ) {
454 | pages.push(page);
455 | } else {
456 | divs.data('pages', [page]);
457 | }
458 | if ( page.load ) {
459 | page.load.apply(divs, self._callbackArgs(divs));
460 | }
461 | }
462 | }
463 | },
464 |
465 | // Return callback arguments.
466 | _callbackArgs: function(nodes) {
467 | var $$ = function(subselector) {
468 | return $(subselector, nodes);
469 | };
470 | return [$, $$, nodes];
471 | },
472 |
473 | // Run _type_ callback on every pages in _div_.
474 | _callback: function(div, type, args) {
475 | if ( !div ) {
476 | return;
477 | }
478 |
479 | var page, pages = div.data('pages')
480 | if ( !pages ) {
481 | return;
482 | }
483 |
484 | for (var i = 0; i < pages.length; i++) {
485 | page = pages[i];
486 | if ( page[type] ) {
487 | page[type].apply(div, self._callbackArgs(div));
488 | }
489 | }
490 | },
491 |
492 | // Find page descriptions for current and next page and fire `close` and
493 | // `open` events.
494 | _setCurrent: function(next) {
495 | if ( self.current ) {
496 | self._callback(self.current, 'close');
497 | }
498 | self._callback(next, 'open');
499 | self.current = next;
500 | },
501 |
502 | // Open URL from link.
503 | _openLink: function(link) {
504 | var href = link.attr('href');
505 | if ( !href || href[0] != '/' ) {
506 | return true;
507 | }
508 | if ( link.data().pagesDisable != undefined ) {
509 | return true;
510 | }
511 |
512 | var data = link.data();
513 | data.link = link;
514 | data.from = 'link';
515 |
516 | href = href.split('#', 2);
517 | var path = href[0];
518 | var hash = href[1];
519 |
520 | self.open(path, data);
521 | if ( hash && location.hash != hash ) {
522 | location.hash = hash;
523 | }
524 | return false;
525 | },
526 |
527 | // Open loaded page.
528 | _openPage: function(page, data) {
529 | if ( page[0] == self.current[0] ) {
530 | self._setCurrent(page);
531 | return;
532 | }
533 |
534 | var anim, pageData = page.data();
535 | data = $.extend(pageData, data);
536 |
537 | if ( data.pageAnimation ) {
538 | anim = data.pageAnimation;
539 | } else {
540 | var pageAnimation = null;
541 | if ( pageData.pages ) {
542 | for (var i = 0; i < pageData.pages.length; i++) {
543 | pageAnimation = pageData.pages[i].animation;
544 | }
545 | }
546 | if ( pageAnimation ) {
547 | anim = pageAnimation(self.current);
548 | }
549 | }
550 | anim = anim || self.animation;
551 |
552 | self.animating.run(function(done) {
553 | if ( data.title != undefined ) {
554 | self.title(data.title);
555 | }
556 | self.animations[anim].animate(self.current, page, function() {
557 | done();
558 | self._setCurrent(page);
559 | }, data);
560 | });
561 | },
562 |
563 | // Internal method to load pages. Used in `open` and `preload`.
564 | _loadPages: function(url, data, callback) {
565 | var body = $('body', self._doc).addClass('page-loading');
566 | body.trigger('page-loading', data);
567 | if ( data.link ) {
568 | data.link.addClass('page-loading');
569 | data.link.trigger('page-loading', data);
570 | }
571 |
572 | if ( self._loading ) {
573 | self.stopLoading(self._loading);
574 | }
575 |
576 | self._loading = self.load(url, data, function(html) {
577 | self._loading = null;
578 |
579 | var nodes = $(html);
580 | if ( self.current.length ) {
581 | self.current.after(nodes);
582 | } else {
583 | $(self._doc.body).append(nodes);
584 | }
585 |
586 | body.removeClass('page-loading');
587 | body.trigger('page-loaded', data);
588 | if ( data.link ) {
589 | data.link.removeClass('page-loading');
590 | data.link.trigger('page-loaded', data);
591 | }
592 |
593 | self._enlive(nodes);
594 | callback(nodes);
595 | });
596 | }
597 |
598 | };
599 |
600 | $(document).ready(function() {
601 | self._lastUrl = self.getURL();
602 | self.init();
603 | if ( !self.disabled ) {
604 | self.enable();
605 | }
606 | });
607 |
608 | })(jQuery);
609 |
--------------------------------------------------------------------------------