First commit

This commit is contained in:
Maxime Thirouin
2014-03-24 08:34:59 +01:00
commit db3eecadd0
12 changed files with 1527 additions and 0 deletions

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

147
.jscs.json Normal file
View File

@@ -0,0 +1,147 @@
{
"excludeFiles": [
]
, "requireCurlyBraces": [
"if"
, "else"
, "for"
, "while"
, "do"
, "try"
, "catch"
]
, "requireSpaceAfterKeywords": [
"if"
, "else"
, "for"
, "while"
, "do"
, "switch"
, "return"
, "try"
, "catch"
]
, "requireSpacesInFunctionExpression": {
"beforeOpeningCurlyBrace": true
}
, "disallowSpacesInFunctionExpression": {
"beforeOpeningRoundBrace": true
}
, "disallowEmptyBlocks": true
, "disallowSpacesInsideObjectBrackets": true
, "disallowSpacesInsideArrayBrackets": true
, "disallowSpacesInsideParentheses": true
, "disallowSpaceAfterObjectKeys": true
, "disallowCommaBeforeLineBreak": true
, "requireOperatorBeforeLineBreak": [
"?"
, "+"
, "-"
, "/"
, "*"
, "="
, "=="
, "==="
, "!="
, "!=="
, ">"
, ">="
, "<"
, "<="
]
, "disallowLeftStickedOperators": [
"?"
, "+"
, "-"
, "/"
, "*"
, "="
, "=="
, "==="
, "!="
, "!=="
, ">"
, ">="
, "<"
, "<="
]
, "requireRightStickedOperators": [
"!"
]
, "disallowRightStickedOperators": [
"?"
, "+"
, "/"
, "*"
, ":"
, "="
, "=="
, "==="
, "!="
, "!=="
, ">"
, ">="
, "<"
, "<="
]
, "disallowSpaceAfterPrefixUnaryOperators": [
"++"
, "--"
, "+"
, "-"
, "~"
, "!"
]
, "disallowSpaceBeforePostfixUnaryOperators": [
"++"
, "--"
]
, "requireSpaceBeforeBinaryOperators": [
"+"
, "-"
, "/"
, "*"
, "="
, "=="
, "==="
, "!="
, "!=="
]
, "requireSpaceAfterBinaryOperators": [
"+"
, "-"
, "/"
, "*"
, "="
, "=="
, "==="
, "!="
, "!=="
]
, "disallowImplicitTypeConversion": [
"numeric"
, "boolean"
, "binary"
, "string"
]
, "disallowKeywords": [
"with"
]
, "disallowMultipleLineStrings": true
, "validateQuoteMarks": "\""
, "disallowMixedSpacesAndTabs": true
, "disallowTrailingWhitespace": true
, "requireKeywordsOnNewLine": [
]
, "requireLineFeedAtFileEnd": true
, "requireCapitalizedConstructors": true
, "safeContextKeyword": "that"
, "validateJSDoc": {
"checkParamNames": true
, "checkRedundantParams": true
, "requireParamTypes": true
}
}

4
.jshintrc Normal file
View File

@@ -0,0 +1,4 @@
{
"asi": true
, "laxcomma": true
}

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# Changelog
## 0.1.0 - 2014-03-24
Initial release

