├── .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 | 5 | Pages.js Integration Test 6 | 7 | 8 | 55 | 58 | 59 | 60 | 67 |
Home. See also contacts or open it from JS
DEL
68 | 69 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require('fs-extra') 2 | url = require('url') 3 | exec = require('child_process').exec 4 | http = require('http') 5 | path = require('path') 6 | glob = require('glob') 7 | coffee = require('coffee-script') 8 | uglify = require('uglify-js') 9 | 10 | project = 11 | 12 | package: -> 13 | JSON.parse(fs.readFileSync('package.json')) 14 | 15 | name: -> 16 | @package().name 17 | 18 | version: -> 19 | @package().version 20 | 21 | tests: -> 22 | fs.readdirSync('test/'). 23 | filter( (i) -> i.match /\.coffee$/ ). 24 | map( (i) -> "test/#{i}" ) 25 | 26 | libs: -> 27 | fs.readdirSync('lib/').map( (i) -> "lib/#{i}" ) 28 | 29 | title: -> 30 | @name()[0].toUpperCase() + @name()[1..-1] 31 | 32 | mocha = 33 | 34 | template: """ 35 | 36 | 37 | 38 | #title# Tests 39 | 40 | 52 | #system# 53 | 60 | #libs# 61 | #tests# 62 | 63 | 64 | see also integration test → 65 | 66 |
67 | 68 | 69 | """ 70 | 71 | html: -> 72 | @render @template, 73 | system: @system() 74 | libs: @scripts project.libs() 75 | tests: @scripts project.tests() 76 | title: project.title() 77 | 78 | render: (template, params) -> 79 | html = template 80 | for name, value of params 81 | html = html.replace("##{name}#", value.replace(/\$/g, '$$$$')) 82 | html 83 | 84 | scripts: (files) -> 85 | files.map( (i) -> "" ).join("\n ") 86 | 87 | system: -> 88 | @scripts ['node_modules/jquery-browser/lib/jquery.js', 89 | 'node_modules/mocha/mocha.js', 90 | 'node_modules/chai/chai.js', 91 | 'node_modules/sinon/lib/sinon.js', 92 | 'node_modules/sinon/lib/sinon/spy.js', 93 | 'node_modules/sinon/lib/sinon/stub.js', 94 | 'node_modules/sinon-chai/lib/sinon-chai.js', 95 | 'node_modules/chai-jquery/chai-jquery.js'] 96 | 97 | task 'server', 'Run test server', -> 98 | server = http.createServer (req, res) -> 99 | pathname = url.parse(req.url).pathname 100 | 101 | if pathname == '/' 102 | res.writeHead 200, 'Content-Type': 'text/html' 103 | res.write mocha.html() 104 | 105 | else if pathname == '/style.css' 106 | res.writeHead 200, 'Content-Type': 'text/css' 107 | res.write fs.readFileSync('node_modules/mocha/mocha.css') 108 | 109 | else if pathname == '/integration' 110 | res.writeHead 200, 'Content-Type': 'text/html' 111 | res.write fs.readFileSync('test/integration.html') 112 | 113 | else if fs.existsSync('.' + pathname) 114 | file = fs.readFileSync('.' + pathname).toString() 115 | if pathname.match(/\.coffee$/) 116 | file = coffee.compile(file) 117 | if pathname.match(/\.(js|coffee)$/) 118 | res.writeHead 200, 'Content-Type': 'application/javascript' 119 | res.write file 120 | 121 | else 122 | res.writeHead 404, 'Content-Type': 'text/plain' 123 | res.write 'Not Found' 124 | res.end() 125 | 126 | server.listen 8000 127 | console.log('Open http://localhost:8000/') 128 | 129 | task 'test', 'Run tests in node', -> 130 | files = ['test/mocha.js'].concat(project.libs()).concat(project.tests()) 131 | options = 132 | ui: 'bdd' 133 | reporter: 'spec' 134 | compilers: 'coffee:coffee-script' 135 | ignoreLeaks: true 136 | colors: true 137 | 138 | command = 'node_modules/.bin/mocha ' 139 | for name, value of options 140 | name = name.replace /[A-Z]/, (letter) -> '-' + letter.toLowerCase() 141 | command += "--#{name} " + if value == true then '' else "#{value} " 142 | command += files.join(' ') 143 | exec command, (error, stdout, stderr) -> 144 | console.log(stdout) if stdout? 145 | console.error(stderr) if stderr? 146 | process.exit(1) if error 147 | 148 | task 'clean', 'Remove all generated files', -> 149 | fs.removeSync('build/') if fs.existsSync('build/') 150 | fs.removeSync('pkg/') if fs.existsSync('pkg/') 151 | 152 | task 'min', 'Create minimized version of library', -> 153 | fs.mkdirsSync('pkg/') unless fs.existsSync('pkg/') 154 | for file in project.libs() 155 | min = uglify.minify(file) 156 | pkg = file.replace('lib/', 'pkg/'). 157 | replace('.js', "-#{project.version()}.min.js") 158 | fs.writeFileSync(pkg, min.code) 159 | 160 | task 'gem', 'Build RubyGem package', -> 161 | fs.removeSync('build/') if fs.existsSync('build/') 162 | fs.mkdirsSync('build/lib/assets/javascripts/') 163 | 164 | copy = require('fs-extra/lib/copy').copyFileSync 165 | gem = project.name().replace('.', '') 166 | 167 | gemspec = fs.readFileSync("gem/#{gem}.gemspec").toString() 168 | gemspec = gemspec.replace('VERSION', "'#{project.version()}'") 169 | fs.writeFileSync("build/#{gem}.gemspec", gemspec) 170 | 171 | copy("gem/#{gem}.rb", "build/lib/#{gem}.rb") 172 | copy('README.md', 'build/README.md') 173 | copy('ChangeLog', 'build/ChangeLog') 174 | copy('LICENSE', 'build/LICENSE') 175 | for file in project.libs() 176 | copy(file, file.replace('lib/', 'build/lib/assets/javascripts/')) 177 | 178 | exec "cd build/; gem build #{gem}.gemspec", (error, message) -> 179 | if error 180 | console.error(error.message) 181 | process.exit(1) 182 | else 183 | fs.mkdirsSync('pkg/') unless fs.existsSync('pkg/') 184 | gemFile = glob.sync('build/*.gem')[0] 185 | copy(gemFile, gemFile.replace(/^build\//, 'pkg/')) 186 | fs.removeSync('build/') 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pages.js [![Build Status](https://travis-ci.org/ai/pages.js.svg)](https://travis-ci.org/ai/pages.js) 2 | 3 | Pages.js is a framework for [History pushState]. It allows you to manage 4 | pages JS code and forget about low-level APIs. 5 | 6 | History pushState is a API to manage user history, it allows you to change 7 | document URL from JS and have full AJAX pages, without `#` in URL. 8 | 9 | For example, user on `example.com` click on usual link (`example.com/conacts`), 10 | but browser doesn’t load new URL. JS loads new page by AJAX, changes pages 11 | with some animation and changes URL to `example.com/contacts`. Of course, 12 | you can use Back button (and go to `example.com`) or send current URL to your 13 | friend (and friend will open contact’s page directly). 14 | 15 | History pushState is supported by modern browsers, but Pages.js will work in 16 | browsers without this API too. They will load pages in an old way with document 17 | reloading. 18 | 19 | 20 | Sponsored by Evil Martians 21 | 22 | 23 | [History pushState]: http://diveintohtml5.info/history.html 24 | 25 | ## Quick Start 26 | 27 | 1. Wrap your page content (without layout, header and footer) into 28 | `
` 29 | and set page URL to `data-url` and title to `data-title` attribute. 30 | 2. Change your server code, to respond AJAX requests without layout. 31 | Just check `HTTP_X_REQUESTED_WITH` HTTP header to equal `"XMLHttpRequest"`. 32 | 33 | For example, in Ruby on Rails add to `ApplicationController`: 34 | ```ruby 35 | layout :disable_for_ajax 36 | def disable_for_ajax 37 | request.xhr? ? nil : 'application' 38 | end 39 | ``` 40 | 3. Add Pages.js library to your pages: 41 | ```html 42 | 43 | ``` 44 | See Installing section for details. 45 | 46 | That’s all. Now your pages will be changed without document reloading 47 | (and JS interruption) and with simple nice animation. 48 | 49 | Of cource, you can customize wrap tags and load method. Quick Start show only 50 | default way. 51 | 52 | ## Installing 53 | 54 | ### Ruby on Rails 55 | 56 | For Ruby on Rails you can use gem for Assets Pipeline. 57 | 58 | 1. Add `pagesjs` gem to `Gemfile`: 59 | 60 | ```ruby 61 | gem "pagesjs" 62 | ``` 63 | 64 | 2. Install gems: 65 | 66 | ```sh 67 | bundle install 68 | ``` 69 | 70 | 3. Include Pages.js to your `application.js.coffee`: 71 | 72 | ```coffee 73 | #= require pages 74 | ``` 75 | 76 | ### Others 77 | 78 | If you don’t use any assets packaging manager (it’s very bad idea), you can use 79 | already minified version of the library. 80 | Take it from: [github.com/ai/pages.js/downloads]. 81 | 82 | [github.com/ai/pages.js/downloads]: https://github.com/ai/pages.js/downloads 83 | 84 | ## Usage 85 | 86 | ### Events 87 | 88 | You can register page handler, to run some JS code for special pages: 89 | 90 | ```js 91 | Pages.add('.comments-page', { 92 | load: function($, $$, page) { 93 | $$('.add').click(function() { 94 | postNewComment(); 95 | }); 96 | }, 97 | open: function($, $$, page) { 98 | page.enableAutoUpdate(); 99 | }, 100 | close: function($, $$, page) { 101 | page.disableAutoUpdate(); 102 | } 103 | }); 104 | ``` 105 | 106 | `Pages.add(selector, options)` allow you to set options for all pages selected 107 | by `selector`: 108 | 109 | * `load`: `function ($, $$, page)` which will be called, when page is loaded 110 | (already contained in document or loaded after by AJAX). Good place to add 111 | events handlers to HTML tags. 112 | * `open`: `function ($, $$, page)` which will be called, when page becomes 113 | visible (it is happened when document ready and when URL is changed). 114 | * `close`: `function ($, $$, page)` which will be called, when page becomes 115 | hidden (URL changed and another page become to be open). 116 | * `animation`: `function (prev)` to return animation, depend on previous page. 117 | For simple solution use `data-page-animation` attribute in page or link tags. 118 | 119 | Callbacks get three arguments: 120 | 121 | * `$`: jQuery. 122 | * `$$`: jQuery finder only in current page (a little bit faster and more safely 123 | than `$`). For example `$$('a')` is equal to `$('a', page)`. 124 | * `page`: jQuery-nodes of selected page. 125 | 126 | You can pass `load` as second argument without other options: 127 | 128 | ```js 129 | Pages.add('.comments-page', function($, $$, page) { 130 | $$('.pagination').ajaxPagination(); 131 | }); 132 | ``` 133 | 134 | ### Loading 135 | 136 | When Pages.js load new page by AJAX it sets `page-loading` class to body and 137 | trigger `page-loading` event on it. When page is loaded, `page-loading` 138 | class will be removed and `page-loaded` event will be triggered. 139 | 140 | ```css 141 | body.page-loading .loader { 142 | display: block; 143 | } 144 | ``` 145 | 146 | Link, which is clicked to open new page, will get `page-loading` class and 147 | `page-loading`, `page-loaded` events too. 148 | 149 | ```css 150 | .menu a.page-loading { 151 | background: url(loading.gif); 152 | } 153 | ``` 154 | 155 | ### Animation 156 | 157 | `Pages.animations` hash contain available animations. You can change current 158 | animation by `Pages.animation`: 159 | 160 | ```js 161 | Pages.animation = 'fade'; 162 | ``` 163 | 164 | You can change animation for special page or link by `data-page-animation` 165 | attribute. 166 | 167 | You can create you own animation, just add object with `animate` function. 168 | When animation ends, you *must* call `done` argument. 169 | 170 | ```js 171 | Pages.animations.cool = { 172 | animate: function(prev, next, done, data) { 173 | prev.coolHiding(); 174 | next.coolShowing(function() { 175 | done(); 176 | }); 177 | } 178 | }; 179 | Pages.animation = 'cool'; 180 | ``` 181 | 182 | Argument `data` contains merged page and link data attributes: 183 | 184 | ```html 185 | Home 186 | Products 187 | Contacts 188 | ``` 189 | 190 | ```js 191 | Pages.animations.slide = { 192 | animate: function(prev, next, done, data) { 193 | prev.slideHide(data.direction); 194 | next.slideShow(data.direction, function() { 195 | done(); 196 | }); 197 | } 198 | }; 199 | Pages.animation = 'slide'; 200 | ``` 201 | 202 | ### Preload 203 | 204 | If you want to preload some pages, just add them to HTML and hide. For example: 205 | 206 | ```html 207 |
208 | Product 1 209 | Product 2 210 | Product 3 211 |
212 | 213 | 214 | 215 | 216 | ``` 217 | 218 | If you want to preload page by JS (for example, after `onload` event), just use 219 | `Pages.preload(url)` method. URL can contain several pages, and can be 220 | different from pages URL. 221 | 222 | ```js 223 | $(document).load(function() { 224 | Pages.preload('/posts/all'); 225 | }); 226 | ``` 227 | 228 | ### History API support 229 | 230 | I prefer graceful degradation and think, that old browsers should reload full 231 | page by old way. But if you want to add page changes animation to old browser, 232 | you can redefine `Pages.isSupported`, `Pages.getURL`, `Pages.setURL`, 233 | `Pages.watchURL` and `Pages.unwatchURL` methods to support any of History API 234 | polyffils. 235 | 236 | ### URL Methods 237 | 238 | You can redefine way, that Pages.js use to get/set current page URL. 239 | For example, to synchronize open page between different tabs by Session Storage, 240 | or to support one page sites: 241 | 242 | ```js 243 | Pages.isSupported = function() { 244 | return true; 245 | }; 246 | 247 | Pages.setURL = function(url) { 248 | location.hash = url; 249 | }; 250 | 251 | Pages.getURL = function() { 252 | return location.hash; 253 | }; 254 | 255 | Pages.watchURL = function(callback) { 256 | $(window).on('hashchange.pages', callback); 257 | }; 258 | 259 | Pages.unwatchURL = function() { 260 | $(window).off('hashchange.pages'); 261 | }; 262 | ``` 263 | 264 | ## Contributing 265 | 266 | 1. To run tests you need node.js and npm. For example, in Ubuntu run: 267 | 268 | ```sh 269 | sudo apt-get install nodejs npm 270 | ``` 271 | 272 | 2. Next install npm dependencies: 273 | 274 | ```sh 275 | npm install 276 | ``` 277 | 278 | 3. Run test server: 279 | 280 | ```sh 281 | ./node_modules/.bin/cake server 282 | ``` 283 | 284 | 4. Open tests in browser: [localhost:8000]. 285 | 5. Also you can see real usage example in integration test: 286 | [localhost:8000/integration]. 287 | 288 | [localhost:8000]: http://localhost:8000 289 | [localhost:8000/integration]: http://localhost:8000/integration 290 | -------------------------------------------------------------------------------- /test/pages_spec.coffee: -------------------------------------------------------------------------------- 1 | Pages = window.Pages 2 | 3 | describe 'Pages', -> 4 | title = null 5 | animations = null 6 | 7 | before -> 8 | title = document.title 9 | # Disable function, that can broke test page 10 | history.pushState = -> 11 | jQuery.ajax = -> 12 | 13 | beforeEach -> 14 | Pages._doc = $('
')[0] 15 | Pages._current = $('') 16 | Pages._loading = null 17 | Pages.enable() 18 | animations = Pages.animations 19 | 20 | afterEach -> 21 | Pages.animating.end() 22 | document.title = title 23 | Pages._pages = [] 24 | Pages._liveCallbacks = [] 25 | Pages.animation = 'immediately' 26 | Pages.animations = animations 27 | Pages.disable() 28 | history.pushState.restore?() 29 | Pages[i]?.restore?() for i of Pages 30 | 31 | html = (string) -> 32 | Pages.disable() 33 | Pages._doc = document.implementation.createHTMLDocument('') 34 | $(Pages._doc.body).html(string) 35 | Pages.init() 36 | Pages.enable() 37 | 38 | find = (selector) -> 39 | jQuery(selector, Pages._doc) 40 | 41 | describe '.add()', -> 42 | 43 | it 'should add page description', -> 44 | Pages.add('.a-page', a: 1) 45 | Pages._pages.should.eql([{ selector: '.a-page', a: 1 }]) 46 | 47 | it 'should add page with only load callback', -> 48 | load = -> 49 | Pages.add('.a-page', load) 50 | Pages._pages.should.eql([{ selector: '.a-page', load: load }]) 51 | 52 | it 'should call load callback at all pages', -> 53 | callback = ($, $$, page) -> 54 | page.length.should.eql(2) 55 | 56 | Pages.add('.a', callback) 57 | html '
' + 58 | '
' 59 | 60 | it 'should pass arguments to callbacks', -> 61 | callback = ($, $$, page) -> 62 | $.should.eql(jQuery) 63 | $$('a').length.should.eql(1) 64 | this.should.eql(page) 65 | page.should.have.class('a') 66 | 67 | Pages.add('.a', load: ( -> ), open: ( -> ), close: ( -> )) 68 | sinon.stub(Pages._pages[0], 'load', callback) 69 | sinon.stub(Pages._pages[0], 'open', callback) 70 | sinon.stub(Pages._pages[0], 'close', callback) 71 | html '
' + 72 | '
' 73 | 74 | Pages._setCurrent(find('a')) 75 | Pages._setCurrent(find('b')) 76 | 77 | it 'should add new content listener', -> 78 | callback = -> 79 | Pages.add(callback) 80 | Pages._liveCallbacks.should.eql([callback]) 81 | 82 | describe '.init()', -> 83 | 84 | it 'should trigger load events if Pages.js is enable', -> 85 | sinon.stub(Pages, '_enlive') 86 | html '
' 87 | Pages._enlive.should.have.been.calledWith($(Pages._doc)) 88 | 89 | it 'should set current', -> 90 | html '
' 91 | sinon.stub(Pages, '_findCurrent').returns(find('.a')) 92 | sinon.stub(Pages, '_setCurrent') 93 | Pages.init() 94 | Pages._setCurrent.should.been.calledWith(find('.a')) 95 | 96 | describe '.isSupported()', -> 97 | 98 | it 'should return boolean', -> 99 | Pages.isSupported().should.be.a('boolean') 100 | 101 | describe '.enable()', -> 102 | 103 | beforeEach -> Pages.disable() 104 | 105 | it 'should enable history management', -> 106 | Pages.enable().should.be.true 107 | Pages.disabled.should.be.false 108 | 109 | it 'should not enable without support', -> 110 | sinon.stub(Pages, 'isSupported').returns(false) 111 | Pages.enable().should.be.false 112 | Pages.disabled.should.be.true 113 | 114 | it 'should add events', -> 115 | Pages.enable() 116 | $._data(window, 'events').popstate.length.should.eql(1) 117 | $._data(Pages._doc, 'events').click.length.should.eql(1) 118 | 119 | it 'should not enabled twice', -> 120 | Pages.enable().should.be.true 121 | Pages.enable().should.be.false 122 | $._data(window, 'events').popstate.length.should.eql(1) 123 | 124 | describe '.disable()', -> 125 | 126 | it 'should disable history management', -> 127 | Pages.disable() 128 | Pages.disabled.should.be.true 129 | 130 | it 'should remove events', -> 131 | Pages.disable() 132 | (typeof $._data(window, 'events') ).should.eql('undefined') 133 | (typeof $._data(Pages._doc, 'events') ).should.eql('undefined') 134 | 135 | describe 'history events', -> 136 | 137 | it 'should not open page by popstate event without URL changes', -> 138 | sinon.spy(Pages, 'open') 139 | $(window).trigger('popstate.pages') 140 | Pages.open.should.not.have.been.called 141 | 142 | it 'should open page by popstate event, when URL change', -> 143 | Pages._lastUrl = null 144 | sinon.spy(Pages, 'open') 145 | $(window).trigger('popstate.pages') 146 | Pages.open.should.have.been.calledWith('/', url: '/', from: 'popstate') 147 | 148 | it 'should open page by link click', -> 149 | sinon.spy(Pages, '_openLink') 150 | html '' 151 | find('a').click() 152 | Pages._openLink.should.have.been.called 153 | 154 | describe '.open()', -> 155 | 156 | beforeEach -> 157 | sinon.stub(Pages, '_openPage') 158 | sinon.stub(history, 'pushState') 159 | 160 | it 'should open loaded page', -> 161 | html '
' 162 | 163 | a = find('.a') 164 | sinon.stub(Pages, 'page').withArgs('/a').returns(a) 165 | 166 | Pages.open('/a').should.be.true 167 | Pages._openPage.should.have.been.calledWith(a, { url: '/a', from: 'js' }) 168 | Pages._lastUrl.should == '/a' 169 | 170 | it 'should load new page', -> 171 | html '
' 172 | Pages.current = find('.b') 173 | sinon.stub Pages, '_loadPages', (url, data, callback) -> 174 | callback($('
'). 175 | insertAfter(Pages.current)) 176 | Pages.add('.a', sinon.spy()) 177 | 178 | Pages.open('/a', { a: 1 }).should.be.false 179 | find('.a').should.be.exists 180 | Pages._loadPages.should.have.been.called 181 | Pages._openPage.should.have.been.called 182 | 183 | it 'should change url if it necessary', -> 184 | html '
' 185 | Pages.open('/a') 186 | history.pushState.should.have.been.calledWith({ }, '', '/a') 187 | 188 | Pages.open('/a') 189 | history.pushState.should.have.been.once 190 | 191 | describe '.page()', -> 192 | 193 | it 'should find loaded page by url', -> 194 | html '
' + 195 | '
' 196 | Pages.page('/a').should.have.class('a') 197 | 198 | it 'should allow set base nodes to find inside', -> 199 | html '
' + 200 | '
' + 201 | '
' + 202 | '
' 203 | Pages.page('/a', find('.b')).length.should.eql(2) 204 | 205 | describe '.selector', -> 206 | 207 | selector = null 208 | beforeEach -> selector = Pages.selector 209 | afterEach -> Pages.selector = selector 210 | 211 | it 'should use in loaded page finding', -> 212 | html '
' + 213 | '
' 214 | Pages.selector = 'div' 215 | Pages.page('/a').should.have.be('div') 216 | 217 | describe '.load()', -> 218 | 219 | after -> jQuery.get.restore?() 220 | 221 | it 'should use jQuery GET AJAX request', -> 222 | sinon.stub(jQuery, 'get').returns(2) 223 | callback = -> 224 | Pages.load('/a', a: 1, callback).should.eql(2) 225 | jQuery.get.should.have.been.calledWith('/a', callback) 226 | 227 | describe '.stopLoading()', -> 228 | 229 | it 'should call abort in loading object', -> 230 | loading = { abort: sinon.spy() } 231 | Pages.stopLoading(loading) 232 | loading.abort.should.have.been.called 233 | 234 | describe '.title()', -> 235 | 236 | it 'should change title', -> 237 | Pages.title('New') 238 | document.title.should.eql('New') 239 | 240 | describe '.animating', -> 241 | 242 | afterEach -> 243 | Pages.animating[i].restore?() for i of Pages.animating 244 | 245 | it 'should run callbacks now without running animation', -> 246 | callback = sinon.spy() 247 | Pages.animating.wait(callback) 248 | callback.should.have.been.called 249 | Pages.animating.waiting.should.be.false 250 | 251 | it 'should run callback, when animation will end', -> 252 | callback = sinon.spy() 253 | 254 | Pages.animating.waiting = true 255 | 256 | Pages.animating.wait(callback) 257 | callback.should.not.have.been.called 258 | 259 | Pages.animating.end() 260 | callback.should.have.been.called 261 | Pages.animating.waiting.should.be.false 262 | 263 | it 'should have shortcut', -> 264 | done = null 265 | callback = (arg) -> done = arg 266 | sinon.spy(Pages.animating, 'wait') 267 | sinon.spy(Pages.animating, 'end') 268 | 269 | Pages.animating.run(callback) 270 | Pages.animating.waiting.should.be.true 271 | Pages.animating.end.should.not.have.been.called 272 | Pages.animating.wait.should.have.been.called 273 | done.should.be('function') 274 | 275 | done() 276 | Pages.animating.end.should.have.been.called 277 | 278 | describe '._enlive()', -> 279 | 280 | it 'should run load event', -> 281 | h = $('
' + 282 | '
' + 283 | '
') 284 | a = sinon.spy() 285 | b1 = sinon.spy() 286 | b2 = sinon.spy() 287 | c = sinon.spy() 288 | c = sinon.spy() 289 | Pages.add('.a', a) 290 | Pages.add('.b', b1) 291 | Pages.add('.b', b2) 292 | 293 | Pages._enlive(h) 294 | 295 | a.should.have.been.calledOnce 296 | b1.should.have.been.calledOnce 297 | b2.should.have.been.calledOnce 298 | c.should.not.have.been.called 299 | 300 | h.filter('.a').data('pages').should.eql([Pages._pages[0]]) 301 | h.find('.b').data('pages').should.eql([Pages._pages[1], Pages._pages[2]]) 302 | 303 | it 'should run common content listeners', -> 304 | h = $('
') 305 | a = sinon.spy() 306 | Pages.add(a) 307 | Pages._enlive(h) 308 | 309 | a.should.have.been.calledOnce 310 | 311 | it 'should run init callback with correct argument', -> 312 | body = $('
') 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 | --------------------------------------------------------------------------------