185
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,185 @@
# Contributing Guide
Please take a moment to review this document in order to make the contribution
process easy and effective for everyone involved.
Following these guidelines helps to communicate that you respect the time of
the developers managing and developing this open source project. In return,
they should reciprocate that respect in addressing your issue, assessing
changes, and helping you finalize your pull requests.
## Using the issue tracker
The issue tracker is the preferred channel for [bug reports](#bugs),
[features requests](#features) and [submitting pull
requests](#pull-requests).
<a name="bugs"></a>
## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository.
Good bug reports are extremely helpful - thank you!
Guidelines for bug reports:
1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `master` or development branch in the repository.
3. **Isolate the problem** &mdash; ideally create a [reduced test
case](http://css-tricks.com/6263-reduced-test-cases/).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report. What is
your environment? What steps will reproduce the issue? What OS experiences the
problem? What would you expect to be the outcome? All these details will help
people to fix any potential bugs.
Example:
> Short and descriptive example bug report title
>
> A summary of the issue and the browser/OS environment in which it occurs. If
> suitable, include the steps required to reproduce the bug.
>
> 1. This is the first step
> 2. This is the second step
> 3. Further steps, etc.
>
> `<url>` - a link to the reduced test case
>
> Any other information you want to share that is relevant to the issue being
> reported. This might include the lines of code that you have identified as
> causing the bug, and potential solutions (and your opinions on their
> merits).
<a name="features"></a>
## Feature requests
Feature requests are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature. Please
provide as much detail and context as possible.
<a name="pull-requests"></a>
## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic
help. They should remain focused in scope and avoid containing unrelated
commits.
**Please ask first** before embarking on any significant pull request (e.g.
implementing features, refactoring code), otherwise you risk spending a lot of
time working on something that the project's developers might not want to merge
into the project.
Please adhere to the coding conventions used throughout a project (indentation,
accurate comments, etc.) and any other requirements (such as test coverage).
Adhering to the following this process is the best way to get your work
included in the project:
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/happyplan
# Navigate to the newly cloned directory
cd happyplan
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/happyplan/happyplan
```
2. If you cloned a while ago, get the latest changes from upstream:
```bash
git checkout master
git pull upstream master
```
3. Create a new topic branch (off the main project development branch) to
contain your feature, change, or fix:
```bash
git checkout -b <topic-branch-name>
```
4. Make sure to update, or add to the tests when appropriate. Patches and
features will not be accepted without tests. Run `npm test` to check that
all tests pass after you've made changes.
5. Commit your changes in logical chunks. Please adhere to these [git commit
message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
or your code is unlikely be merged into the main project. Use Git's
[interactive rebase](https://help.github.com/articles/interactive-rebase)
feature to tidy up your commits before making them public.
6. Locally merge (or rebase) the upstream development branch into your topic branch:
```bash
git pull [--rebase] upstream master
```
7. Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
with a clear title and description.
9. If you are asked to amend your changes before they can be merged in, please
use `git commit --amend` (or rebasing for multi-commit Pull Requests) and
force push to your remote feature branch. You may also be asked to squash
commits.
**IMPORTANT**: By submitting a patch, you agree to license your work under the
same license as that used by the project.
<a name="maintainers"></a>
## Maintainers
If you have commit access, please follow this process for merging patches and cutting new releases.
### Reviewing changes
1. Check that a change is within the scope and philosophy of the project.
2. Check that a change has any necessary tests and a proper, descriptive commit message.
3. Checkout the change and test it locally.
4. If the change is good, and authored by someone who cannot commit to
`master`, please try to avoid using GitHub's merge button. Apply the change
to `master` locally (feel free to amend any minor problems in the author's
original commit if necessary).
5. If the change is good, and authored by another maintainer/collaborator, give
them a "Ship it!" comment and let them handle the merge.
### Submitting changes
1. All non-trivial changes should be put up for review using GitHub Pull
Requests.
2. Your change should not be merged into `master` (or another feature branch),
without at least one "Ship it!" comment from another maintainer/collaborator
on the project. "Looks good to me" is not the same as "Ship it!".
3. Try to avoid using GitHub's merge button. Locally rebase your change onto
`master` and then push to GitHub.
4. Once a feature branch has been merged into its target branch, please delete
the feature branch from the remote repository.
### Releasing a new version
1. Include all new functional changes in the CHANGELOG.
2. Use a dedicated commit to increment the version. The version needs to be
added to the `CHANGELOG.md` (inc. date) and the `package.json`.
3. The commit message must be of `v0.0.0` format.
4. Create an annotated tag for the version: `git tag -m "v0.0.0" v0.0.0`.
5. Push the changes and tags to GitHub: `git push --tags origin master`.
6. Publish the new version to npm: `npm publish`.

20
LICENSE-MIT Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 "MoOx" Maxime Thirouin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

487
README.md Normal file
View File

@@ -0,0 +1,487 @@
# Pjax
<img align="right" src="https://dl.dropboxusercontent.com/u/14108185/memes/mind-blow.gif">
> When Ajax navigation meets Push State
Pjax is ~~a jQuery plugin~~ **a standalone JavaScript module** that uses
ajax (XmlHttpRequest) and
[pushState()](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history)
to deliver a fast browsing experience.
_It allow you to completely transform user experience of standard websites
(server side generated or static ones) to make them feel they browse an app._
## How Pjax works
Pjax loads page using ajax & updates the browser's current url using pushState without reloading your page's layout or any resources (js, css), giving a fast page load.
_But under the hood, it's just ONE http request with a pushState() call._
Obviously, for [browsers that don't support pushState()](http://caniuse.com/#search=pushstate) Pjax fully degrades (yeah, it doesn't do anything at all).
It simply works with all permalinks & can update all parts of the page you
want (including html metas, title, navigation state).
- It's not limited to one container, like jQuery-Pjax is,
- It fully support browser history (back & forward buttons),
- It **will** support keyboard browsing (@todo),
- Automatically fallback to classic navigation for externals pages (thanks to Capitain Obvious help),
- Automatically fallback to classic navigation for internals pages that will not have the appropriated DOM tree,
- You can add pretty cool CSS transitions (animations) very easily.
- It's around 3kb (minified & gzipped).
### Under the hood
- It listen to every clicks on links _you want_ (by default all of them),
- When a internal link hitted, Pjax grabs HTML from your server via ajax,
- Pjax render pages DOM tree (without loading any resources - images, css, js...)
- It check if all defined parts can be replaced:
- if page doesn't suit requirement, classic navigation used,
- if page suits requirement, Pjax does all defined DOM replacements
- Then, it updates the browser's current url using pushState
## Overview
Pjax is fully automatic. You won't need to setup anything on the existing HTML.
You just need to designate some elements on your page that will be replaced when
you navigate your site.
Consider the following page.
```html
<!doctype html>
<html>
<head>
<!-- metas, title, styles, ... -->
</head>
<body>
<header class="my-Header"><nav><!-- a .is-active is in there --></nav></header>
<section class="my-Content">
Sha blah <a href="/blah ">blah</a>.
</section>
<aside class="my-Sidebar">Sidebar stuff</aside>
<footer class="my-Footer"></footer>
<script src="onDomReadystuff.js"></script>
<script><!-- analytics --></script>
</body>
</html>
```
We want Pjax to grab the url `/blah` then replace `.my-Content` with whatever it gets back.
Oh and the `<nav>` (that contains a status marker somewhere) can be updated too (or stay the same, as you wish).
And also the `<aside>` please.
So we want to update `[".my-Header", ".my-Content", ".my-Sidebar"]`, **without reloading styles nor scripts**.
We do this by telling Pjax to listen on `a` tags and use CSS selectors defined above (without forgetting minimal meta):
``` javascript
new Pjax({ selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"] })
```
Now when someone in a Pjax-compatible browser clicks "blah" the content of all selectors will be replaced with the one found in the "blah" content.
_Magic! For real!_ **There is completely no need to do anything on server side!**
## Differences with [jQuery-pjax](https://github.com/defunkt/jquery-pjax)
- No jQuery dependency,
- Not limited to a container,
- No server side requirements,
- Works for CommonJS environment (browserify), AMD (RequireJS) or even globally,
- Allow page transition with CSS animations,
- Can be easily hacked since every method is public (so overridable)
## Installation
You can install pjax from **npm**
```shell
$ npm install pjax
```
Or using **bower**
```shell
$ bower install pjax
```
Pjax can obviously be downloaded directly.
## No dependencies
_There is nothing you need. No jQuery or something._
## Compatibility
Pjax only works with [browsers that support the `history.pushState` API](http://caniuse.com/#search=pushstate).
When the API isn't supported Pjax goes into fallback mode (it just does nothing).
To see if Pjax is actually supported by your browser, use `Pjax.isSupported()`.
## Usage
### `new Pjax()`
Let's talk more about the most basic way to get started:
```js
new Pjax({
elements: "a" // default is "a[href], form[action]"
, selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"]
})
```
This will enable Pjax on all links and designate the part to replace using CSS selectors `"title", ".my-Header", ".my-Content", ".my-Sidebar"`.
For some reason, you might want to just target some elements to apply Pjax behavior.
In that case, you can 2 differents things:
- use a custom selector like "a.js-Pjax" or ".js-Pjax a" depending on what you want.
- override `Pjax.prototype.getElements` that just basically `querSelectorAll` the `elements` option. In this function you just need to return a `NodeList`.
```js
// use case 1
new Pjax({ elements: "a.js-Pjax" })
// use case 2
Pjax.prototype.getElements = function() {
return document.getElementsByClassName(".js-Pjax")
}
new Pjax({})
```
When instanciating a `Pjax` object, you need to pass all options as an object:
#### Options
##### `elements` (String, default "a[href], form[action]")
CSS Selector to use to retrieve links to apply Pjax
##### `selectors` (Array, default ["title", ".js-Pjax"])
CSS Selectors to replace. If a query returns multiples items, it will just keep the index.
Example of what you can do:
```html
<!doctype html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<header class="js-Pjax"></header>
<section class="js-Pjax">...</section>
<footer class="my-Footer"></footer>
<script>...</script>
</body>
</html>
```
This example is correct and should work "as expected".
_If there is not the same amount of DOM element from current page and new page,
the Pjax behavior will fallback to normal page load._
##### `switches` (Object, default {})
Objects containing callbacks that can be used to switch old element with new element.
Keys should be one of the defined selector.
Examples:
```js
new Pjax({
selectors: ["title", ".Navbar", ".js-Pjax"]
, switches: {
// "title": Pjax.switches.outerHTML // default behavior
".Navbar": function(oldEl, newEl, options) {
// here it's a stupid example since it's the default behavior too
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
},
".js-Pjax": Pjax.switches.sideBySide
}
})
```
Callbacks are binded to Pjax instance itself to allow you to reuse it (ex: `this.onSwitch()`)
###### Existing switches callback
- `Pjax.switches.outerHTML`: default behavior, replace elements using outerHTML
- `Pjax.switches.innerHTML`: replace elements using innerHTML & copy className too
- `Pjax.switches.sideBySide`: smart replacement that allow you to have both elements in the same parent when you want to use CSS animations. Old elements are removed when all childs have been fully animated ([animationEnd](http://www.w3.org/TR/css3-animations/#animationend) event triggered)
###### Create a switch callback
Your function can do whatever you want, be should
- replace oldEl content by newEl content in some fashion
- call `this.onSwitch()` to trigger attached callback.
Here is the default behavior as example
```js
function(oldEl, newEl, pjaxRequestOptions, switchesClasses) {
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
}
```
##### `switchesOptions` (Object, default {})
This are options that can be used during switch by switchers ( for now, only `Pjax.switches.sideBySide` use it).
Very convenient when you use something like [Animate.css](https://github.com/daneden/animate.css)
with or without [WOW.js](https://github.com/matthieua/WOW).
```js
new Pjax({
selectors: ["title", ".js-Pjax"]
, switches: {
".js-Pjax": Pjax.switches.sideBySide
}
, switchesClasses: {
".js-Pjax": {
classNames: {
// class added on the element that will be removed
remove: "Animated Animated--reverse Animate--fast Animate--noDelay"
// class added on the element that will be added
, add: "Animated"
// class added on the element when it go backward
, backward: "Animate--slideInRight"
// class added on the element when it go forward (used for new page too)
, forward: "Animate--slideInLeft"
}
, callbacks: {
// to make a nice transition with 2 pages as the same time
// we are playing with absolute positioning for element we are removing
// & we need live metrics to have something great
// see associated CSS below
removeElement: function(el) {
el.style.marginLeft = "-" + (el.getBoundingClientRect().width/2) + "px"
}
}
}
})
```
_Note that remove class include `Animated--reverse` which is a simple way to not have
to create duplicate transition for (slideIn + reverse => slideOut).
The following CSS will be required to make something nice
```css
/*
if your content elements doesn't have a fixed width,
you can get issue when absolute positioning will be used
so you will need that rules
*/
.js-Pjax { position: relative } /* parent element where switch will be made */
.js-Pjax-child { width: 100% }
/* position for the elements that will be removed */
.js-Pjax-remove {
position: absolute;
left: 50%;
/* transform: translateX(-50%) */
/* transform can't be used since we already use generic translate for the remove effect (eg animate.css) */
/* margin-left: -width/2; // made with js */
/* you can totally drop the margin-left thing from switchesOptions if you use custom animations */
}
/* CSS animations */
.Animated {
animation-fill-mode: both;
animation-duration: 1s;
}
.Animated--reverse { animation-direction: reverse }
.Animate--fast { animation-duration: .5s }
.Animate--noDelay { animation-delay: 0s !important; }
.Animate--slideInRight { animation-name: Animation-slideInRight }
@keyframes Animation-slideInRight {
0% {
opacity: 0;
transform: translateX(100rem);
}
100% {
transform: translateX(0);
}
}
.Animate--slideInLeft { animation-name: Animation-slideInLeft }
@keyframes Animation-slideInLeft {
0% {
opacity: 0;
transform: translateX(-100rem);
}
100% {
transform: translateX(0);
}
}
```
To get understand this CSS, here is a HTML snippet
```html
<!doctype html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<section class="js-Pjax">
<div class="js-Pjax-child">
Your content here
</div>
<!--
when switching will be made you will have the following tree
<div class="js-Pjax-child js-Pjax-remove Animate...">
Your OLD content here
</div>
<div class="js-Pjax-child js-Pjax-add Animate...">
Your NEW content here
</div>
-->
</section>
<script>...</script>
</body>
</html>
```
##### `history` (Boolean, default true)
Enable pushState. Only disable if you are crazy.
Internaly, this option is used when `popstate` is used (to not pushState again).
You should forget that option.
##### `analytics` (Function, default to a function that push `_gaq` `trackPageview` or send `ga` `pageview`
Function that allow you to add behavior for analytics. By default it try to track
a pageview with Google Analytics.
It's called every time a page is switched, even for history buttons.
##### `scrollTo` (Integer, default to 0)
Value (in px) to scrollTo when a page is switched.
##### `debug` (Boolean, default to false)
Enable verbose mode & doesn't use fallback when there is an error.
Useful to debug page layout differences.
#### Extend Pjax
Pjax prototype & utilities methods can be used & changed so you can patch or hack
Pjax behavior, as you wish.
Here is a summary of functions:
- `Pjax.isSupported` (`function()`): return wheter or not the browser handle pushState correctly
- `Pjax.on` (`function(els, events, listener, useCapture)`): addEventListener, that handles NodeList & supports space separated event name
- `Pjax.off` (`function(els, events, listener, useCapture)`): removeEventListener, that handles NodeList & supports space separated event name
- `Pjax.trigger` (`function(els, events)`): fireEvent, that handles NodeList & supports space separated event name
- `Pjax.clone` (`function(obj)`): clone object
- `Pjax.executeScripts` (`function(el)`): execute scripts that are inside an element (script src or inline scripts through `Pjax.evalScript`)
- `Pjax.evalScript` (`function(el)`): execute inline script. Don't execute a script if it contains `document.write`.
- `Pjax.prototype.log` (`function()`): console.log function that is enable/disabled by `debug` option
- `Pjax.prototype.getElements` (`function(el)`): retrieve elements to attach Pjax behavior
- `Pjax.prototype.parseDOM` (`function(el)`): parse DOM to attach behavior using `Pjax.prototype.getElements` & `Pjax.prototype.attachLink`
- `Pjax.prototype.attachLink` (`function(el)`): attach Pjax behavior to a link
- `Pjax.prototype.forEachSelectors` (`function(cb, context, DOMcontext)`): call a function for each selectors defined
- `Pjax.prototype.switchSelectors` (`function(selectors, fromEl, toEl, options)`): loop on selectors to switch elements
- `Pjax.prototype.latestChance` (`function(href)`): when everything is fucked up, it's our only hope (just call `window.location = href`)
- `Pjax.prototype.onSwitch` (`function()`): callback triggered when elements are switched, for now it's just trigger a window resize event (lots of lib are listening to this event to draw stuff)
- `Pjax.prototype.loadContent` (`function(html, options)`): switch elements for each selectors
- `Pjax.prototype.doRequest` (`function(location, callback)`): make the ajax request to grab page from the server
- `Pjax.prototype.loadUrl` (`function(href, options)`): do the ajax request, handle html results & eventually handle browser history, analytics & scroll.
### Events
Pjax fires a number of events regardless of how its invoked.
All events are fired from the _document_, not the link was clicked.
#### Ajax related events
* `pjax:send` - Fired after the Pjax request begins.
* `pjax:complete` - Fired after the Pjax request finishes.
* `pjax:success` - Fired after the Pjax request succeeds.
* `pjax:error` - Fired after the Pjax request fails. Returning false will prevent the the fallback redirect.
`send` and `complete` are a good pair of events to use if you are implementing a loading indicator (eg: [topbar](http://buunguyen.github.io/topbar/))
```js
$(document).on('pjax:send', topbar.show)
$(document).on('pjax:complete', topbar.hide)
```
#### Note about DOM ready state
Most of the time, you have code attached & related to the current DOM, that you only execute when page/dom is ready.
Since Pjax doesn't magically rexecute you previous code each time you load a page, you need to make a simple thing to rexecute appropriate code:
```js
function whenDOMReady() {
// do you stuff
}
whenDOMReady()
new Pjax()
document.addEventListener("pjax:success", whenDOMReady)
```
_Note: Don't create the Pjax in the `whenDOMReady` function._
For my concern & usage, I `js-Pjax`ify all body children, including stuff like navigation & footer (to get navigation state easily updated).
So attached behavior are rexecuted each time a page is loaded, like in the snippet above.
If you want to just update a specific part (it's totally a good idea), you can just
add the DOM related code in a function & rexecute this function when "pjax:success" event is done.
```js
// do your global stuff
//... dom ready blah blah
function whenContainerReady() {
// do your container related stuff
}
whenContainerReady()
new Pjax()
document.addEventListener("pjax:success", whenContainerReady)
```
## [Contributing]
Please read the file nobody reads (make me lie) [CONTRIBUTING.md](CONTRIBUTING.md)
### tl;dr;
Fork, clone, then
```shell
$ npm i -g gulp
$ npm i
$ gulp
```
Now you can work on the file, then make a commit and a push something when gulp doesn't show any error.
Thanks.
## [CHANGELOG](CHANGELOG.md)
## [License](LICENSE-MIT)

11
TODO.md Normal file
View File

@@ -0,0 +1,11 @@
## @todo
- Add tests, like a lot
- handle hash in url (see https://github.com/defunkt/jquery-pjax/blob/master/jquery.pjax.js#L287-L303)
- add keyboard support
- add timeout option (600ms ?)
- add form support
- add cache
- abort previous pjax request when several requests are made by a crazy person clicking everywhere
- better switchFallback ?
- handle document.write scripts ?

31
gulpfile.js Normal file
View File

@@ -0,0 +1,31 @@
///
var pkg = require("./package.json")
, gulp = require("gulp")
, plumber = require("gulp-plumber")
///
// Lint JS
///
var jshint = require("gulp-jshint")
, jsFiles = [".jshintrc", "*.json", "*.js"]
gulp.task("scripts.lint", function() {
gulp.src(jsFiles)
.pipe(plumber())
.pipe(jshint(".jshintrc"))
.pipe(jshint.reporter("jshint-stylish"))
})
var jscs = require("gulp-jscs")
gulp.task("scripts.cs", function() {
gulp.src("*.js")
.pipe(plumber())
.pipe(jscs())
})
gulp.task("scripts", ["scripts.lint", "scripts.cs"])
gulp.task("watch", function() {
gulp.watch([jsFiles], ["scripts"])
})
gulp.task("default", ["scripts", "watch"])

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "pjax",
"version": "0.0.0",
"description": "Boost browsing experience using Ajax navigation (+ Push state)",
"main": "pjax.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/MoOx/pjax.git"
},
"keywords": [
"pjax",
"push",
"state",
"ajax",
"navigation",
"transition",
"animation"
],
"author": "MoOx <m@moox.io>",
"license": "MIT",
"bugs": {
"url": "https://github.com/MoOx/pjax/issues"
},
"homepage": "https://github.com/MoOx/pjax",
"devDependencies": {
"jshint-stylish": "^0.1.5",
"gulp-jscs": "^0.3.2",
"gulp-plumber": "^0.5.6",
"gulp": "^3.5.6",
"gulp-jshint": "^1.5.1"
}
}

585
pjax.js Normal file
View File

@@ -0,0 +1,585 @@
(function(root, factory) {
if (typeof exports === "object") {
// CommonJS
module.exports = factory()
}
else if (typeof define === "function" && define.amd) {
// AMD
define([], factory)
}
else {
// Global Variables
root.Pjax = factory()
}
}(this, function() {
function newUid() {
return (new Date().getTime())
}
var Pjax = function(options) {
this.firstrun = true
this.options = options
this.options.elements = this.options.elements || "a[href], form[action]"
this.options.selectors = this.options.selectors || ["title", ".js-Pjax"]
this.options.switches = this.options.switches || {}
this.options.switchesOptions = this.options.switchesOptions || {}
this.options.history = this.options.history || true
this.options.analytics = this.options.analytics || function(options) {
// options.backward or options.foward can be true or undefined
// by default, we do track back/foward hit
// https://productforums.google.com/forum/#!topic/analytics/WVwMDjLhXYk
if (window._gaq) {
_gaq.push(["_trackPageview"])
}
if (window.ga) {
ga("send", "pageview", {"page": options.url, "title": options.title})
}
}
this.options.scrollTo = this.options.scrollTo || 0
this.options.debug = this.options.debug || false
this.maxUid = this.lastUid = newUid()
// we cant replace body.outerHTML or head.outerHTML
// it create a bug where new body or new head are created in the dom
// if you set head.outerHTML, a new body tag is appended, so the dom get 2 body
// & it break the switchFallback which replace head & body
if (!this.options.switches.head) {
this.options.switches.head = this.switchElementsAlt
}
if (!this.options.switches.body) {
this.options.switches.body = this.switchElementsAlt
}
this.log("Pjax options", this.options)
if (typeof options.analytics !== "function") {
options.analytics = function() {}
}
this.parseDOM(document)
Pjax.on(window, "popstate", function(st) {
if (st.state) {
var opt = Pjax.clone(this.options)
opt.url = st.state.url
opt.title = st.state.title
opt.history = false
if (st.state.uid < this.lastUid) {
opt.backward = true
}
else {
opt.forward = true
}
this.lastUid = st.state.uid
// @todo implement history cache here, based on uid
this.loadUrl(st.state.url, opt)
}
}.bind(this))
}
// make internal methods public
Pjax.isSupported = function() {
// Borrowed wholesale from https://github.com/defunkt/jquery-pjax
return window.history &&
window.history.pushState &&
window.history.replaceState &&
// pushState isnt reliable on iOS until 5.
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
}
Pjax.forEachEls = function(els, fn, context) {
if (els instanceof HTMLCollection || els instanceof NodeList) {
return Array.prototype.forEach.call(els, fn, context)
}
// assume simple dom element
fn.call(context, els)
}
Pjax.on = function(els, events, listener, useCapture) {
events = (typeof events === "string" ? events.split(" ") : events)
events.forEach(function(e) {
Pjax.forEachEls(els, function(el) {
el.addEventListener(e, listener, useCapture)
})
}, this)
}
Pjax.off = function(els, events, listener, useCapture) {
events = (typeof events === "string" ? events.split(" ") : events)
events.forEach(function(e) {
Pjax.forEachEls(els, function(el) {
el.removeEventListener(e, listener, useCapture)
})
}, this)
}
Pjax.trigger = function(els, events) {
events = (typeof events === "string" ? events.split(" ") : events)
events.forEach(function(e) {
var event
if (document.createEvent) {
event = document.createEvent("HTMLEvents")
event.initEvent(e, true, true)
}
else {
event = document.createEventObject()
event.eventType = e
}
event.eventName = e
if (document.createEvent) {
Pjax.forEachEls(els, function(el) {
el.dispatchEvent(event)
})
}
else {
Pjax.forEachEls(els, function(el) {
el.fireEvent("on" + event.eventType, event)
})
}
}, this)
}
Pjax.clone = function(obj) {
if (null === obj || "object" != typeof obj) {
return obj
}
var copy = obj.constructor()
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = obj[attr]
}
}
return copy
}
// Finds and executes scripts (used for newly added elements)
// Needed since innerHTML does not run scripts
Pjax.executeScripts = function(el) {
// console.log("going to execute scripts for ", el)
Pjax.forEachEls(el.querySelectorAll("script"), function(script) {
if (!script.type || script.type.toLowerCase() === "text/javascript") {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
Pjax.evalScript(script)
}
})
}
Pjax.evalScript = function(el) {
// console.log("going to execute script", el)
var code = (el.text || el.textContent || el.innerHTML || "")
, head = document.querySelector("head") || document.documentElement
, script = document.createElement("script")
if (code.match("document.write")) {
if (console && console.log) {
console.log("Script contains document.write. Cant be executed correctly. Code skipped ", el)
}
return false
}
script.type = "text/javascript"
try {
script.appendChild(document.createTextNode(code))
}
catch (e) {
// old IEs have funky script nodes
script.text = code
}
// execute
head.insertBefore(script, head.firstChild)
head.removeChild(script) // avoid pollution
return true
}
Pjax.prototype = {
log: function() {
if (this.options.debug && console) {
if (typeof console.log === "function") {
console.log.apply(console, arguments);
}
// ie is weird
else if (console.log) {
console.log(arguments);
}
}
}
, getElements: function(el) {
return el.querySelectorAll(this.options.elements)
}
, parseDOM: function(el) {
Pjax.forEachEls(this.getElements(el), function(el) {
switch (el.tagName.toLowerCase()) {
case "a": this.attachLink(el)
break
case "form":
// todo
this.log("Pjax doesnt support <form> yet. TODO :)")
break
default:
throw "Pjax can only be applied on <a> or <form> submit"
}
}, this)
}
, attachLink: function(el) {
Pjax.on(el, "click", function(event) {
//var el = event.currentTarget
// Dont break browser special behavior on links (like page in new window)
if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return -1
}
// Ignore external links.
if (el.protocol !== window.location.protocol || el.host !== window.location.host) {
return -2
}
// Ignore anchors on the same page
if (el.pathname === location.pathname && el.hash.length > 0) {
return -3
}
// Ignore anchors on the same page
if (el.hash && el.href.replace(el.hash, "") === location.href.replace(location.hash, "")) {
return -4
}
// Ignore empty anchor "foo.html#"
if (el.href === location.href + "#") {
return -5
}
event.preventDefault()
// dont do "nothing" if user try to reload the page
if (el.href === window.location.href) {
window.location.reload()
return -6
}
this.loadUrl(el.href, Pjax.clone(this.options))
}.bind(this))
Pjax.on(el, "keyup", function(event) {
this.log("pjax todo")
// todo handle a link hitted by keyboard (enter/space) when focus is on it
}.bind(this))
}
, forEachSelectors: function(cb, context, DOMcontext) {
DOMcontext = DOMcontext || document
this.options.selectors.forEach(function(selector) {
Pjax.forEachEls(DOMcontext.querySelectorAll(selector), cb, context)
})
}
, switchSelectors: function(selectors, fromEl, toEl, options) {
selectors.forEach(function(selector) {
var newEls = fromEl.querySelectorAll(selector)
var oldEls = toEl.querySelectorAll(selector)
this.log("Pjax switch", selector, newEls, oldEls)
if (newEls.length !== oldEls.length) {
// Pjax.forEachEls(newEls, function(el) {
// this.log("newEl", el, el.outerHTML)
// }, this)
// Pjax.forEachEls(oldEls, function(el) {
// this.log("oldEl", el, el.outerHTML)
// }, this)
throw "DOM doesnt look the same on new loaded page: " + selector + " - new " + newEls.length + ", old " + oldEls.length
}
Pjax.forEachEls(newEls, function(newEl, i) {
var oldEl = oldEls[i]
this.log("newEl", newEl, "oldEl", oldEl)
if (this.options.switches[selector]) {
this.options.switches[selector].bind(this)(oldEl, newEl, options, this.options.switchesOptions[selector])
}
else {
Pjax.switches.outerHTML.bind(this)(oldEl, newEl, options)
}
}, this)
}, this)
}
// too much problem with the code below
// + its too dangerous
// , switchFallback: function(fromEl, toEl) {
// this.switchSelectors(["head", "body"], fromEl, toEl)
// // execute script when DOM is like it should be
// Pjax.executeScripts(document.querySelector("head"))
// Pjax.executeScripts(document.querySelector("body"))
// }
, latestChance: function(href) {
window.location = href
}
, onSwitch: function() {
Pjax.trigger(window, "resize scroll")
}
, loadContent: function(html, options) {
var tmpEl = document.implementation.createHTMLDocument()
tmpEl.documentElement.innerHTML = html
// this.log("load content", tmpEl.documentElement.innerHTML)
// try {
this.switchSelectors(this.options.selectors, tmpEl, document, options)
// FF bug: Wont autofocus fields that are inserted via JS.
// This behavior is incorrect. So if theres no current focus, autofocus
// the last field.
//
// http://www.w3.org/html/wg/drafts/html/master/forms.html
var autofocusEl = Array.prototype.slice.call(document.querySelectorAll("[autofocus]")).pop()
if (autofocusEl && document.activeElement !== autofocusEl) {
autofocusEl.focus();
}
// execute scripts when DOM have been completely updated
this.options.selectors.forEach(function(selector) {
Pjax.forEachEls(document.querySelectorAll(selector), function(el) {
Pjax.executeScripts(el)
})
})
// }
// catch(e) {
// if (this.options.debug) {
// this.log("Pjax switch fail: ", e)
// }
// this.switchFallback(tmpEl, document)
// }
}
, doRequest: function(location, callback) {
var request = new XMLHttpRequest()
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
callback(request.responseText)
}
else if (request.readyState === 4 && (request.status === 404 || request.status === 500)){
callback(false)
}
}
request.open("GET", location + (!/[?&]/.test(location) ? "?" : "&") + (new Date().getTime()), true)
request.setRequestHeader("X-Requested-With", "XMLHttpRequest")
request.send(null)
}
, loadUrl: function(href, options) {
this.log("load href", href, options)
Pjax.trigger(document, "pjax:send", options);
// Do the request
this.doRequest(href, function(html) {
// Fail if unable to load HTML via AJAX
if (html === false) {
Pjax.trigger(document,"pjax:complete pjax:error", options)
return
}
// Clear out any focused controls before inserting new page contents.
document.activeElement.blur()
try {
this.loadContent(html, options)
}
catch (e) {
if (!this.options.debug) {
if (console && console.error) {
console.error("Pjax switch fail: ", e)
}
this.latestChance(href)
return
}
else {
throw e
}
}
if (options.history) {
if (this.firstrun) {
this.lastUid = this.maxUid = newUid()
this.firstrun = false
window.history.replaceState({
"url": window.location.href
, "title": document.title
, "uid": this.maxUid
}
, document.title)
}
// Update browser history
this.lastUid = this.maxUid = newUid()
window.history.pushState({
"url": href
, "title": options.title
, "uid": this.maxUid
}
, options.title
, href)
}
this.forEachSelectors(function(el) {
this.parseDOM(el)
}, this)
// Fire Events
Pjax.trigger(document,"pjax:complete pjax:success", options)
options.analytics()
// Scroll page to top on new page load
if (options.scrollTo !== false) {
if (options.scrollTo.length > 1) {
window.scrollTo(options.scrollTo[0], options.scrollTo[1])
}
else {
window.scrollTo(0, options.scrollTo)
}
}
}.bind(this))
}
}
Pjax.switches = {
outerHTML: function(oldEl, newEl, options) {
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
}
, innerHTML: function(oldEl, newEl, options) {
oldEl.innerHTML = newEl.innerHTML
oldEl.className = newEl.className
this.onSwitch()
}
, sideBySide: function(oldEl, newEl, options, switchOptions) {
var elsToRemove = []
, elsToAdd = []
, fragToAppend = document.createDocumentFragment()
// height transition are shitty on safari
// so commented for now (until I found something ?)
// , relevantHeight = 0
, animationEventNames = "animationend webkitAnimationEnd MSAnimationEnd oanimationend"
, animatedElsNumber = 0
, sexyAnimationEnd = function(e) {
if (e.target != e.currentTarget) {
// end triggered by an animation on a child
return
}
animatedElsNumber--
if (animatedElsNumber <= 0 && elsToRemove) {
elsToRemove.forEach(function(el) {
// browsing quickly can make the el
// already removed by last page update ?
if (el.parentNode) {
el.parentNode.removeChild(el)
}
})
elsToAdd.forEach(function(el) {
el.className = el.className.replace(el.getAttribute("data-pjax-classes"), "")
el.removeAttribute("data-pjax-classes")
// Pjax.off(el, animationEventNames, sexyAnimationEnd, true)
})
elsToAdd = null // free memory
elsToRemove = null // free memory
// assume the height is now useless (avoid bug since there is overflow hidden on the parent)
// oldEl.style.height = "auto"
// this is to trigger some repaint (example: picturefill)
this.onSwitch()
//Pjax.trigger(window, "scroll")
}
}.bind(this)
// Force height to be able to trigger css animation
// here we get the relevant height
// oldEl.parentNode.appendChild(newEl)
// relevantHeight = newEl.getBoundingClientRect().height
// oldEl.parentNode.removeChild(newEl)
// oldEl.style.height = oldEl.getBoundingClientRect().height + "px"
forEach.call(oldEl.childNodes, function(el) {
elsToRemove.push(el)
if (el.classList && !el.classList.contains("js-Pjax-remove")) {
// for fast switch, clean element that just have been added, & not cleaned yet.
if (el.hasAttribute("data-pjax-classes")) {
el.className = el.className.replace(el.getAttribute("data-pjax-classes"), "")
el.removeAttribute("data-pjax-classes")
}
el.classList.add("js-Pjax-remove")
if (switchOptions.callbacks && switchOptions.callbacks.removeElement) {
switchOptions.callbacks.removeElement(el)
}
el.className += " " + switchOptions.classNames.remove + " " + (options.backward ? switchOptions.classNames.backward : switchOptions.classNames.forward)
animatedElsNumber++
Pjax.on(el, animationEventNames, sexyAnimationEnd, true)
}
})
forEach.call(newEl.childNodes, function(el) {
if (el.classList) {
var addClasses = " js-Pjax-add " + switchOptions.classNames.add + " " + (options.backward ? switchOptions.classNames.forward : switchOptions.classNames.backward)
if (switchOptions.callbacks && switchOptions.callbacks.addElement) {
switchOptions.callbacks.addElement(el)
}
el.className += addClasses
el.setAttribute("data-pjax-classes", addClasses)
elsToAdd.push(el)
fragToAppend.appendChild(el)
animatedElsNumber++
Pjax.on(el, animationEventNames, sexyAnimationEnd, true)
}
})
// pass all className of the parent
oldEl.className = newEl.className
oldEl.appendChild(fragToAppend)
// oldEl.style.height = relevantHeight + "px"
}
}
if (Pjax.isSupported()) {
return Pjax
}
// if there isnt required browser functions, returning stupid api
else {
var stupidPjax = function() {}
for (var key in Pjax.prototype) {
if (Pjax.prototype.hasOwnProperty(key) && typeof Pjax.prototype[key] === "function") {
stupidPjax[key] = stupidPjax
}
}
return stupidPjax
}
}))