Compare commits

..

36 Commits

Author SHA1 Message Date
dependabot[bot]
d27c8cbdba Bump hosted-git-info from 2.5.0 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.5.0 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.5.0...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-09 11:22:06 +00:00
dependabot[bot]
480334b182 Bump stringstream from 0.0.5 to 0.0.6 (#217)
Bumps [stringstream](https://github.com/mhart/StringStream) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/mhart/StringStream/releases)
- [Commits](https://github.com/mhart/StringStream/compare/v0.0.5...v0.0.6)

Signed-off-by: dependabot[bot] <support@github.com>
2019-10-10 18:00:57 +02:00
Behind The Math
c26c223a65 0.2.8 2019-03-09 22:56:16 -05:00
Behind The Math
3b3f4d7794 Update README with instructions for building from the source 2019-03-04 09:34:44 -05:00
BehindTheMath
7940a6e3e5 Handle non-string HTML passed to loadContent() (#200)
Fixes #187.
2019-03-04 09:32:27 -05:00
BehindTheMath
493d56c2d0 Update README (#199)
* Move the npm installation option to last.
* Clarify that index.js is not a bundle
2019-03-03 07:45:34 +01:00
Behind The Math
c13149626b Prettier fixes 2019-03-03 01:37:45 -05:00
BehindTheMath
3c1a4b2e18 Switch linting to ESLint and Prettier (#191)
* Switch linting to ESLint and Prettier
* Clean up config
* Prettier fixes
2019-02-13 22:26:57 -05:00
BehindTheMath
2c6506af65 Fix evalScripts() (#186)
* Set the id of the inserted <script>.
* Check if the <script> still exists before trying to remove it.
2018-11-25 15:21:07 -05:00
BehindTheMath
fefb63ae87 Remove keyup event listener for forms (#184)
Let the browser take care of when to do implicit submission.
2018-11-20 21:17:54 -05:00
Robin North
52fb3bf938 Fix Edge form support (#178)
* Fix looping through form elements in Edge

* Update lockfile
2018-10-10 13:21:04 -04:00
Nathaniel Watts
6b648a7c90 Edit README (#176) 2018-09-22 21:21:50 -04:00
Behind The Math
03ebc657f0 0.2.7 2018-08-15 15:26:02 -04:00
BehindTheMath
6f39767cf9 Ensure correct XHR encoding for multipart/form-data forms (#174)
Fixes #168
2018-08-15 15:07:04 -04:00
BehindTheMath
03d64863c8 Fix README. Also pass the current options object to loadContent() (#171)
Fixes #167
2018-07-23 20:46:13 -04:00
BehindTheMath
c36225a24c Fix options.history to correctly parse being set to false (#165)
Fixes #164
2018-06-18 15:42:42 -04:00
Behind The Math
c589ab9c25 Add index.d.ts to package.json so it will be installed by npm 2018-06-17 22:29:58 -04:00
BehindTheMath
8abb21e1e9 Fix parsing values of option elements in forms (#162)
* Fix a bug where the value of <option> would not get sent if falsy

According to the spec, the value attribute should be sent if it
exists, even if it's falsy.

* Don't send an <option> tag if it's disabled, even if it's selected
2018-05-30 15:39:08 -04:00
Robin North
8dbe7553b9 Document refresh and reload methods (#160) 2018-05-29 22:23:23 -04:00
Behind The Math
f639a8eae1 0.2.6 2018-04-30 15:10:24 -04:00
BehindTheMath
e49d8947f7 Add the option to use FormData to encode the form elements (#153)
* Add the option to use FormData to encode the form elements

If the form's enctype attribute is set to "multipart/form-data",
use FormData to encode the form's elements.
2018-04-29 15:05:22 -04:00
BehindTheMath
7d26a75fdf Use the same options object in handle-response as in send-request (#148)
Instead of cloning this.options again in handle-response.js, pass the options object from send-request.js. This way, pjax.state.options will also have the request options.
2018-04-26 09:27:50 -04:00
Robin North
358b6f6836 Support multiple select fields in form submissions 2018-04-12 18:03:28 -04:00
Robin North
d3447a95aa Track npm lockfile 2018-04-12 11:50:58 +01:00
BehindTheMath
d6bf21ed22 Fix bugs and add tests (#145)
* Fix bug when checking if elements were parsed already

parse-element.js checks if the element was already parsed by
checking for the `data-pjax-click-state` attribute. However, this
attribute was not added until the link is clicked.

Originally, there was a separate attribute, `data-pjax-enabled`,
which tracked if the element was parsed already, but that was
changed in 9a86044.

This commit merges the attributes for mouse clicks and key presses
into one and adds that attribute when the element is initially
parsed.

* More bug fixes

* Fix documentation for currentUrlFullReload

* Ignore lines from coverage if they can't be tested

* Refactor attach-link and attach-form

* Fix and refactors tests

* Add tests

* Add TS definitions for options.requestOptions

* Code cleanup
2018-04-09 23:36:32 -04:00
Behind The Math
17d8262025 Add X-PJAX-Selectors header 2018-04-09 23:28:46 -04:00
BehindTheMath
75eb83dbc2 Add replaceNode switch (#141)
* Add replaceNode switch

* Add test for replaceNode()

* Update TS definitions
2018-03-20 10:52:55 -04:00
BehindTheMath
5e41a32cf4 Add Typescript definitions (#138)
* Add Typescript definitions
* Add test file for TS
2018-03-15 21:16:11 -04:00
Behind The Math
a2982cfcba Edit README to assign the Pjax instance to a variable 2018-03-15 16:38:03 -04:00
BehindTheMath
2166866967 Handle XHR response error (#137)
* Move the XHR callback to a separate file
* Trigger an error event if the response cannot be parsed.
* Add tests for handle-response.js
2018-03-15 16:12:32 -04:00
Robin North
c1e5bf9c78 Update README 2018-03-15 09:15:13 +00:00
Behind The Math
333ee344f4 Fix bug in contains() argument list
Fixes #135
2018-03-09 09:32:58 -05:00
Robin North
05fa833169 loadUrl enhancements (#134)
`loadUrl` enhancements

- Make `options` parameter optional
- Allow partial overriding of instance options when calling `loadUrl` directly
- Make `requestOptions` optional
- Document `loadUrl` usage and provide examples
2018-03-06 10:06:38 +00:00
Robin North
f98f2dd63b Add tests for update-query-string 2018-03-04 15:12:53 +00:00
Robin North
07baae8e4d Fix form submission (#129)
* Fix check for radio and checkbox inputs

* Fix GET form submission

* Add example forms for testing

* Refactor query string building
2018-03-02 20:25:08 +00:00
Maxime Thirouin
0c7af354fd Update README.md 2018-02-09 14:12:12 +01:00
68 changed files with 12598 additions and 1441 deletions

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
pjax.js
pjax.min.js
*.json

8
.eslintrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": ["eslint-config-i-am-meticulous/es5"],
"rules": {
"import/order": "off",
"import/max-dependencies": "off",
"import/extensions": ["error", "never"]
}
}

5
.gitignore vendored
View File

@@ -1,6 +1,9 @@
.DS_Store
node_modules/
.nyc_output/
.idea/
dist/
coverage/
tests/scripts/index.html
pjax.js
.nyc_output/
pjax.min.js

132
.jscsrc
View File

@@ -1,132 +0,0 @@
{
"excludeFiles": [
"node_modules/**",
"pjax.js",
"pjax.min.js"
],
"fileExtensions": [
".js"
],
"requireCurlyBraces": [
"if",
"else",
"for",
"while",
"do",
"try",
"catch"
],
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"return",
"try",
"catch"
],
"requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true,
"requireSpacesInConditionalExpression": {
"afterTest": true,
"beforeConsequent": true,
"afterConsequent": true,
"beforeAlternate": true
},
"requireSpacesInFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"disallowSpacesInFunctionExpression": {
"beforeOpeningRoundBrace": true
},
"disallowMultipleVarDecl": true,
"requireBlocksOnNewline": 1,
"disallowPaddingNewlinesInBlocks": true,
"disallowEmptyBlocks": true,
"disallowSpacesInsideObjectBrackets": true,
"disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true,
"disallowQuotedKeysInObjects": "allButReserved",
"disallowSpaceAfterObjectKeys": true,
"requireCommaBeforeLineBreak": true,
"requireOperatorBeforeLineBreak": [
"?",
"+",
"-",
"/",
"*",
"=",
"==",
"===",
"!=",
"!==",
">",
">=",
"<",
"<="
],
"disallowSpaceAfterPrefixUnaryOperators": [
"++",
"--",
"+",
"-",
"~",
"!"
],
"disallowSpaceBeforePostfixUnaryOperators": [
"++",
"--"
],
"requireSpaceBeforeBinaryOperators": [
"+",
"-",
"/",
"*",
"=",
"==",
"===",
"!=",
"!=="
],
"requireSpaceAfterBinaryOperators": [
"+",
"-",
"/",
"*",
"=",
"==",
"===",
"!=",
"!=="
],
"disallowImplicitTypeConversion": [
"numeric",
"boolean",
"binary",
"string"
],
"requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties",
"disallowKeywords": [
"with"
],
"disallowMultipleLineStrings": true,
"validateQuoteMarks": "\"",
"validateIndentation": 2,
"disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true,
"requireKeywordsOnNewLine": [
"else"
],
"requireLineFeedAtFileEnd": true,
"requireCapitalizedConstructors": true,
"safeContextKeyword": "that",
"requireDotNotation": true,
"jsDoc": {
"checkParamNames": true,
"checkRedundantParams": true,
"requireParamTypes": true
},
"requireSpaceAfterLineComment": true
}

View File

@@ -1,9 +0,0 @@
{
"newcap": false,
"undef": true,
"unused": true,
"asi": true,
"esnext": true,
"node": true,
"browser": true
}

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
pjax.js
pjax.min.js
*.json

View File

@@ -1,7 +1,4 @@
language: "node_js"
node_js:
- "6"
- "8"
# Force Travis to use npm v5
# https://github.com/travis-ci/travis-ci/issues/4653#issuecomment-194051953
before_install: if [[ `npm -v` != 5* ]]; then npm i -g npm@5; fi
- "10"

View File

@@ -1,3 +1,54 @@
# 0.2.8 - 2019-03-09
- Fixed: Edge form support.
([#178](https://github.com/MoOx/pjax/pull/178) - @robinnorth)
- Fixed: Removed keyup event listener for forms.
([#184](https://github.com/MoOx/pjax/pull/184) - @BehindTheMath)
- Fixed: Bugs in evalScripts().
([#186](https://github.com/MoOx/pjax/pull/186) - @BehindTheMath)
- Fixed: Handle non-string HTML passed to loadContent().
([#200](https://github.com/MoOx/pjax/pull/200) - @BehindTheMath)
- Tooling: Switch linting to ESLint and Prettier.
([#191](https://github.com/MoOx/pjax/pull/191) - @BehindTheMath)
# 0.2.7 - 2018-08-15
- Fixed: Parsing values of option elements in forms.
([#162](https://github.com/MoOx/pjax/pull/162) - @BehindTheMath)
- Fixed: Added index.d.ts to package.json so it will be installed by npm.
([c589ab9](https://github.com/MoOx/pjax/commit/c589ab9c25bee6161bf3e557eaca44e51c14fb89) - @BehindTheMath)
- Fixed: `options.history` to correctly parse being set to false.
([#165](https://github.com/MoOx/pjax/pull/165) - @BehindTheMath).
- Fixed: Pass the current `options` object to `loadContent()`.
([#171](https://github.com/MoOx/pjax/pull/171) - @BehindTheMath)
- Fixed: Ensure correct XHR encoding for multipart/form-data forms
([#174](https://github.com/MoOx/pjax/pull/174) - @BehindTheMath)
- Added: More documentation.
([#160](https://github.com/MoOx/pjax/pull/160), [#171](https://github.com/MoOx/pjax/pull/171) - @robinnorth, @BehindTheMath)
# 0.2.6 - 2018-04-30
- Fixed: Form submission for GET requests.
([#129](https://github.com/MoOx/pjax/pull/129) - @robinnorth)
- Fixed: Refactor `loadUrl()` to make manually calling simpler.
([#134](https://github.com/MoOx/pjax/pull/134) - @robinnorth)
- Fixed: Support multiple select fields in form submissions.
([#147](https://github.com/MoOx/pjax/pull/147) - @robinnorth)
- Fixed: Use the same options object in `handle-response` as in `send-request`. This way, `pjax.state.options` will also have the request options.
([#148](https://github.com/MoOx/pjax/pull/148) - @BehindTheMath)
- Added: Move the XHR callback to a separate method, and trigger an error event if the response cannot be parsed.
([#137](https://github.com/MoOx/pjax/pull/137) - @BehindTheMath)
- Added: TypeScript definitions.
([#138](https://github.com/MoOx/pjax/pull/138) - @BehindTheMath)
- Added: `replaceNode` switch, as an alternative to the `outerHTML` switch.
([#141](https://github.com/MoOx/pjax/pull/141) - @BehindTheMath)
- Added: `X-PJAX-Selectors` HTTP header. This is a serialized JSON array of selectors, taken from `options.selectors`. You can use this to send back only the elements that Pjax will use to switch, instead of sending the whole page.
([#144](https://github.com/MoOx/pjax/pull/144) - @BehindTheMath)
- Added: An option to use `FormData` to submit forms.
([#153](https://github.com/MoOx/pjax/pull/153) - @BehindTheMath)
- Added: Tests.
([f98f2dd](https://github.com/MoOx/pjax/commit/f98f2dd63b48113ff91b6bd8808257bfc723ef6b), [#145](https://github.com/MoOx/pjax/pull/145) - @robinnorth, @BehindTheMath)
# 0.2.5 - 2018-02-02
- Fixed: Async switch functions now work correctly, because the DOM is now parsed after all the switches finish.

527
README.md
View File

@@ -1,68 +1,79 @@
# Pjax
[![Build Status](http://img.shields.io/travis/MoOx/pjax.svg)](https://travis-ci.org/MoOx/pjax).
[![Build Status](https://img.shields.io/travis/MoOx/pjax.svg)](https://travis-ci.org/MoOx/pjax).
> Easily enable fast AJAX navigation on any website (using pushState() + XHR)
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.
Pjax is **a standalone JavaScript module** that uses [AJAX](https://developer.mozilla.org/en-US/docs/Web/Guide/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 allows you to completely transform the user experience of standard websites
(server-side generated or static ones) to make them feel like they are browsing an app,
especially for users with low bandwidth connection._
_It allows you to completely transform the user experience of standard websites (server-side generated or static ones) to make users feel like they are browsing an app, especially for those with low bandwidth connections._
**No more full page reloads. No more multiple HTTP requests.**
## Demo
[You can see this running on my website](http://moox.io), with sexy CSS animations when switching pages.
_Pjax does not rely on other libraries, like jQuery or similar. It is written entirely in vanilla JS._
## Installation
- You can install Pjax from **npm**:
```shell
npm install pjax
```
- You can also link directly to the [bundle](https://cdn.jsdelivr.net/npm/pjax/pjax.js):
- You can link directly to the [bundle](https://cdn.jsdelivr.net/npm/pjax/pjax.js):
```html
<script src="https://cdn.jsdelivr.net/npm/pjax@VERSION/pjax.js"></script>
```
Or the [minified bundle](https://cdn.jsdelivr.net/npm/pjax/pjax.min.js):
- Or the [minified bundle](https://cdn.jsdelivr.net/npm/pjax/pjax.min.js):
```html
<script src="https://cdn.jsdelivr.net/npm/pjax@VERSION/pjax.min.js"></script>
```
## No dependencies
- You can also install Pjax from **npm**:
```shell
npm install pjax
```
**Note**: If you use this option, you will need to do one of the following:
- Link a script tag to either `pjax.js` or `pjax.min.js`. E.g.:
```html
<script src="./node_modules/pjax/pjax.js"></script>
```
- Use a bundler like Webpack. (`index.js` cannot be used in the browser without a bundler).
_Pjax does not rely on other libraries, like jQuery or similar. It is written entirely in vanilla JS._
- Or you can clone the repo and build the bundle from the source using npm:
```shell
git clone https://github.com/MoOx/pjax.git
cd pjax
npm install
npm run build
```
and then link a script tag to either `pjax.js` or `pjax.min.js`. E.g.:
```html
<script src="./pjax.min.js"></script>
```
## How Pjax works
---
Pjax loads pages using AJAX and updates the browser's current URL using `pushState()` without reloading your page's layout or any resources (JS, CSS), giving a fast page load.
## What Pjax Does
_But under the hood, it's just ONE HTTP request with a `pushState()` call._
_Under the hood, it's just ONE HTTP request with a `pushState()` call._
Obviously, for [browsers that don't support `history.pushState()`](http://caniuse.com/#search=pushstate) Pjax gracefully degrades and does not do anything at all.
Pjax loads pages using AJAX and updates the browser's current URL using `pushState()` _without_ reloading your page's layout or any resources (JS, CSS), giving a fast page load.
It simply works with all permalinks and can update all parts of the page you
want (including HTML metas, title, and navigation state).
It works with all permalinks and can update all the parts of the page you want (including HTML metas, title, and navigation state).
- It's not limited to one container, like jQuery-Pjax is.
- It fully supports browser history (back and forward buttons).
- It supports keyboard browsing.
In the case of [browsers that don't support `history.pushState()`](http://caniuse.com/#search=pushstate), Pjax gracefully degrades and does not do anything at all.
Additionally, Pjax:
- Is not limited to one container, like jQuery-Pjax is.
- Fully supports browser history (back and forward buttons).
- Supports keyboard browsing.
- Automatically falls back to standard navigation for external pages (thanks to Captain Obvious's help).
- Automatically falls back to standard navigation for internal pages that do not have an appropriate DOM tree.
- You can add pretty cool CSS transitions (animations) very easily.
- It's around 4kb (minified and gzipped).
- Allows for CSS transitions (animations) very easily.
- Is only around 6kb (minified and gzipped).
### Under the hood
## How Pjax Works
- It listens to every click on links _you want_ (by default all of them).
- When an internal link is clicked, Pjax grabs HTML from your server via AJAX.
- Pjax renders the page's DOM tree (without loading any resources - images, CSS, JS...).
- When an internal link is clicked, Pjax fetches the page's HTML via AJAX.
- Pjax renders the page's DOM tree (without loading any resources - images, CSS, JS, etc).
- It checks that all defined parts can be replaced:
- If the page doesn't meet the requirements, standard navigation is used.
- If the page meets the requirements, Pjax does all defined DOM replacements.
@@ -70,9 +81,8 @@ want (including HTML metas, title, and navigation state).
## Overview
Pjax is fully automatic. You don't need to setup anything in the existing HTML.
You just need to designate some elements on your page that will be replaced when
you navigate your site.
_Pjax is fully automatic_. You don't need to change anything about your existing HTML,
you just need to designate which elements on your page that you want to be replaced when your site is navigated.
Consider the following page.
@@ -80,49 +90,83 @@ Consider the following page.
<!DOCTYPE html>
<html>
<head>
<!-- metas, title, styles, ... -->
<!-- metas, title, styles, etc -->
<title>My Cool Blog</title>
<meta name="description" content="Welcome to My Cool Blog">
<link href="/styles.css" rel="stylesheet">
</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>.
<header class="the-header">
<nav>
<a href="/" class="is-active">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
<section class="the-content">
<h1>My Cool Blog</h1>
<p>
Thanks for stopping by!
<a href="/about">Click Here to find out more about me.</a>
</p>
</section>
<aside class="my-Sidebar">Sidebar stuff</aside>
<footer class="my-Footer"></footer>
<aside class="the-sidebar">
<h3>Recent Posts</h3>
<!-- sidebar content -->
</aside>
<footer class="the-footer">
&copy; My Cool Blog
</footer>
<script src="onDomReadystuff.js"></script>
<script><!-- analytics --></script>
<script>
// analytics
</script>
</body>
</html>
```
We want Pjax to intercept the URL `/blah`, and replace `.my-Content` with the results of the request.
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 want Pjax to intercept the URL `/about`, and replace `.the-content` with the resulting content of the request.
We do this by telling Pjax to listen on `a` tags and use CSS selectors defined above (without forgetting minimal meta):
It would also be nice if we could replace the `<nav>` to show that the `/about` link is active, as well as update our page meta and the `<aside>` sidebar.
So all in all we want to update the page title and meta, header, content area, and sidebar, **without reloading styles or scripts**.
We can easily do this by telling Pjax to listen on all `a` tags (which is the default) and use CSS selectors defined above (without forgetting minimal meta):
``` javascript
new Pjax({ selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"] })
var pjax = new Pjax({
selectors: [
"title",
"meta[name=description]",
".the-header",
".the-content",
".the-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.
Now, when someone in a Pjax-compatible browser clicks an internal link on the page, the content of each of the selectors will be replaced with the specific content pieces found in the next page's content.
_Magic! For real!_ **There is no need to do anything 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 (Webpack/Browserify), AMD (RequireJS) or even globally
- Allow page transition with CSS animations
- Can be easily tweaked, since every method is public (and as a result, overridable)
- No jQuery dependency.
- Not limited to a container.
- No server-side requirements.
- Works for CommonJS environment (Webpack/Browserify), AMD (RequireJS) or even globally.
- Allows page transitions with CSS animations.
- Can be easily tweaked, since every method is public (and as a result, overridable).
## 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 (and it just does nothing).
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 (and it just does nothing).
To see if Pjax is actually supported by your browser, use `Pjax.isSupported()`.
@@ -130,124 +174,216 @@ To see if Pjax is actually supported by your browser, use `Pjax.isSupported()`.
### `new Pjax()`
Let's talk more about the most basic way to get started:
Let's talk more about the most basic way to get started.
When instantiating `Pjax`, you can pass options into the constructor as an object:
```js
new Pjax({
var pjax = new Pjax({
elements: "a", // default is "a[href], form[action]"
selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"]
selectors: ["title", ".the-header", ".the-content", ".the-sidebar"]
})
```
This will enable Pjax on all links, and designate the part to replace using CSS 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", ".the-header", ".the-content", ".the-sidebar"`.
For some reason, you might want to just target some elements to apply Pjax behavior.
In that case, you can do two different things:
In some cases, you might want to only target some specific elements to apply Pjax behavior. In that case, you can do two different 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 `querySelectorAll` the `elements` option. In this function you just need to return a `NodeList`.
1. Use a custom CSS selector( such as `"a.js-Pjax"` or `".js-Pjax a"`, etc).
2. Override `Pjax.prototype.getElements`.
- **Note**: If doing this, make sure to return a `NodeList`.
```js
// use case 1
new Pjax({ elements: "a.js-Pjax" })
var pjax = new Pjax({ elements: "a.js-Pjax" })
```
```js
// use case 2
Pjax.prototype.getElements = function() {
return document.getElementsByClassName(".js-Pjax")
}
new Pjax({})
var pjax = new Pjax()
```
When instantiating a `Pjax` object, you need to pass all options as an object:
### `loadUrl(href, [options])`
#### Options
With this method, you can manually trigger the loading of a URL:
##### `elements` (String, default: `"a[href], form[action]"`)
```js
var pjax = new Pjax()
CSS selector to use to retrieve links to apply Pjax to.
// use case 1
pjax.loadUrl("/your-url")
##### `selectors` (Array, default: `["title", ".js-Pjax"]`)
// use case 2 (with options override)
pjax.loadUrl("/your-other-url", { timeout: 10 })
```
CSS selectors to replace. If a query returns multiples items, it will just keep the index.
### `handleResponse(responseText, request, href, options)`
This method takes the raw response, processes the URL, then calls `pjax.loadContent()` to actually load it into the DOM.
It is passed the following arguments:
* **responseText** (string): This is the raw response text. This is equivalent to `request.responseText`.
* **request** (XMLHttpRequest): This is the XHR object.
* **href** (string): This is the URL that was passed to `loadUrl()`.
* **options** (object): This is an object with the options for this request. The structure basically matches the regular options object, with a few extra internal properties.
You can override this if you want to process the data before, or instead of, it being loaded into the DOM.
For example, if you want to check for a non-HTML response, you could do the following:
```js
var pjax = new Pjax();
pjax._handleResponse = pjax.handleResponse;
pjax.handleResponse = function(responseText, request, href, options) {
if (request.responseText.match("<html")) {
pjax._handleResponse(responseText, request, href, options);
} else {
// handle non-HTML response here
}
}
```
### `refresh([el])`
Use this method to bind Pjax to children of a DOM element that didn't exist when Pjax was initialised e.g. content inserted dynamically by another library or script. If called with no arguments, Pjax will parse the entire document again to look for newly-inserted elements.
```js
// Inside a callback or Promise that runs after new DOM content has been inserted
var newContent = document.querySelector(".new-content");
pjax.refresh(newContent);
```
### `reload()`
A helper shortcut for `window.location.reload()`. Used to force a page reload.
```js
pjax.reload()
```
## Options
### `elements` (String, default: `"a[href], form[action]"`)
CSS selector(s) used to find links to apply Pjax to. If needing multiple specific selectors, separate them by a comma.
```js
// Single element
var pjax = new Pjax({
elements: ".ajax"
})
```
```js
// Multiple elements
var pjax = new Pjax({
elements: ".pjax, .ajax",
})
```
### `selectors` (Array, default: `["title", ".js-Pjax"]`)
CSS selectors used to find which content to replace.
```js
var pjax = new Pjax({
selectors: [
"title",
"the-content",
]
})
```
If a query returns multiples items, it will just keep the index.
Example of what you can do:
```html
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<header class="js-Pjax"></header>
<header class="js-Pjax">...</header>
<section class="js-Pjax">...</section>
<footer class="my-Footer"></footer>
<footer class="the-footer">...</footer>
<script>...</script>
</body>
</html>
```
This example is correct and should work "as expected".
_If the current page and new page do not have the same amount of DOM elements,
Pjax will fall back to normal page load._
##### `switches` (Object, default: `{}`)
**NOTE:** _If the current page and new page do not have the same amount of DOM elements, Pjax will fall back to normal page load._
Objects containing callbacks that can be used to switch old elements with new elements.
Keys should be one of the defined selectors.
### `switches` (Object, default: `{}`)
This is an object containing callbacks that can be used to switch old elements with new elements.
The object keys should be one of the defined selectors (from the `selectors` option).
Examples:
```js
new Pjax({
var pjax = 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
"title": Pjax.switches.outerHTML, // default behavior
".the-content": function(oldEl, newEl, options) {
// this is identical to the default behavior
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
},
".js-Pjax": Pjax.switches.sideBySide
}
})
```
Callbacks are bound to Pjax instance itself to allow you to reuse it (ex: `this.onSwitch()`)
Callbacks are bound to the Pjax instance itself to allow you to reuse it (ex: `this.onSwitch()`)
###### Existing switches callback
### Existing Switch Callbacks
- `Pjax.switches.outerHTML`: default behavior, replace elements using outerHTML
- `Pjax.switches.innerHTML`: replace elements using innerHTML and copy className too
- `Pjax.switches.sideBySide`: smart replacement that allows you to have both elements in the same parent when you want to use CSS animations. Old elements are removed when all children have been fully animated ([animationEnd](http://www.w3.org/TR/css3-animations/#animationend) event triggered)
- `Pjax.switches.outerHTML`:
The default behavior, replaces elements using `outerHTML`.
- `Pjax.switches.innerHTML`:
Replaces elements using `innerHTML` and copies `className`.
- `Pjax.switches.replaceNode`:
Replaces elements using `replaceChild`
- `Pjax.switches.sideBySide`:
Smart replacing that allows you to have both elements in the same parent when you want to use CSS animations. Old elements are removed when all children have been fully animated (an [animationEnd](http://www.w3.org/TR/css3-animations/#animationend) event is triggered).
###### Create a switch callback
### Creating a Switch Callback
Your function can do whatever you want, but you need to:
Your callback function can do whatever you want, but you need to:
- replace `oldEl`'s content with `newEl`'s content in some fashion
- call `this.onSwitch()` to trigger the attached callback.
1. Replace the `oldEl`'s content with the `newEl`'s content in some fashion.
2. Call `this.onSwitch()` to trigger the attached callback.
Here is the default behavior as an example:
```js
function(oldEl, newEl, pjaxRequestOptions, switchesClasses) {
function(oldEl, newEl, pjaxOptions) {
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
}
```
##### `switchesOptions` (Object, default: `{}`)
### `switchesOptions` (Object, default: `{}`)
These are options that can be used during switch by switchers (for now, only `Pjax.switches.sideBySide` uses it).
This is 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).
These are options that can be used during content replacement by switches. For now, only `Pjax.switches.sideBySide` uses it. This is 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({
var pjax = new Pjax({
selectors: ["title", ".js-Pjax"],
switches: {
".js-Pjax": Pjax.switches.sideBySide
@@ -255,18 +391,18 @@ new Pjax({
switchesOptions: {
".js-Pjax": {
classNames: {
// class added on the element that will be removed
// class added to the old element being replaced, e.g. a fade out
remove: "Animated Animated--reverse Animate--fast Animate--noDelay",
// class added on the element that will be added
// class added to the new element that is replacing the old one, e.g. a fade in
add: "Animated",
// class added on the element when it go backward
// class added on the element when navigating back
backward: "Animate--slideInRight",
// class added on the element when it go forward (used for new page too)
// class added on the element when navigating 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
// to make a nice transition with 2 pages at the same time
// we are playing with absolute positioning for the element we are removing
// & we need live metrics to have something great
// see associated CSS below
removeElement: function(el) {
@@ -277,30 +413,29 @@ new Pjax({
}
})
```
_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:
_Note that `remove` includes `Animated--reverse` which is a simple way to not have to have a duplicate transition (slideIn + reverse => slideOut)._
Here is some css that works well with the above configuration:
```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
Note: If your content elements don't have a fixed width it can cause
an issue when positioning absolutely
*/
.js-Pjax { position: relative } /* parent element where switch will be made */
.js-Pjax-child { width: 100% }
.js-Pjax-child { width: 100% }
/* position for the elements that will be removed */
.js-Pjax-remove {
/* 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 {
@@ -308,13 +443,14 @@ The following CSS will be required to make something nice:
animation-duration: 1s;
}
.Animated--reverse { animation-direction: reverse }
.Animated--reverse { animation-direction: reverse }
.Animate--fast { animation-duration: .5s }
.Animate--noDelay { animation-delay: 0s !important; }
.Animate--fast { animation-duration: .5s }
.Animate--noDelay { animation-delay: 0s !important; }
.Animate--slideInRight { animation-name: Animation-slideInRight }
@keyframes Animation-slideInRight {
.Animate--slideInRight { animation-name: Animation-slideInRight }
@keyframes Animation-slideInRight {
0% {
opacity: 0;
transform: translateX(100rem);
@@ -323,10 +459,11 @@ The following CSS will be required to make something nice:
100% {
transform: translateX(0);
}
}
}
.Animate--slideInLeft { animation-name: Animation-slideInLeft }
@keyframes Animation-slideInLeft {
.Animate--slideInLeft { animation-name: Animation-slideInLeft }
@keyframes Animation-slideInLeft {
0% {
opacity: 0;
transform: translateX(-100rem);
@@ -335,7 +472,7 @@ The following CSS will be required to make something nice:
100% {
transform: translateX(0);
}
}
}
```
To give context to this CSS, here is an HTML snippet:
@@ -344,7 +481,7 @@ To give context to this CSS, here is an HTML snippet:
<!doctype html>
<html>
<head>
<title>Page title</title>
<title>Page Title</title>
</head>
<body>
<section class="js-Pjax">
@@ -352,13 +489,15 @@ To give context to this CSS, here is an HTML snippet:
Your content here
</div>
<!--
when switching will be made you will have the following tree
During the replacement process, you'll 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>
@@ -366,45 +505,46 @@ To give context to this CSS, here is an HTML snippet:
</html>
```
##### `history` (Boolean, default: `true`)
### `history` (Boolean, default: `true`)
Enable the use of `pushState()`. Disabling this will prevent Pjax from updating browser history.
However, there is almost no use case where you would want to do that.
Enable the use of `pushState()`. Disabling this will prevent Pjax from updating browser history. While possible, there is almost no use case where you would want to do this.
Internally, this option is used when a `popstate` event triggers Pjax (to not `pushState()` again).
##### `analytics` (Function|Boolean, default: a function that pushes `_gaq` `_trackPageview` or sends `ga` `pageview`
### `analytics` (Function | Boolean, default: a function that pushes `_gaq` `_trackPageview` or sends `ga` `pageview`
Function that allows you to add behavior for analytics. By default it tries to track
a pageview with Google Analytics (if it exists on the page).
It's called every time a page is switched, even for history navigation.
a pageview with Google Analytics (if it exists on the page). It's called every time a page is switched, even for history navigation.
Set to `false` to disable this behavior.
##### `scrollTo` (Integer, default: `0`)
### `scrollTo` (Integer | \[Integer, Integer\] | False, default: `0`)
Value (in px from the top of the page) to scroll to when a page is switched.
When set to an integer, this is the value (in px from the top of the page) to scroll to when a page is switched.
##### `scrollRestoration` (Boolean, default: `true`)
When set to an array of 2 integers (\[x, y\]), this is the value to scroll both horizontally and vertically.
When set to true, attempt to restore the scroll position when navigating backwards or forwards.
Set this to `false` to disable scrolling, which will mean the page will stay in that same position it was before loading the new elements.
##### `cacheBust` (Boolean, default: `true`)
### `scrollRestoration` (Boolean, default: `true`)
When set to true, append a timestamp query string segment to the requested URLs
in order to skip browser cache.
When set to `true`, Pjax will attempt to restore the scroll position when navigating backwards or forwards.
##### `debug` (Boolean, default: `false`)
### `cacheBust` (Boolean, default: `true`)
When set to `true`, Pjax appends a timestamp query string segment to the requested URL in order to skip the browser cache.
### `debug` (Boolean, default: `false`)
Enables verbose mode. Useful to debug page layout differences.
##### `currentUrlFullReload` (Boolean, default: `false`)
### `currentUrlFullReload` (Boolean, default: `false`)
When set to true, clicking on a link that points to the current URL will trigger a full page reload.
When set to `true`, clicking on a link that points to the current URL will trigger a full page reload.
The default is `false`, so clicking on such a link will do nothing.
If you want to add some custom behavior, add a click listener to the link,
and set `preventDefault` to true, to prevent Pjax from receiving the event.
When set to `false`, clicking on such a link will cause Pjax to load the current page without a full page reload. If you want to add some custom behavior, add a click listener to the link and call `preventDefault()`. This will prevent Pjax from receiving the event.
**Note**: This must be done before Pjax is instantiated, otherwise Pjax's event handler will be called first, and `preventDefault()` won't have been called yet.
Here is some sample code:
@@ -417,22 +557,23 @@ Here is some sample code:
el.addEventListener("click", function(e) {
if (el.href === window.location.href.split("#")[0]) {
e.preventDefault();
console.log("Link to current page clicked");
// Custom code goes here.
}
})
}
var pjax = new Pjax()
```
(Note that if `cacheBust` is set to true, the code that checks if the href
is the same as the current page's URL will not work, due to the query string
appended to force a cache bust).
(Note that if `cacheBust` is set to `true`, the code that checks if the href is the same as the current page's URL will not work, due to the query string appended to force a cache bust).
##### `timeout` (Integer, default: `0`)
### `timeout` (Integer, default: `0`)
The timeout in milliseconds for the XHR requests. Set to 0 to disable the timeout.
The timeout in _milliseconds_ for the XHR requests. Set to `0` to disable the timeout.
### Events
## Events
Pjax fires a number of events regardless of how it's invoked.
@@ -441,7 +582,7 @@ All events are fired from the _document_, not the link that was clicked.
* `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.
* `pjax:error` - Fired after the Pjax request fails. The request object will be passed along as `event.options.request`.
`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/))
@@ -450,11 +591,32 @@ document.addEventListener('pjax:send', topbar.show)
document.addEventListener('pjax:complete', topbar.hide)
```
#### Note about DOM ready state
## HTTP Headers
Pjax uses several custom headers when it makes and receives HTTP requests. If the requests are going to your server, you can use those headers for some meta information about the response.
### Request Headers
Pjax sends the following headers with every request:
- `X-Requested-With: "XMLHttpRequest"`
- `X-PJAX: "true"`
- `X-PJAX-Selectors`:
A serialized JSON array of selectors, taken from `options.selectors`. You can use this to send back only the elements that Pjax will use to switch, instead of sending the whole page. Note that you'll need to deserialize this on the server (Such as by using `JSON.parse()`)
### Response Headers
Pjax looks for the following headers on the response:
- `X-PJAX-URL` or `X-XHR-Redirected-To`
Pjax first checks the `responseURL` property on the XHR object to see if the request was redirected by the server. Most browsers support this, but not all. To ensure Pjax will be able to tell when the request is redirected, you can include one of these headers with the response, set to the final URL.
## DOM Ready State
Most of the time, you will have code related to the current DOM that you only execute when the DOM is ready.
Since Pjax doesn't magically re-execute your previous code each time you load a page, you need to add some simple code to achieve this:
Since Pjax doesn't automatically re-execute your previous code each time you load a page, you'll need to add code to re-trigger the DOM ready code. Here's a simple example:
```js
function whenDOMReady() {
@@ -463,47 +625,41 @@ function whenDOMReady() {
whenDOMReady()
new Pjax()
var pjax = new Pjax()
document.addEventListener("pjax:success", whenDOMReady)
```
_Note: Don't create the Pjax instance in the `whenDOMReady` function._
For my concern and usage, I `js-Pjax`-ify all body children, including stuff like navigation and footer (to get navigation state easily updated).
The attached behavior is re-executed 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 and re-execute this function when "pjax:success" event is fired.
If you want to just update a specific part (which is a good idea), you can add the DOM-related code in a function and re-execute this function when the `pjax:success` event is fired.
```js
// do your global stuff
//... DOM ready blah blah
//... DOM ready code
function whenContainerReady() {
// do your container related stuff
}
whenContainerReady()
new Pjax()
var pjax = new Pjax()
document.addEventListener("pjax:success", whenContainerReady)
```
---
## FAQ
### Q: Disqus doesn't work anymore, how can I fix that ?
A: There is a few things you need to do:
- wrap your Disqus snippet into a DOM element that you will add to the `selector`
property (or just wrap it with `class="js-Pjax"`) and be sure to have at least the empty
wrapper on each page (to avoid differences of DOM between pages)
- edit your Disqus snippet like the one below
#### A: There are a few things you need to do:
#### Disqus snippet before Pjax integration
- Wrap your Disqus snippet into a DOM element that you will add to the `selector` property (or just wrap it with `class="js-Pjax"`) and be sure to have at least an empty wrapper on each page (to avoid differences of DOM between pages).
- Edit your Disqus snippet like the one below.
#### Disqus snippet _before_ Pjax integration
```html
<script>
@@ -511,6 +667,7 @@ wrapper on each page (to avoid differences of DOM between pages)
var disqus_identifier = 'PAGEID'
var disqus_url = 'PAGEURL'
var disqus_script = 'embed.js'
(function(d,s) {
s = d.createElement('script');s.async=1;s.src = '//' + disqus_shortname + '.disqus.com/'+disqus_script;
(d.getElementsByTagName('head')[0]).appendChild(s);
@@ -518,11 +675,11 @@ wrapper on each page (to avoid differences of DOM between pages)
</script>
```
#### Disqus snippet after Pjax integration
#### Disqus snippet _after_ Pjax integration
```html
<div class="js-Pjax"><!-- needs to be here on every Pjax-ified page, even if empty -->
<!-- if (blah blah) { // eventual server-side test to know whether or not you include this script -->
<!-- if (some condition) { // eventual server-side test to know whether or not you include this script -->
<script>
var disqus_shortname = 'YOURSHORTNAME'
var disqus_identifier = 'PAGEID'
@@ -536,8 +693,8 @@ wrapper on each page (to avoid differences of DOM between pages)
(d.getElementsByTagName('head')[0]).appendChild(s);
})(document)
}
// if disqus <script> already loaded, we just reset disqus the right way
// see http://help.disqus.com/customer/portal/articles/472107-using-disqus-on-ajax-sites
// if disqus <script> is already loaded, we just reset disqus the right way
// see https://help.disqus.com/developer/using-disqus-on-ajax-sites
else {
DISQUS.reset({
reload: true,
@@ -554,19 +711,15 @@ wrapper on each page (to avoid differences of DOM between pages)
**Note: Pjax only handles inline `<script>` blocks for the container you are switching.**
---
## Examples
Clone this repository and run `npm run example`, which will open the example app in your browser.
---
## CONTRIBUTING
* ⇄ Pull requests and ★ Stars are always welcome.
* For bugs and feature requests, please create an issue.
* Pull requests must be accompanied by passing automated tests (`npm test`).
- ⇄ Pull requests and ★ Stars are always welcome.
- For bugs and feature requests, please create an issue.
- Pull requests must be accompanied by passing automated tests (`npm test`). If the API is changed, please update the Typescript definitions as well (`pjax.d.ts`).
## [CHANGELOG](CHANGELOG.md)

View File

@@ -1,27 +1,59 @@
/* global Pjax */
console.log("Document initialized:", window.location.href)
var pjax;
var initButtons = function() {
var buttons = document.querySelectorAll("button[data-manual-trigger]");
if (!buttons) {
return;
}
// jshint -W083
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function(e) {
var el = e.currentTarget;
if (el.getAttribute("data-manual-trigger-override") === "true") {
// Manually load URL with overridden Pjax instance options
pjax.loadUrl("/example/page2.html", { cacheBust: false });
} else {
// Manually load URL with current Pjax instance options
pjax.loadUrl("/example/page2.html");
}
});
}
// jshint +W083
};
console.log("Document initialized:", window.location.href);
document.addEventListener("pjax:send", function() {
console.log("Event: pjax:send", arguments)
})
console.log("Event: pjax:send", arguments);
});
document.addEventListener("pjax:complete", function() {
console.log("Event: pjax:complete", arguments)
})
console.log("Event: pjax:complete", arguments);
});
document.addEventListener("pjax:error", function() {
console.log("Event: pjax:error", arguments)
})
console.log("Event: pjax:error", arguments);
});
document.addEventListener("pjax:success", function() {
console.log("Event: pjax:success", arguments)
})
console.log("Event: pjax:success", arguments);
// Init page content
initButtons();
});
document.addEventListener("DOMContentLoaded", function() {
var pjax = new Pjax({
// Init Pjax instance
pjax = new Pjax({
elements: [".js-Pjax"],
selectors: [".body"]
// currentUrlFullReload: true,
})
console.log("Pjax initialized.", pjax)
})
selectors: [".body", "title"],
cacheBust: true
});
console.log("Pjax initialized.", pjax);
// Init page content
initButtons();
});

138
example/forms.html Normal file
View File

@@ -0,0 +1,138 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Forms</title>
<script src="../pjax.js"></script>
<script src="example.js"></script>
</head>
<body>
<div class="body">
<h1>Forms</h1>
Hello. Try out the examples below and inspect the results in your browser's developer tools, or go to the <a href="index.html" class="js-Pjax">Index</a>.
<h3>GET form</h3>
<form action="" method="get" class="js-Pjax" id="get-form">
<label for="get-form-text">Text input:</label>
<input type="text" name="textInput" id="get-form-text" value="Foobar" />
<br />
<br />
<label for="get-form-number">Number input:</label>
<input type="number" name="numberInput" id="get-form-number" value="1" />
<br />
<br />
<label for="get-form-email">Email input:</label>
<input type="email" name="emailInput" id="get-form-email" value="example@example.com" />
<br />
<br />
<label for="get-form-textarea">Textarea:</label>
<textarea name="textarea" id="get-form-textarea">This is some text</textarea>
<br />
<br />
<fieldset>
<label for="get-form-radio-1">Radio input:</label>
<input type="radio" name="radioInput" value="radio-1" checked id="get-form-radio-1" />
<label for="get-form-radio-2">Radio input alt:</label>
<input type="radio" name="radioInput" value="radio-2" id="get-form-radio-2" />
</fieldset>
<br />
<br />
<label for="get-form-checkbox">Checkbox input:</label>
<input type="checkbox" name="checkboxInput" checked id="get-form-checkbox" />
<br />
<br />
<label for="get-form-select">Select list:</label>
<select multiple name="select" id="get-form-select">
<option>
Option 1
</option>
<option>
Option 2
</option>
</select>
<br />
<br />
<input type="submit" value="Submit" />
</form>
<br />
<br />
<h3>POST form</h3>
<form action="" method="post" class="js-Pjax" id="post-form">
<label for="post-form-text">Text input:</label>
<input type="text" name="textInput" id="post-form-text" value="Foobar" />
<br />
<br />
<label for="post-form-number">Number input:</label>
<input type="number" name="numberInput" id="post-form-number" value="1" />
<br />
<br />
<label for="post-form-email">Email input:</label>
<input type="email" name="emailInput" id="post-form-email" value="example@example.com" />
<br />
<br />
<label for="post-form-textarea">Textarea:</label>
<textarea name="textarea" id="post-form-textarea">This is some text</textarea>
<br />
<br />
<fieldset>
<label for="post-form-radio-1">Radio input:</label>
<input type="radio" name="radioInput" value="radio-1" checked id="post-form-radio-1" />
<label for="post-form-radio-2">Radio input alt:</label>
<input type="radio" name="radioInput" value="radio-2" id="post-form-radio-2" />
</fieldset>
<br />
<br />
<label for="post-form-checkbox">Checkbox input:</label>
<input type="checkbox" name="checkboxInput" checked id="post-form-checkbox" />
<br />
<br />
<label for="post-form-select">Select list:</label>
<select multiple name="select" id="post-form-select">
<option>
Option 1
</option>
<option>
Option 2
</option>
</select>
<br />
<br />
<input type="submit" value="Submit" />
</form>
</div>
</body>
</html>

View File

@@ -1,17 +1,29 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta charset="utf-8">
<title>Hello</title>
<script src='../pjax.js'></script>
<script src='example.js'></script>
<script src="../pjax.js"></script>
<script src="example.js"></script>
</head>
<body>
<div class='body'>
<div class="body">
<h1>Index</h1>
Hello.
Go to <a href='page2.html' class="js-Pjax">Page 2</a> or <a href='page3.html' class="js-Pjax">Page 3</a> and view your console to see Pjax events.
Clicking on <a href='index.html'>this page</a> will just reload the page entirely.
Go to <a href="page2.html" class="js-Pjax">Page 2</a> or <a href="page3.html" class="js-Pjax">Page 3</a> and view your console to see Pjax events.
Clicking on <a href="index.html">this page</a> will do nothing.
<h2>Manual URL loading</h2>
You can use Pjax's <i>loadUrl</i> method to manually load a URL. Click the buttons below to try it out!<br /><br />
<button data-manual-trigger>loadUrl with current options</button><br /><br />
<button data-manual-trigger data-manual-trigger-override="true">loadUrl with overridden options (no cache busting)</button>
<h2>Forms</h2>
You can submit GET or POST forms with Pjax! Go to the <a href="forms.html">form examples</a> to try it out.
</div>
</body>
</html>

View File

@@ -1,15 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>Hello</title>
<script src='../pjax.js'></script>
<script src='example.js'></script>
<meta charset="utf-8">
<title>Page 2</title>
<script src="../pjax.js"></script>
<script src="example.js"></script>
</head>
<body>
<div class='body'>
<div class="body">
<h1>Page 2</h1>
Hello. Go to <a href='index.html' class="js-Pjax">Index</a>.
Hello. Go to <a href="index.html" class="js-Pjax">Index</a>.
</div>
</body>
</html>

View File

@@ -1,15 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>Hello</title>
<script src='../pjax.js'></script>
<script src='example.js'></script>
<meta charset="utf-8">
<title>Page 3</title>
<script src="../pjax.js"></script>
<script src="example.js"></script>
</head>
<body>
<div class='body'>
<div class="body">
<h1>Page 3</h1>
Hello. Go to <a href='index.html' class="js-Pjax">Index</a>.
Hello. Go to <a href="index.html" class="js-Pjax">Index</a>.
</div>
</body>
</html>

205
index.d.ts vendored Normal file
View File

@@ -0,0 +1,205 @@
declare class Pjax {
options: Pjax.IOptions;
constructor(options?: Partial<Pjax.IOptions>);
static switches: {
[key in DefaultSwitches]: Pjax.Switch
};
/**
* Checks if Pjax is supported by the environment.
*/
static isSupported: () => boolean;
log: VoidFunction;
getElements(el: Element | Document): NodeListOf<Element>;
parseDOM(el: Element | Document): void;
refresh: ElementFunction;
reload: VoidFunction;
attachLink(el: HTMLAnchorElement): void;
attachForm(el: HTMLFormElement): void;
forEachSelectors(cb: ElementFunction, context: Pjax, DOMcontext?: Element | Document): void;
switchesSelectors(selectors: string[], fromEl: Element | Document, toEl: Element | Document, options: Pjax.IOptions): void;
latestChance(href: string): void;
onSwitch: VoidFunction;
/**
* Loads the HTML from the response into the DOM.
*
* @param {string} html
* @param {Pjax.IOptions} options
*/
loadContent(html: string, options: Pjax.IOptions): void;
/**
* Aborts an ongoing XHR request.
*
* @param {XMLHttpRequest} request
*/
abortRequest(request: XMLHttpRequest): void;
/**
* Makes the XHR request.
*
* @param {string} location The URI for the request.
* @param {Pjax.IOptions | null} options
* @param {(requestText: string, request: XMLHttpRequest, href: string) => void} callback The callback to call when
* the response is received. The signature should match that of <code>handleResponse</code>.
* @returns {XMLHttpRequest}
*/
doRequest(location: string, options: Pjax.IOptions | null,
callback: (requestText: string, request: XMLHttpRequest, href: string) => void): XMLHttpRequest;
/**
* Saves the state, updates the URL if there were any redirects, then calls loadContent().
*
* @param {string} requestText The raw text of the response. Same as <code>request.responseText</code>.
* @param {XMLHttpRequest} request The XHR object.
* @param {string} href The original URI used to initiate the request.
* @param options The Pjax options object used for the request
*/
handleResponse(requestText: string, request: XMLHttpRequest, href: string, options?: Pjax.IOptions): void;
/**
* Initiates the request by calling <code>doRequest()</code>.
* @param {string} href
* @param {Pjax.IOptions} options
*/
loadUrl(href: string, options?: Pjax.IOptions): void;
/**
* Called after all switches complete (even async).
*/
afterAllSwitches: VoidFunction;
/**
* Allows reassignment of existing prototype functions to be able to do something before calling the original function.
* For example:
*
* <pre>
* pjax._handleResponse = pjax.handleResponse;
* pjax.handleResponse = (requestText: string, request: XMLHttpRequest, href: string) => {
* return pjax._handleResponse(requestText, request, href);
* }
* </pre>
*/
[key: string]: Function | Pjax.IOptions;
}
declare namespace Pjax {
export interface IOptions {
/**
* CSS selector to use to retrieve links to apply Pjax to.
*/
elements: string;
/**
* CSS selectors for the elements to replace.
*/
selectors: string[];
/**
* Objects containing callbacks that can be used to switch old elements with new elements.
* Keys should be one of the defined selectors.
*/
switches: StringKeyedObject<Switch>;
/**
* These are options that can be used during switches.
* Keys should be one of the defined selectors.
*/
switchesOptions: StringKeyedObject;
/**
* Enable the use of pushState(). Disabling this will prevent Pjax from updating browser history.
* Internally, this option is used when a popstate event triggers Pjax (to not pushState() again).
*/
history: boolean;
/**
* Function that allows you to add behavior for analytics.
* By default it tries to track a pageview with Google Analytics (if it exists on the page).
* It's called every time a page is switched, even for history navigation.
* Set to false to disable this behavior.
*/
analytics: Function | false;
/**
* When set to an integer, this is the value (in px from the top of the page) to scroll to when a page is switched.
* When set to an array of 2 integers ([x, y]), this is the value to scroll both horizontally and vertically.
* Set this to false to disable scrolling, which will mean the page will stay in that same position it was before
* loading the new elements.
*/
scrollTo: number | [number, number] | false;
/**
* When set to true, attempt to restore the scroll position when navigating backwards or forwards.
*/
scrollRestoration: boolean;
/**
* When set to true, append a timestamp query string segment to the requested URLs in order to skip browser cache.
*/
cacheBust: boolean;
/**
* Enables verbose mode.
*/
debug: boolean;
/**
* The timeout in milliseconds for the XHR requests. Set to 0 to disable the timeout.
*/
timeout: number;
/**
* When set to true, clicking on a link that points to the current URL will trigger a full page reload.
* (Note that if cacheBust is set to true, the code that checks if the href is the same as the current page's URL
* will not work, due to the query string appended to force a cache bust).
*/
currentUrlFullReload: boolean;
/**
* Hold the information to make an XHR request.
*/
requestOptions?: {
requestUrl?: string;
requestMethod?: string;
requestParams?: IRequestParams[];
formData?: FormData;
}
}
export type Switch = (oldEl: Element, newEl: Element, options?: IOptions, switchesOptions?: StringKeyedObject) => void;
export interface IRequestParams {
name: string,
value: string
}
}
interface StringKeyedObject<T = any> {
[key: string]: T
}
type ElementFunction = (el: Element) => void;
declare enum DefaultSwitches {
innerHTML = "innerHTML",
ouetrHTML = "outerHTML",
sideBySide = "sideBySide",
replaceNode = "replaceNode"
}
export = Pjax;

334
index.js
View File

@@ -1,216 +1,195 @@
var clone = require("./lib/clone.js")
var executeScripts = require("./lib/execute-scripts.js")
var forEachEls = require("./lib/foreach-els.js")
var switches = require("./lib/switches")
var newUid = require("./lib/uniqueid.js")
var executeScripts = require("./lib/execute-scripts");
var forEachEls = require("./lib/foreach-els");
var parseOptions = require("./lib/parse-options");
var switches = require("./lib/switches");
var newUid = require("./lib/uniqueid");
var on = require("./lib/events/on.js")
var trigger = require("./lib/events/trigger.js")
var on = require("./lib/events/on");
var trigger = require("./lib/events/trigger");
var contains = require("./lib/util/contains.js")
var noop = require("./lib/util/noop")
var clone = require("./lib/util/clone");
var contains = require("./lib/util/contains");
var extend = require("./lib/util/extend");
var noop = require("./lib/util/noop");
var Pjax = function(options) {
this.state = {
numPendingSwitches: 0,
href: null,
options: null
}
};
var parseOptions = require("./lib/proto/parse-options.js")
parseOptions.call(this,options)
this.log("Pjax options", this.options)
this.options = parseOptions(options);
this.log("Pjax options", this.options);
if (this.options.scrollRestoration && "scrollRestoration" in history) {
history.scrollRestoration = "manual"
history.scrollRestoration = "manual";
}
this.maxUid = this.lastUid = newUid()
this.maxUid = this.lastUid = newUid();
this.parseDOM(document)
this.parseDOM(document);
on(window, "popstate", function(st) {
on(
window,
"popstate",
function(st) {
if (st.state) {
var opt = clone(this.options)
opt.url = st.state.url
opt.title = st.state.title
opt.history = false
opt.requestOptions = {}
opt.scrollPos = st.state.scrollPos
var opt = clone(this.options);
opt.url = st.state.url;
opt.title = st.state.title;
// Since state already exists, prevent it from being pushed again
opt.history = false;
opt.scrollPos = st.state.scrollPos;
if (st.state.uid < this.lastUid) {
opt.backward = true
opt.backward = true;
} else {
opt.forward = true;
}
else {
opt.forward = true
}
this.lastUid = st.state.uid
this.lastUid = st.state.uid;
// @todo implement history cache here, based on uid
this.loadUrl(st.state.url, opt)
}
}.bind(this))
this.loadUrl(st.state.url, opt);
}
}.bind(this)
);
};
Pjax.switches = switches
Pjax.switches = switches;
Pjax.prototype = {
log: require("./lib/proto/log.js"),
log: require("./lib/proto/log"),
getElements: function(el) {
return el.querySelectorAll(this.options.elements)
return el.querySelectorAll(this.options.elements);
},
parseDOM: function(el) {
var parseElement = require("./lib/proto/parse-element")
forEachEls(this.getElements(el), parseElement, this)
var parseElement = require("./lib/proto/parse-element");
forEachEls(this.getElements(el), parseElement, this);
},
refresh: function(el) {
this.parseDOM(el || document)
this.parseDOM(el || document);
},
reload: function() {
window.location.reload()
window.location.reload();
},
attachLink: require("./lib/proto/attach-link.js"),
attachLink: require("./lib/proto/attach-link"),
attachForm: require("./lib/proto/attach-form.js"),
attachForm: require("./lib/proto/attach-form"),
forEachSelectors: function(cb, context, DOMcontext) {
return require("./lib/foreach-selectors.js").bind(this)(this.options.selectors, cb, context, DOMcontext)
return require("./lib/foreach-selectors").bind(this)(
this.options.selectors,
cb,
context,
DOMcontext
);
},
switchSelectors: function(selectors, fromEl, toEl, options) {
return require("./lib/switches-selectors.js").bind(this)(this.options.switches, this.options.switchesOptions, selectors, fromEl, toEl, options)
return require("./lib/switches-selectors").bind(this)(
this.options.switches,
this.options.switchesOptions,
selectors,
fromEl,
toEl,
options
);
},
latestChance: function(href) {
window.location = href
window.location = href;
},
onSwitch: function() {
trigger(window, "resize scroll")
trigger(window, "resize scroll");
this.state.numPendingSwitches--
this.state.numPendingSwitches--;
// debounce calls, so we only run this once after all switches are finished.
if (this.state.numPendingSwitches === 0) {
this.afterAllSwitches()
this.afterAllSwitches();
}
},
loadContent: function(html, options) {
var tmpEl = document.implementation.createHTMLDocument("pjax")
if (typeof html !== "string") {
trigger(document, "pjax:complete pjax:error", options);
return;
}
var tmpEl = document.implementation.createHTMLDocument("pjax");
// parse HTML attributes to copy them
// since we are forced to use documentElement.innerHTML (outerHTML can't be used for <html>)
var htmlRegex = /<html[^>]+>/gi
var htmlAttribsRegex = /\s?[a-z:]+(?:\=(?:\'|\")[^\'\">]+(?:\'|\"))*/gi
var matches = html.match(htmlRegex)
var htmlRegex = /<html[^>]+>/gi;
var htmlAttribsRegex = /\s?[a-z:]+(?:=['"][^'">]+['"])*/gi;
var matches = html.match(htmlRegex);
if (matches && matches.length) {
matches = matches[0].match(htmlAttribsRegex)
matches = matches[0].match(htmlAttribsRegex);
if (matches.length) {
matches.shift()
matches.shift();
matches.forEach(function(htmlAttrib) {
var attr = htmlAttrib.trim().split("=")
var attr = htmlAttrib.trim().split("=");
if (attr.length === 1) {
tmpEl.documentElement.setAttribute(attr[0], true)
tmpEl.documentElement.setAttribute(attr[0], true);
} else {
tmpEl.documentElement.setAttribute(attr[0], attr[1].slice(1, -1));
}
else {
tmpEl.documentElement.setAttribute(attr[0], attr[1].slice(1, -1))
}
})
});
}
}
tmpEl.documentElement.innerHTML = html
this.log("load content", tmpEl.documentElement.attributes, tmpEl.documentElement.innerHTML.length)
tmpEl.documentElement.innerHTML = html;
this.log(
"load content",
tmpEl.documentElement.attributes,
tmpEl.documentElement.innerHTML.length
);
// Clear out any focused controls before inserting new page contents.
if (document.activeElement && contains(this.options.selectors, document.activeElement)) {
if (
document.activeElement &&
contains(document, this.options.selectors, document.activeElement)
) {
try {
document.activeElement.blur()
} catch (e) { }
document.activeElement.blur();
} catch (e) {} // eslint-disable-line no-empty
}
this.switchSelectors(this.options.selectors, tmpEl, document, options)
this.switchSelectors(this.options.selectors, tmpEl, document, options);
},
abortRequest: require("./lib/abort-request.js"),
abortRequest: require("./lib/abort-request"),
doRequest: require("./lib/send-request.js"),
doRequest: require("./lib/send-request"),
handleResponse: require("./lib/proto/handle-response"),
loadUrl: function(href, options) {
this.log("load href", href, options)
options =
typeof options === "object"
? extend({}, this.options, options)
: clone(this.options);
this.log("load href", href, options);
// Abort any previous request
this.abortRequest(this.request)
this.abortRequest(this.request);
trigger(document, "pjax:send", options)
trigger(document, "pjax:send", options);
// Do the request
options.requestOptions.timeout = this.options.timeout
this.request = this.doRequest(href, options.requestOptions, function(html, request) {
// Fail if unable to load HTML via AJAX
if (html === false) {
trigger(document, "pjax:complete pjax:error", options)
return
}
// push scroll position to history
var currentState = window.history.state || {}
window.history.replaceState({
url: currentState.url || window.location.href,
title: currentState.title || document.title,
uid: currentState.uid || newUid(),
scrollPos: [document.documentElement.scrollLeft || document.body.scrollLeft,
document.documentElement.scrollTop || document.body.scrollTop]
},
document.title, window.location)
var oldHref = href
if (request.responseURL) {
if (href !== request.responseURL) {
href = request.responseURL
}
}
else if (request.getResponseHeader("X-PJAX-URL")) {
href = request.getResponseHeader("X-PJAX-URL")
}
else if (request.getResponseHeader("X-XHR-Redirected-To")) {
href = request.getResponseHeader("X-XHR-Redirected-To")
}
// Add back the hash if it was removed
var a = document.createElement("a")
a.href = oldHref
var oldHash = a.hash
a.href = href
if (oldHash && !a.hash) {
a.hash = oldHash
href = a.href
}
this.state.href = href
this.state.options = clone(options)
try {
this.loadContent(html, options)
}
catch (e) {
if (!this.options.debug) {
if (console && console.error) {
console.error("Pjax switch fail: ", e)
}
return this.latestChance(href)
}
else {
throw e
}
}
}.bind(this))
this.request = this.doRequest(
href,
options,
this.handleResponse.bind(this)
);
},
afterAllSwitches: function() {
@@ -219,114 +198,121 @@ Pjax.prototype = {
// the last field.
//
// http://www.w3.org/html/wg/drafts/html/master/forms.html
var autofocusEl = Array.prototype.slice.call(document.querySelectorAll("[autofocus]")).pop()
var autofocusEl = Array.prototype.slice
.call(document.querySelectorAll("[autofocus]"))
.pop();
if (autofocusEl && document.activeElement !== autofocusEl) {
autofocusEl.focus()
autofocusEl.focus();
}
// execute scripts when DOM have been completely updated
this.options.selectors.forEach(function(selector) {
forEachEls(document.querySelectorAll(selector), function(el) {
executeScripts(el)
})
})
executeScripts(el);
});
});
var state = this.state
var state = this.state;
if (state.options.history) {
if (!window.history.state) {
this.lastUid = this.maxUid = newUid()
window.history.replaceState({
this.lastUid = this.maxUid = newUid();
window.history.replaceState(
{
url: window.location.href,
title: document.title,
uid: this.maxUid,
scrollPos: [0, 0]
},
document.title)
document.title
);
}
// Update browser history
this.lastUid = this.maxUid = newUid()
this.lastUid = this.maxUid = newUid();
window.history.pushState({
window.history.pushState(
{
url: state.href,
title: state.options.title,
uid: this.maxUid,
scrollPos: [0, 0]
},
state.options.title,
state.href)
state.href
);
}
this.forEachSelectors(function(el) {
this.parseDOM(el)
}, this)
this.parseDOM(el);
}, this);
// Fire Events
trigger(document,"pjax:complete pjax:success", state.options)
trigger(document, "pjax:complete pjax:success", state.options);
if (typeof state.options.analytics === "function") {
state.options.analytics()
state.options.analytics();
}
if (state.options.history) {
// First parse url and check for hash to override scroll
var a = document.createElement("a")
a.href = this.state.href
var a = document.createElement("a");
a.href = this.state.href;
if (a.hash) {
var name = a.hash.slice(1)
name = decodeURIComponent(name)
var name = a.hash.slice(1);
name = decodeURIComponent(name);
var curtop = 0
var target = document.getElementById(name) || document.getElementsByName(name)[0]
var curtop = 0;
var target =
document.getElementById(name) || document.getElementsByName(name)[0];
if (target) {
// http://stackoverflow.com/questions/8111094/cross-browser-javascript-function-to-find-actual-position-of-an-element-in-page
if (target.offsetParent) {
do {
curtop += target.offsetTop
curtop += target.offsetTop;
target = target.offsetParent
} while (target)
target = target.offsetParent;
} while (target);
}
}
window.scrollTo(0, curtop)
}
else if (state.options.scrollTo !== false) {
window.scrollTo(0, curtop);
} else if (state.options.scrollTo !== false) {
// Scroll page to top on new page load
if (state.options.scrollTo.length > 1) {
window.scrollTo(state.options.scrollTo[0], state.options.scrollTo[1])
}
else {
window.scrollTo(0, state.options.scrollTo)
window.scrollTo(state.options.scrollTo[0], state.options.scrollTo[1]);
} else {
window.scrollTo(0, state.options.scrollTo);
}
}
}
else if (state.options.scrollRestoration && state.options.scrollPos) {
window.scrollTo(state.options.scrollPos[0], state.options.scrollPos[1])
} else if (state.options.scrollRestoration && state.options.scrollPos) {
window.scrollTo(state.options.scrollPos[0], state.options.scrollPos[1]);
}
this.state = {
numPendingSwitches: 0,
href: null,
options: null
};
}
}
}
};
Pjax.isSupported = require("./lib/is-supported.js")
Pjax.isSupported = require("./lib/is-supported");
// arguably could do `if( require("./lib/is-supported.js")()) {` but that might be a little to simple
// arguably could do `if( require("./lib/is-supported")()) {` but that might be a little to simple
if (Pjax.isSupported()) {
module.exports = Pjax
module.exports = Pjax;
}
// if there isnt required browser functions, returning stupid api
else {
var stupidPjax = noop
var stupidPjax = noop;
for (var key in Pjax.prototype) {
if (Pjax.prototype.hasOwnProperty(key) && typeof Pjax.prototype[key] === "function") {
stupidPjax[key] = noop
if (
Pjax.prototype.hasOwnProperty(key) &&
typeof Pjax.prototype[key] === "function"
) {
stupidPjax[key] = noop;
}
}
module.exports = stupidPjax
module.exports = stupidPjax;
}

View File

@@ -1,8 +1,8 @@
var noop = require("./util/noop")
var noop = require("./util/noop");
module.exports = function(request) {
if (request && request.readyState < 4) {
request.onreadystatechange = noop
request.abort()
request.onreadystatechange = noop;
request.abort();
}
}
};

View File

@@ -1,39 +1,48 @@
module.exports = function(el) {
var code = (el.text || el.textContent || el.innerHTML || "")
var src = (el.src || "")
var parent = el.parentNode || document.querySelector("head") || document.documentElement
var script = document.createElement("script")
var code = el.text || el.textContent || el.innerHTML || "";
var src = el.src || "";
var parent =
el.parentNode || document.querySelector("head") || document.documentElement;
var 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)
console.log(
"Script contains document.write. Cant be executed correctly. Code skipped ",
el
);
}
return false
return false;
}
script.type = "text/javascript"
script.type = "text/javascript";
script.id = el.id;
/* istanbul ignore if */
if (src !== "") {
script.src = src
script.async = false // force synchronous loading of peripheral JS
script.src = src;
script.async = false; // force synchronous loading of peripheral JS
}
if (code !== "") {
try {
script.appendChild(document.createTextNode(code))
}
catch (e) {
script.appendChild(document.createTextNode(code));
} catch (e) {
/* istanbul ignore next */
// old IEs have funky script nodes
script.text = code
script.text = code;
}
}
// execute
parent.appendChild(script)
parent.appendChild(script);
// avoid pollution only in head or body tags
if (["head", "body"].indexOf(parent.tagName.toLowerCase()) > 0) {
parent.removeChild(script)
if (
(parent instanceof HTMLHeadElement || parent instanceof HTMLBodyElement) &&
parent.contains(script)
) {
parent.removeChild(script);
}
return true
}
return true;
};

View File

@@ -1,11 +1,11 @@
var forEachEls = require("../foreach-els")
var forEachEls = require("../foreach-els");
module.exports = function(els, events, listener, useCapture) {
events = (typeof events === "string" ? events.split(" ") : events)
events = typeof events === "string" ? events.split(" ") : events;
events.forEach(function(e) {
forEachEls(els, function(el) {
el.removeEventListener(e, listener, useCapture)
})
})
}
el.removeEventListener(e, listener, useCapture);
});
});
};

View File

@@ -1,11 +1,11 @@
var forEachEls = require("../foreach-els")
var forEachEls = require("../foreach-els");
module.exports = function(els, events, listener, useCapture) {
events = (typeof events === "string" ? events.split(" ") : events)
events = typeof events === "string" ? events.split(" ") : events;
events.forEach(function(e) {
forEachEls(els, function(el) {
el.addEventListener(e, listener, useCapture)
})
})
}
el.addEventListener(e, listener, useCapture);
});
});
};

View File

@@ -1,31 +1,31 @@
var forEachEls = require("../foreach-els")
var forEachEls = require("../foreach-els");
module.exports = function(els, events, opts) {
events = (typeof events === "string" ? events.split(" ") : events)
events = typeof events === "string" ? events.split(" ") : events;
events.forEach(function(e) {
var event
event = document.createEvent("HTMLEvents")
event.initEvent(e, true, true)
event.eventName = e
var event;
event = document.createEvent("HTMLEvents");
event.initEvent(e, true, true);
event.eventName = e;
if (opts) {
Object.keys(opts).forEach(function(key) {
event[key] = opts[key]
})
event[key] = opts[key];
});
}
forEachEls(els, function(el) {
var domFix = false
var domFix = false;
if (!el.parentNode && el !== document && el !== window) {
// THANK YOU IE (9/10/11)
// dispatchEvent doesn't work if the element is not in the DOM
domFix = true
document.body.appendChild(el)
domFix = true;
document.body.appendChild(el);
}
el.dispatchEvent(event)
el.dispatchEvent(event);
if (domFix) {
el.parentNode.removeChild(el)
el.parentNode.removeChild(el);
}
})
})
}
});
});
};

View File

@@ -1,18 +1,18 @@
var forEachEls = require("./foreach-els")
var evalScript = require("./eval-script")
var forEachEls = require("./foreach-els");
var evalScript = require("./eval-script");
// Finds and executes scripts (used for newly added elements)
// Needed since innerHTML does not run scripts
module.exports = function(el) {
if (el.tagName.toLowerCase() === "script") {
evalScript(el)
evalScript(el);
}
forEachEls(el.querySelectorAll("script"), function(script) {
if (!script.type || script.type.toLowerCase() === "text/javascript") {
if (script.parentNode) {
script.parentNode.removeChild(script)
script.parentNode.removeChild(script);
}
evalScript(script)
evalScript(script);
}
})
}
});
};

View File

@@ -1,9 +1,13 @@
/* global HTMLCollection: true */
module.exports = function(els, fn, context) {
if (els instanceof HTMLCollection || els instanceof NodeList || els instanceof Array) {
return Array.prototype.forEach.call(els, fn, context)
if (
els instanceof HTMLCollection ||
els instanceof NodeList ||
els instanceof Array
) {
return Array.prototype.forEach.call(els, fn, context);
}
// assume simple DOM element
return fn.call(context, els)
}
return fn.call(context, els);
};

View File

@@ -1,8 +1,8 @@
var forEachEls = require("./foreach-els")
var forEachEls = require("./foreach-els");
module.exports = function(selectors, cb, context, DOMcontext) {
DOMcontext = DOMcontext || document
DOMcontext = DOMcontext || document;
selectors.forEach(function(selector) {
forEachEls(DOMcontext.querySelectorAll(selector), cb, context)
})
}
forEachEls(DOMcontext.querySelectorAll(selector), cb, context);
});
};

View File

@@ -1,8 +1,12 @@
module.exports = function() {
// Borrowed wholesale from https://github.com/defunkt/jquery-pjax
return window.history &&
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]\D|WebApps\/.+CFNetwork)/)
}
!navigator.userAgent.match(
/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/
)
);
};

53
lib/parse-options.js Normal file
View File

@@ -0,0 +1,53 @@
/* global _gaq: true, ga: true */
var defaultSwitches = require("./switches");
module.exports = function(options) {
options = options || {};
options.elements = options.elements || "a[href], form[action]";
options.selectors = options.selectors || ["title", ".js-Pjax"];
options.switches = options.switches || {};
options.switchesOptions = options.switchesOptions || {};
options.history =
typeof options.history === "undefined" ? true : options.history;
options.analytics =
typeof options.analytics === "function" || options.analytics === false
? options.analytics
: defaultAnalytics;
options.scrollTo =
typeof options.scrollTo === "undefined" ? 0 : options.scrollTo;
options.scrollRestoration =
typeof options.scrollRestoration !== "undefined"
? options.scrollRestoration
: true;
options.cacheBust =
typeof options.cacheBust === "undefined" ? true : options.cacheBust;
options.debug = options.debug || false;
options.timeout = options.timeout || 0;
options.currentUrlFullReload =
typeof options.currentUrlFullReload === "undefined"
? false
: options.currentUrlFullReload;
// We cant replace body.outerHTML or head.outerHTML.
// It creates a bug where a new body or head are created in the DOM.
// If you set head.outerHTML, a new body tag is appended, so the DOM has 2 body nodes, and vice versa
if (!options.switches.head) {
options.switches.head = defaultSwitches.switchElementsAlt;
}
if (!options.switches.body) {
options.switches.body = defaultSwitches.switchElementsAlt;
}
return options;
};
/* istanbul ignore next */
function defaultAnalytics() {
if (window._gaq) {
_gaq.push(["_trackPageview"]);
}
if (window.ga) {
ga("send", "pageview", { page: location.pathname, title: document.title });
}
}

View File

@@ -1,95 +1,139 @@
var on = require("../events/on")
var clone = require("../clone")
var on = require("../events/on");
var clone = require("../util/clone");
var attrClick = "data-pjax-click-state"
var attrState = "data-pjax-state";
var formAction = function(el, event) {
if (isDefaultPrevented(event)) {
return;
}
// Since loadUrl modifies options and we may add our own modifications below,
// clone it so the changes don't persist
var options = clone(this.options)
var options = clone(this.options);
// Initialize requestOptions
options.requestOptions = {
requestUrl: el.getAttribute("action") || window.location.href,
requestMethod: el.getAttribute("method") || "GET"
}
};
// create a testable virtual link of the form action
var virtLinkElement = document.createElement("a")
virtLinkElement.setAttribute("href", options.requestOptions.requestUrl)
var virtLinkElement = document.createElement("a");
virtLinkElement.setAttribute("href", options.requestOptions.requestUrl);
var attrValue = checkIfShouldAbort(virtLinkElement, options);
if (attrValue) {
el.setAttribute(attrState, attrValue);
return;
}
event.preventDefault();
if (el.enctype === "multipart/form-data") {
options.requestOptions.formData = new FormData(el);
} else {
options.requestOptions.requestParams = parseFormElements(el);
}
el.setAttribute(attrState, "submit");
options.triggerElement = el;
this.loadUrl(virtLinkElement.href, options);
};
function parseFormElements(el) {
var requestParams = [];
var formElements = el.elements;
for (var i = 0; i < formElements.length; i++) {
var element = formElements[i];
var tagName = element.tagName.toLowerCase();
// jscs:disable disallowImplicitTypeConversion
if (
!!element.name &&
element.attributes !== undefined &&
tagName !== "button"
) {
// jscs:enable disallowImplicitTypeConversion
var type = element.attributes.type;
if (
!type ||
(type.value !== "checkbox" && type.value !== "radio") ||
element.checked
) {
// Build array of values to submit
var values = [];
if (tagName === "select") {
var opt;
for (var j = 0; j < element.options.length; j++) {
opt = element.options[j];
if (opt.selected && !opt.disabled) {
values.push(opt.hasAttribute("value") ? opt.value : opt.text);
}
}
} else {
values.push(element.value);
}
for (var k = 0; k < values.length; k++) {
requestParams.push({
name: encodeURIComponent(element.name),
value: encodeURIComponent(values[k])
});
}
}
}
}
return requestParams;
}
function checkIfShouldAbort(virtLinkElement, options) {
// Ignore external links.
if (virtLinkElement.protocol !== window.location.protocol || virtLinkElement.host !== window.location.host) {
el.setAttribute(attrClick, "external")
return
if (
virtLinkElement.protocol !== window.location.protocol ||
virtLinkElement.host !== window.location.host
) {
return "external";
}
// Ignore click if we are on an anchor on the same page
if (virtLinkElement.pathname === window.location.pathname && virtLinkElement.hash.length > 0) {
el.setAttribute(attrClick, "anchor-present")
return
if (
virtLinkElement.hash &&
virtLinkElement.href.replace(virtLinkElement.hash, "") ===
window.location.href.replace(location.hash, "")
) {
return "anchor";
}
// Ignore empty anchor "foo.html#"
if (virtLinkElement.href === window.location.href.split("#")[0] + "#") {
el.setAttribute(attrClick, "anchor-empty")
return
return "anchor-empty";
}
// if declared as a full reload, just normally submit the form
if (options.currentUrlFullReload) {
el.setAttribute(attrClick, "reload")
return
if (
options.currentUrlFullReload &&
virtLinkElement.href === window.location.href.split("#")[0]
) {
return "reload";
}
event.preventDefault()
var paramObject = []
for (var elementKey in el.elements) {
var element = el.elements[elementKey]
// jscs:disable disallowImplicitTypeConversion
if (!!element.name && element.attributes !== undefined && element.tagName.toLowerCase() !== "button") {
// jscs:enable disallowImplicitTypeConversion
if ((element.attributes.type !== "checkbox" && element.attributes.type !== "radio") || element.checked) {
paramObject.push({name: encodeURIComponent(element.name), value: encodeURIComponent(element.value)})
}
}
}
// Creating a getString
var paramsString = (paramObject.map(function(value) {return value.name + "=" + value.value})).join("&")
options.requestOptions.requestPayload = paramObject
options.requestOptions.requestPayloadString = paramsString
el.setAttribute(attrClick, "submit")
options.triggerElement = el
this.loadUrl(virtLinkElement.href, options)
}
var isDefaultPrevented = function(event) {
return event.defaultPrevented || event.returnValue === false
}
return event.defaultPrevented || event.returnValue === false;
};
module.exports = function(el) {
var that = this
var that = this;
el.setAttribute(attrState, "");
on(el, "submit", function(event) {
if (isDefaultPrevented(event)) {
return
}
formAction.call(that, el, event)
})
on(el, "keyup", function(event) {
if (isDefaultPrevented(event)) {
return
}
if (event.keyCode === 13) {
formAction.call(that, el, event)
}
}.bind(this))
}
formAction.call(that, el, event);
});
};

View File

@@ -1,96 +1,99 @@
var on = require("../events/on")
var clone = require("../clone")
var on = require("../events/on");
var clone = require("../util/clone");
var attrClick = "data-pjax-click-state"
var attrKey = "data-pjax-keyup-state"
var attrState = "data-pjax-state";
var linkAction = function(el, event) {
if (isDefaultPrevented(event)) {
return;
}
// Since loadUrl modifies options and we may add our own modifications below,
// clone it so the changes don't persist
var options = clone(this.options)
var options = clone(this.options);
// Initialize requestOptions since loadUrl expects it to be an object
options.requestOptions = {}
// Dont break browser special behavior on links (like page in new window)
if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
el.setAttribute(attrClick, "modifier")
return
var attrValue = checkIfShouldAbort(el, event);
if (attrValue) {
el.setAttribute(attrState, attrValue);
return;
}
// we do test on href now to prevent unexpected behavior if for some reason
// user have href that can be dynamically updated
// Ignore external links.
if (el.protocol !== window.location.protocol || el.host !== window.location.host) {
el.setAttribute(attrClick, "external")
return
}
// Ignore click if we are on an anchor on the same page
if (el.pathname === window.location.pathname && el.hash.length > 0) {
el.setAttribute(attrClick, "anchor-present")
return
}
// Ignore anchors on the same page (keep native behavior)
if (el.hash && el.href.replace(el.hash, "") === window.location.href.replace(location.hash, "")) {
el.setAttribute(attrClick, "anchor")
return
}
// Ignore empty anchor "foo.html#"
if (el.href === window.location.href.split("#")[0] + "#") {
el.setAttribute(attrClick, "anchor-empty")
return
}
event.preventDefault()
event.preventDefault();
// dont do "nothing" if user try to reload the page by clicking the same link twice
if (
this.options.currentUrlFullReload &&
el.href === window.location.href.split("#")[0]
) {
el.setAttribute(attrClick, "reload")
this.reload()
return
el.setAttribute(attrState, "reload");
this.reload();
return;
}
el.setAttribute(attrClick, "load")
el.setAttribute(attrState, "load");
options.triggerElement = el
this.loadUrl(el.href, options)
options.triggerElement = el;
this.loadUrl(el.href, options);
};
function checkIfShouldAbort(el, event) {
// 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 "modifier";
}
// we do test on href now to prevent unexpected behavior if for some reason
// user have href that can be dynamically updated
// Ignore external links.
if (
el.protocol !== window.location.protocol ||
el.host !== window.location.host
) {
return "external";
}
// Ignore anchors on the same page (keep native behavior)
if (
el.hash &&
el.href.replace(el.hash, "") ===
window.location.href.replace(location.hash, "")
) {
return "anchor";
}
// Ignore empty anchor "foo.html#"
if (el.href === window.location.href.split("#")[0] + "#") {
return "anchor-empty";
}
}
var isDefaultPrevented = function(event) {
return event.defaultPrevented || event.returnValue === false
}
return event.defaultPrevented || event.returnValue === false;
};
module.exports = function(el) {
var that = this
var that = this;
el.setAttribute(attrState, "");
on(el, "click", function(event) {
if (isDefaultPrevented(event)) {
return
}
linkAction.call(that, el, event)
})
on(el, "keyup", function(event) {
if (isDefaultPrevented(event)) {
return
}
// Dont break browser special behavior on links (like page in new window)
if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
el.setAttribute(attrKey, "modifier")
return
}
linkAction.call(that, el, event);
});
on(
el,
"keyup",
function(event) {
if (event.keyCode === 13) {
linkAction.call(that, el, event)
linkAction.call(that, el, event);
}
}.bind(this))
}
}.bind(this)
);
};

View File

@@ -0,0 +1,71 @@
var clone = require("../util/clone");
var newUid = require("../uniqueid");
var trigger = require("../events/trigger");
module.exports = function(responseText, request, href, options) {
options = clone(options || this.options);
options.request = request;
// Fail if unable to load HTML via AJAX
if (responseText === false) {
trigger(document, "pjax:complete pjax:error", options);
return;
}
// push scroll position to history
var currentState = window.history.state || {};
window.history.replaceState(
{
url: currentState.url || window.location.href,
title: currentState.title || document.title,
uid: currentState.uid || newUid(),
scrollPos: [
document.documentElement.scrollLeft || document.body.scrollLeft,
document.documentElement.scrollTop || document.body.scrollTop
]
},
document.title,
window.location.href
);
// Check for redirects
var oldHref = href;
if (request.responseURL) {
if (href !== request.responseURL) {
href = request.responseURL;
}
} else if (request.getResponseHeader("X-PJAX-URL")) {
href = request.getResponseHeader("X-PJAX-URL");
} else if (request.getResponseHeader("X-XHR-Redirected-To")) {
href = request.getResponseHeader("X-XHR-Redirected-To");
}
// Add back the hash if it was removed
var a = document.createElement("a");
a.href = oldHref;
var oldHash = a.hash;
a.href = href;
if (oldHash && !a.hash) {
a.hash = oldHash;
href = a.href;
}
this.state.href = href;
this.state.options = options;
try {
this.loadContent(responseText, options);
} catch (e) {
trigger(document, "pjax:error", options);
if (!this.options.debug) {
if (console && console.error) {
console.error("Pjax switch fail: ", e);
}
return this.latestChance(href);
} else {
throw e;
}
}
};

View File

@@ -1,11 +1,11 @@
module.exports = function() {
if (this.options.debug && console) {
if (typeof console.log === "function") {
console.log.apply(console, arguments)
console.log.apply(console, arguments);
}
// IE is weird
else if (console.log) {
console.log(arguments)
console.log(arguments);
}
}
}
};

View File

@@ -1,20 +1,22 @@
var attrState = "data-pjax-state";
module.exports = function(el) {
switch (el.tagName.toLowerCase()) {
case "a":
// only attach link if el does not already have link attached
if (!el.hasAttribute("data-pjax-click-state")) {
this.attachLink(el)
if (!el.hasAttribute(attrState)) {
this.attachLink(el);
}
break
break;
case "form":
// only attach link if el does not already have link attached
if (!el.hasAttribute("data-pjax-click-state")) {
this.attachForm(el)
if (!el.hasAttribute(attrState)) {
this.attachForm(el);
}
break
break;
default:
throw "Pjax can only be applied on <a> or <form> submit"
throw "Pjax can only be applied on <a> or <form> submit";
}
}
};

View File

@@ -1,40 +0,0 @@
/* global _gaq: true, ga: true */
var defaultSwitches = require("../switches")
module.exports = function(options) {
options = options || {}
options.elements = options.elements || "a[href], form[action]"
options.selectors = options.selectors || ["title", ".js-Pjax"]
options.switches = options.switches || {}
options.switchesOptions = options.switchesOptions || {}
options.history = options.history || true
options.analytics = (typeof options.analytics === "function" || options.analytics === false) ?
options.analytics :
function() {
if (window._gaq) {
_gaq.push(["_trackPageview"])
}
if (window.ga) {
ga("send", "pageview", {page: location.pathname, title: document.title})
}
}
options.scrollTo = (typeof options.scrollTo === "undefined") ? 0 : options.scrollTo
options.scrollRestoration = (typeof options.scrollRestoration !== "undefined") ? options.scrollRestoration : true
options.cacheBust = (typeof options.cacheBust === "undefined") ? true : options.cacheBust
options.debug = options.debug || false
options.timeout = options.timeout || 0
options.currentUrlFullReload = (typeof options.currentUrlFullReload === "undefined") ? false : options.currentUrlFullReload
// We cant replace body.outerHTML or head.outerHTML.
// It creates a bug where a new body or head are created in the DOM.
// If you set head.outerHTML, a new body tag is appended, so the DOM has 2 body nodes, and vice versa
if (!options.switches.head) {
options.switches.head = defaultSwitches.switchElementsAlt
}
if (!options.switches.body) {
options.switches.body = defaultSwitches.switchElementsAlt
}
this.options = options
}

View File

@@ -1,46 +1,86 @@
var updateQueryString = require("./util/update-query-string");
module.exports = function(location, options, callback) {
options = options || {}
var requestMethod = options.requestMethod || "GET"
var requestPayload = options.requestPayloadString || null
var request = new XMLHttpRequest()
options = options || {};
var queryString;
var requestOptions = options.requestOptions || {};
var requestMethod = (requestOptions.requestMethod || "GET").toUpperCase();
var requestParams = requestOptions.requestParams || null;
var formData = requestOptions.formData || null;
var requestPayload = null;
var request = new XMLHttpRequest();
var timeout = options.timeout || 0;
request.onreadystatechange = function() {
if (request.readyState === 4) {
if (request.status === 200) {
callback(request.responseText, request)
}
else {
callback(null, request)
}
callback(request.responseText, request, location, options);
} else if (request.status !== 0) {
callback(null, request, location, options);
}
}
};
request.onerror = function(e) {
console.log(e)
callback(null, request)
}
console.log(e);
callback(null, request, location, options);
};
request.ontimeout = function() {
callback(null, request)
callback(null, request, location, options);
};
// Prepare the request payload for forms, if available
if (requestParams && requestParams.length) {
// Build query string
queryString = requestParams
.map(function(param) {
return param.name + "=" + param.value;
})
.join("&");
switch (requestMethod) {
case "GET":
// Reset query string to avoid an issue with repeat submissions where checkboxes that were
// previously checked are incorrectly preserved
location = location.split("?")[0];
// Append new query string
location += "?" + queryString;
break;
case "POST":
// Send query string as request payload
requestPayload = queryString;
break;
}
} else if (formData) {
requestPayload = formData;
}
// Add a timestamp as part of the query string if cache busting is enabled
if (this.options.cacheBust) {
location += (!/[?&]/.test(location) ? "?" : "&") + new Date().getTime()
if (options.cacheBust) {
location = updateQueryString(location, "t", Date.now());
}
request.open(requestMethod.toUpperCase(), location, true)
request.timeout = options.timeout
request.setRequestHeader("X-Requested-With", "XMLHttpRequest")
request.setRequestHeader("X-PJAX", "true")
request.open(requestMethod, location, true);
request.timeout = timeout;
request.setRequestHeader("X-Requested-With", "XMLHttpRequest");
request.setRequestHeader("X-PJAX", "true");
request.setRequestHeader(
"X-PJAX-Selectors",
JSON.stringify(options.selectors)
);
// Add the request payload if available
if (options.requestPayloadString !== undefined && options.requestPayloadString !== "") {
// Send the proper header information along with the request
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
// Send the proper header information for POST forms
if (requestPayload && requestMethod === "POST" && !formData) {
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded"
);
}
request.send(requestPayload)
request.send(requestPayload);
return request
}
return request;
};

View File

@@ -1,37 +1,59 @@
var forEachEls = require("./foreach-els")
var forEachEls = require("./foreach-els");
var defaultSwitches = require("./switches")
var defaultSwitches = require("./switches");
module.exports = function(switches, switchesOptions, selectors, fromEl, toEl, options) {
var switchesQueue = []
module.exports = function(
switches,
switchesOptions,
selectors,
fromEl,
toEl,
options
) {
var switchesQueue = [];
selectors.forEach(function(selector) {
var newEls = fromEl.querySelectorAll(selector)
var oldEls = toEl.querySelectorAll(selector)
var newEls = fromEl.querySelectorAll(selector);
var oldEls = toEl.querySelectorAll(selector);
if (this.log) {
this.log("Pjax switch", selector, newEls, oldEls)
this.log("Pjax switch", selector, newEls, oldEls);
}
if (newEls.length !== oldEls.length) {
throw "DOM doesnt look the same on new loaded page: " + selector + " - new " + newEls.length + ", old " + oldEls.length
throw "DOM doesnt look the same on new loaded page: " +
selector +
" - new " +
newEls.length +
", old " +
oldEls.length;
}
forEachEls(newEls, function(newEl, i) {
var oldEl = oldEls[i]
forEachEls(
newEls,
function(newEl, i) {
var oldEl = oldEls[i];
if (this.log) {
this.log("newEl", newEl, "oldEl", oldEl)
this.log("newEl", newEl, "oldEl", oldEl);
}
var callback = (switches[selector]) ?
switches[selector].bind(this, oldEl, newEl, options, switchesOptions[selector]) :
defaultSwitches.outerHTML.bind(this, oldEl, newEl, options)
var callback = switches[selector]
? switches[selector].bind(
this,
oldEl,
newEl,
options,
switchesOptions[selector]
)
: defaultSwitches.outerHTML.bind(this, oldEl, newEl, options);
switchesQueue.push(callback)
}, this)
}, this)
switchesQueue.push(callback);
},
this
);
}, this);
this.state.numPendingSwitches = switchesQueue.length
this.state.numPendingSwitches = switchesQueue.length;
switchesQueue.forEach(function(queuedSwitch) {
queuedSwitch()
})
}
queuedSwitch();
});
};

View File

@@ -1,109 +1,140 @@
var on = require("./events/on.js")
var on = require("./events/on");
module.exports = {
outerHTML: function(oldEl, newEl) {
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
oldEl.outerHTML = newEl.outerHTML;
this.onSwitch();
},
innerHTML: function(oldEl, newEl) {
oldEl.innerHTML = newEl.innerHTML
oldEl.className = newEl.className
this.onSwitch()
oldEl.innerHTML = newEl.innerHTML;
if (newEl.className === "") {
oldEl.removeAttribute("class");
} else {
oldEl.className = newEl.className;
}
this.onSwitch();
},
switchElementsAlt: function(oldEl, newEl) {
oldEl.innerHTML = newEl.innerHTML
oldEl.innerHTML = newEl.innerHTML;
// Copy attributes from the new element to the old one
if (newEl.hasAttributes()) {
var attrs = newEl.attributes
var attrs = newEl.attributes;
for (var i = 0; i < attrs.length; i++) {
oldEl.attributes.setNamedItem(attrs[i].cloneNode())
oldEl.attributes.setNamedItem(attrs[i].cloneNode());
}
}
this.onSwitch()
this.onSwitch();
},
// Equivalent to outerHTML(), but doesn't require switchElementsAlt() for <head> and <body>
replaceNode: function(oldEl, newEl) {
oldEl.parentNode.replaceChild(newEl, oldEl);
this.onSwitch();
},
sideBySide: function(oldEl, newEl, options, switchOptions) {
var forEach = Array.prototype.forEach
var elsToRemove = []
var elsToAdd = []
var fragToAppend = document.createDocumentFragment()
var animationEventNames = "animationend webkitAnimationEnd MSAnimationEnd oanimationend"
var animatedElsNumber = 0
var forEach = Array.prototype.forEach;
var elsToRemove = [];
var elsToAdd = [];
var fragToAppend = document.createDocumentFragment();
var animationEventNames =
"animationend webkitAnimationEnd MSAnimationEnd oanimationend";
var animatedElsNumber = 0;
var sexyAnimationEnd = function(e) {
if (e.target !== e.currentTarget) {
// end triggered by an animation on a child
return
return;
}
animatedElsNumber--
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)
el.parentNode.removeChild(el);
}
})
});
elsToAdd.forEach(function(el) {
el.className = el.className.replace(el.getAttribute("data-pjax-classes"), "")
el.removeAttribute("data-pjax-classes")
})
el.className = el.className.replace(
el.getAttribute("data-pjax-classes"),
""
);
el.removeAttribute("data-pjax-classes");
});
elsToAdd = null // free memory
elsToRemove = null // free memory
elsToAdd = null; // free memory
elsToRemove = null; // free memory
// this is to trigger some repaint (example: picturefill)
this.onSwitch()
this.onSwitch();
}
}.bind(this)
}.bind(this);
switchOptions = switchOptions || {}
switchOptions = switchOptions || {};
forEach.call(oldEl.childNodes, function(el) {
elsToRemove.push(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.className = el.className.replace(
el.getAttribute("data-pjax-classes"),
""
);
el.removeAttribute("data-pjax-classes");
}
el.classList.add("js-Pjax-remove")
el.classList.add("js-Pjax-remove");
if (switchOptions.callbacks && switchOptions.callbacks.removeElement) {
switchOptions.callbacks.removeElement(el)
switchOptions.callbacks.removeElement(el);
}
if (switchOptions.classNames) {
el.className += " " + switchOptions.classNames.remove + " " + (options.backward ? switchOptions.classNames.backward : switchOptions.classNames.forward)
el.className +=
" " +
switchOptions.classNames.remove +
" " +
(options.backward
? switchOptions.classNames.backward
: switchOptions.classNames.forward);
}
animatedElsNumber++
on(el, animationEventNames, sexyAnimationEnd, true)
animatedElsNumber++;
on(el, animationEventNames, sexyAnimationEnd, true);
}
})
});
forEach.call(newEl.childNodes, function(el) {
if (el.classList) {
var addClasses = ""
var addClasses = "";
if (switchOptions.classNames) {
addClasses = " js-Pjax-add " + switchOptions.classNames.add + " " + (options.backward ? switchOptions.classNames.forward : switchOptions.classNames.backward)
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)
switchOptions.callbacks.addElement(el);
}
el.className += addClasses
el.setAttribute("data-pjax-classes", addClasses)
elsToAdd.push(el)
fragToAppend.appendChild(el)
animatedElsNumber++
on(el, animationEventNames, sexyAnimationEnd, true)
el.className += addClasses;
el.setAttribute("data-pjax-classes", addClasses);
elsToAdd.push(el);
fragToAppend.appendChild(el);
animatedElsNumber++;
on(el, animationEventNames, sexyAnimationEnd, true);
}
})
});
// pass all className of the parent
oldEl.className = newEl.className
oldEl.appendChild(fragToAppend)
oldEl.className = newEl.className;
oldEl.appendChild(fragToAppend);
}
}
};

View File

@@ -1,8 +1,8 @@
module.exports = (function() {
var counter = 0
var counter = 0;
return function() {
var id = ("pjax" + (new Date().getTime())) + "_" + counter
counter++
return id
}
})()
var id = "pjax" + new Date().getTime() + "_" + counter;
counter++;
return id;
};
})();

View File

@@ -1,12 +1,13 @@
module.exports = function(obj) {
/* istanbul ignore if */
if (null === obj || "object" !== typeof obj) {
return obj
return obj;
}
var copy = obj.constructor()
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = obj[attr]
copy[attr] = obj[attr];
}
}
return copy
}
return copy;
};

View File

@@ -1,12 +1,12 @@
module.exports = function contains(doc, selectors, el) {
for (var i = 0; i < selectors.length; i++) {
var selectedEls = doc.querySelectorAll(selectors[i])
var selectedEls = doc.querySelectorAll(selectors[i]);
for (var j = 0; j < selectedEls.length; j++) {
if (selectedEls[j].contains(el)) {
return true
return true;
}
}
}
return false
}
return false;
};

21
lib/util/extend.js Normal file
View File

@@ -0,0 +1,21 @@
module.exports = function(target) {
if (target == null) {
return null;
}
var to = Object(target);
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
if (source != null) {
for (var key in source) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(source, key)) {
to[key] = source[key];
}
}
}
}
return to;
};

View File

@@ -1 +1 @@
module.exports = function() {}
module.exports = function() {};

View File

@@ -0,0 +1,9 @@
module.exports = function(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf("?") !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, "$1" + key + "=" + value + "$2");
} else {
return uri + separator + key + "=" + value;
}
};

9349
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "pjax",
"version": "0.2.5",
"version": "0.2.8",
"description": "Easily enable fast AJAX navigation on any website (using pushState + XHR)",
"keywords": [
"pjax",
@@ -22,17 +22,22 @@
"index.js",
"lib",
"pjax.js",
"pjax.min.js"
"pjax.min.js",
"index.d.ts"
],
"types": "index.d.ts",
"devDependencies": {
"browserify": "^15.0.0",
"jscs": "^3.0.7",
"eslint": "^5.7.0",
"eslint-config-i-am-meticulous": "^11.0.0",
"husky": "^1.2.0",
"jsdom": "^11.5.1",
"jsdom-global": "^3.0.2",
"jshint": "^2.5.6",
"lint-staged": "^8.1.0",
"npmpub": "^3.1.0",
"nyc": "^11.4.1",
"opn-cli": "^3.1.0",
"prettier": "^1.14.3",
"serve": "^6.4.4",
"tap-nyc": "^1.0.3",
"tap-spec": "^4.1.1",
@@ -40,7 +45,7 @@
"uglify-js": "^3.3.8"
},
"scripts": {
"lint": "jscs . && jshint . --exclude-path .gitignore",
"lint": "eslint .",
"standalone": "browserify index.js --standalone Pjax > pjax.js",
"build": "npm run standalone && uglifyjs pjax.js -o pjax.min.js",
"build-debug": "browserify index.js --debug --standalone Pjax > pjax.js",
@@ -51,5 +56,17 @@
"example": "opn http://localhost:3000/example/ && serve -p 3000 .",
"prepublish": "npm run build",
"release": "npmpub"
},
"husky": {
"hooks": {
"pre-commit": "npm test && lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write",
"git add"
]
}
}

View File

@@ -1,18 +1,18 @@
var tape = require("tape")
var tape = require("tape");
var abortRequest = require("../../lib/abort-request.js")
var sendRequest = require("../../lib/send-request.js")
var abortRequest = require("../../lib/abort-request.js");
var sendRequest = require("../../lib/send-request.js");
// Polyfill responseURL property into XMLHttpRequest if it doesn't exist,
// just for the purposes of this test
// This polyfill is not complete; it won't show the updated location if a
// redirection occurred, but it's fine for our purposes.
if (!("responseURL" in XMLHttpRequest.prototype)) {
var nativeOpen = XMLHttpRequest.prototype.open
var nativeOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this.responseURL = url
return nativeOpen.apply(this, arguments)
}
this.responseURL = url;
return nativeOpen.apply(this, arguments);
};
}
tape("test aborting xhr request", function(t) {
@@ -20,37 +20,36 @@ tape("test aborting xhr request", function(t) {
options: {
cacheBust: true
}
})
});
t.test("- pending request is aborted", function(t) {
var r = requestCacheBust("https://httpbin.org/delay/10", {}, function() {
t.fail("xhr was not aborted")
})
t.equal(r.readyState, 1, "xhr readyState is '1' (SENT)")
abortRequest(r)
t.equal(r.readyState, 0, "xhr readyState is '0' (ABORTED)")
t.equal(r.status, 0, "xhr HTTP status is '0' (ABORTED)")
t.equal(r.responseText, "", "xhr response is empty")
t.end()
})
t.fail("xhr was not aborted");
});
t.equal(r.readyState, 1, "xhr readyState is '1' (SENT)");
abortRequest(r);
t.equal(r.readyState, 0, "xhr readyState is '0' (ABORTED)");
t.equal(r.status, 0, "xhr HTTP status is '0' (ABORTED)");
t.equal(r.responseText, "", "xhr response is empty");
t.end();
});
t.test("- request is not aborted if it has already completed", function(t) {
var r = requestCacheBust("https://httpbin.org/get", {}, function() {
abortRequest(r)
t.equal(r.readyState, 4, "xhr readyState is '4' (DONE)")
t.equal(r.status, 200, "xhr HTTP status is '200' (OK)")
t.end()
})
})
abortRequest(r);
t.equal(r.readyState, 4, "xhr readyState is '4' (DONE)");
t.equal(r.status, 200, "xhr HTTP status is '200' (OK)");
t.end();
});
});
t.test("- request is not aborted if it is undefined", function(t) {
var r
var r;
try {
abortRequest(r)
abortRequest(r);
} catch (e) {
t.fail("aborting an undefined request threw an error");
}
catch (e) {
t.fail("aborting an undefined request threw an error")
}
t.equal(typeof r, "undefined", "undefined xhr was ignored")
t.end()
})
t.end()
})
t.equal(typeof r, "undefined", "undefined xhr was ignored");
t.end();
});
t.end();
});

View File

@@ -1,17 +0,0 @@
var tape = require("tape")
var clone = require("../../lib/clone")
tape("test clone method", function(t) {
var obj = {one: 1, two: 2}
var cloned = clone(obj)
t.notEqual(obj, cloned, "cloned object isn't the object")
t.same(obj, cloned, "cloned object have the same values than object")
cloned.tree = 3
t.notSame(obj, cloned, "modified cloned object haven't the same values than object")
t.end()
})

View File

@@ -1,23 +1,49 @@
var tape = require("tape")
var tape = require("tape");
var evalScript = require("../../lib/eval-script")
var evalScript = require("../../lib/eval-script");
tape("test evalScript method", function(t) {
document.body.className = ""
document.body.className = "";
var script = document.createElement("script")
script.innerHTML = "document.body.className = 'executed'"
var script = document.createElement("script");
script.innerHTML = "document.body.className = 'executed'";
t.equal(document.body.className, "", "script hasn't been executed yet")
t.equal(document.body.className, "", "script hasn't been executed yet");
evalScript(script)
t.equal(document.body.className, "executed", "script has been properly executed")
evalScript(script);
t.equal(
document.body.className,
"executed",
"script has been properly executed"
);
script.innerHTML = "document.write('failure')"
document.body.text = "document.write hasn't been executed"
var bodyText = document.body.text
evalScript(script)
t.equal(document.body.text, bodyText, "document.write hasn't been executed")
script.innerHTML = "document.write('failure')";
var bodyText = "document.write hasn't been executed";
document.body.text = bodyText;
evalScript(script);
t.equal(document.body.text, bodyText, "document.write hasn't been executed");
t.end()
})
t.end();
});
tape(
"evalScript should not throw an error if the script removed itself",
function(t) {
var script = document.createElement("script");
script.id = "myScript";
script.innerHTML =
"const script = document.querySelector('#myScript');" +
"script.parentNode.removeChild(script);";
try {
evalScript(script);
t.pass("Missing script tested successfully");
} catch (e) {
console.error(e);
t.fail("Attempted to remove missing script");
}
t.end();
}
);

View File

@@ -1,110 +1,177 @@
var tape = require("tape")
var tape = require("tape");
var on = require("../../lib/events/on")
var off = require("../../lib/events/off")
var trigger = require("../../lib/events/trigger")
var on = require("../../lib/events/on");
var off = require("../../lib/events/off");
var trigger = require("../../lib/events/trigger");
var el = document.createElement("div")
var el2 = document.createElement("span")
var els = [el, el2]
var el = document.createElement("div");
var el2 = document.createElement("span");
var els = [el, el2];
var classCb = function() {
this.className += "on"
}
this.className += "on";
};
var attrCb = function() {
this.setAttribute("data-state", this.getAttribute("data-state") + "ON")
}
this.setAttribute("data-state", this.getAttribute("data-state") + "ON");
};
tape("test events on/off/trigger for one element, one event", function(t) {
el.className = ""
on(el, "click", classCb)
trigger(el, "click")
t.equal(el.className, "on", "attached callback has been fired properly")
el.className = "";
on(el, "click", classCb);
trigger(el, "click");
t.equal(el.className, "on", "attached callback has been fired properly");
el.className = "off"
off(el, "click", classCb)
trigger(el, "click")
t.equal(el.className, "off", "triggered event didn't fire detached callback")
el.className = "off";
off(el, "click", classCb);
trigger(el, "click");
t.equal(el.className, "off", "triggered event didn't fire detached callback");
t.end()
})
t.end();
});
tape("test events on/off/trigger for multiple elements, one event", function(t) {
el.className = ""
el2.className = ""
tape("test events on/off/trigger for multiple elements, one event", function(
t
) {
el.className = "";
el2.className = "";
on(els, "click", classCb)
trigger(els, "click")
t.equal(el.className, "on", "attached callback has been fired properly on the first element")
t.equal(el2.className, "on", "attached callback has been fired properly on the second element")
on(els, "click", classCb);
trigger(els, "click");
t.equal(
el.className,
"on",
"attached callback has been fired properly on the first element"
);
t.equal(
el2.className,
"on",
"attached callback has been fired properly on the second element"
);
el.className = "off"
el2.className = "off"
off(els, "click", classCb)
trigger(els, "click")
t.equal(el.className, "off", "triggered event didn't fire detached callback on the first element")
t.equal(el2.className, "off", "triggered event didn't fire detached callback on the second element")
el.className = "off";
el2.className = "off";
off(els, "click", classCb);
trigger(els, "click");
t.equal(
el.className,
"off",
"triggered event didn't fire detached callback on the first element"
);
t.equal(
el2.className,
"off",
"triggered event didn't fire detached callback on the second element"
);
t.end()
})
t.end();
});
tape("test events on/off/trigger for one element, multiple events", function(t) {
el.className = ""
on(el, "click mouseover", classCb)
trigger(el, "click mouseover")
t.equal(el.className, "onon", "attached callbacks have been fired properly")
tape("test events on/off/trigger for one element, multiple events", function(
t
) {
el.className = "";
on(el, "click mouseover", classCb);
trigger(el, "click mouseover");
t.equal(el.className, "onon", "attached callbacks have been fired properly");
el.className = "off"
off(el, "click mouseover", classCb)
trigger(el, "click mouseover")
t.equal(el.className, "off", "triggered events didn't fire detached callback")
el.className = "off";
off(el, "click mouseover", classCb);
trigger(el, "click mouseover");
t.equal(
el.className,
"off",
"triggered events didn't fire detached callback"
);
t.end()
})
t.end();
});
tape("test events on/off/trigger for multiple elements, multiple events", function(t) {
el.className = ""
el2.className = ""
el.setAttribute("data-state", "")
el2.setAttribute("data-state", "")
on(els, "click mouseover", classCb)
on(els, "resize scroll", attrCb)
trigger(els, "click mouseover resize scroll")
t.equal(el.className, "onon", "attached callbacks has been fired properly on the first element")
t.equal(el.getAttribute("data-state"), "ONON", "attached callbacks has been fired properly on the first element")
t.equal(el2.className, "onon", "attached callbacks has been fired properly on the second element")
t.equal(el2.getAttribute("data-state"), "ONON", "attached callbacks has been fired properly on the second element")
tape(
"test events on/off/trigger for multiple elements, multiple events",
function(t) {
el.className = "";
el2.className = "";
el.setAttribute("data-state", "");
el2.setAttribute("data-state", "");
on(els, "click mouseover", classCb);
on(els, "resize scroll", attrCb);
trigger(els, "click mouseover resize scroll");
t.equal(
el.className,
"onon",
"attached callbacks has been fired properly on the first element"
);
t.equal(
el.getAttribute("data-state"),
"ONON",
"attached callbacks has been fired properly on the first element"
);
t.equal(
el2.className,
"onon",
"attached callbacks has been fired properly on the second element"
);
t.equal(
el2.getAttribute("data-state"),
"ONON",
"attached callbacks has been fired properly on the second element"
);
el.className = "off"
el2.className = "off"
el.setAttribute("data-state", "off")
el2.setAttribute("data-state", "off")
off(els, "click mouseover", classCb)
off(els, "resize scroll", attrCb)
trigger(els, "click mouseover resize scroll")
t.equal(el.className, "off", "triggered events didn't fire detached callbacks on the first element")
t.equal(el.getAttribute("data-state"), "off", "triggered events didn't fire detached callbacks on the first element")
t.equal(el2.className, "off", "triggered events didn't fire detached callbacks on the first element")
t.equal(el2.getAttribute("data-state"), "off", "triggered events didn't fire detached callbacks on the first element")
el.className = "off";
el2.className = "off";
el.setAttribute("data-state", "off");
el2.setAttribute("data-state", "off");
off(els, "click mouseover", classCb);
off(els, "resize scroll", attrCb);
trigger(els, "click mouseover resize scroll");
t.equal(
el.className,
"off",
"triggered events didn't fire detached callbacks on the first element"
);
t.equal(
el.getAttribute("data-state"),
"off",
"triggered events didn't fire detached callbacks on the first element"
);
t.equal(
el2.className,
"off",
"triggered events didn't fire detached callbacks on the first element"
);
t.equal(
el2.getAttribute("data-state"),
"off",
"triggered events didn't fire detached callbacks on the first element"
);
t.end()
})
t.end();
}
);
tape("test events on top level elements", function(t) {
var el = document
var el = document;
el.className = ""
on(el, "click", classCb)
trigger(el, "click")
t.equal(el.className, "on", "attached callback has been fired properly on document")
el.className = "";
on(el, "click", classCb);
trigger(el, "click");
t.equal(
el.className,
"on",
"attached callback has been fired properly on document"
);
el = window
el = window;
el.className = ""
el.className = "";
// With jsdom, the default this is global, not window, so we need to explicitly bind to window.
on(el, "click", classCb.bind(window))
trigger(el, "click")
t.equal(el.className, "on", "attached callback has been fired properly on window")
on(el, "click", classCb.bind(window));
trigger(el, "click");
t.equal(
el.className,
"on",
"attached callback has been fired properly on window"
);
t.end()
})
t.end();
});

View File

@@ -1,16 +1,49 @@
var tape = require("tape")
var tape = require("tape");
var executeScripts = require("../../lib/execute-scripts")
var executeScripts = require("../../lib/execute-scripts");
tape("test executeScripts method", function(t) {
document.body.className = ""
tape(
"test executeScripts method when the script tag is inside a container",
function(t) {
document.body.className = "";
var container = document.createElement("div")
container.innerHTML = "<" + "script" + ">document.body.className = 'executed';</" + "script" + "><" + "script" + ">document.body.className += ' correctly';</" + "script" + ">"
var container = document.createElement("div");
container.innerHTML =
"<" +
"script" +
">document.body.className = 'executed';</" +
"script" +
"><" +
"script" +
">document.body.className += ' correctly';</" +
"script" +
">";
t.equal(document.body.className, "", "script hasn't been executed yet")
executeScripts(container)
t.equal(document.body.className, "executed correctly", "script has been properly executed")
t.equal(document.body.className, "", "script hasn't been executed yet");
executeScripts(container);
t.equal(
document.body.className,
"executed correctly",
"script has been properly executed"
);
t.end()
})
t.end();
}
);
tape("test executeScripts method with just a script tag", function(t) {
document.body.className = "";
var script = document.createElement("script");
script.innerHTML = "document.body.className = 'executed correctly';";
t.equal(document.body.className, "", "script hasn't been executed yet");
executeScripts(script);
t.equal(
document.body.className,
"executed correctly",
"script has been properly executed"
);
t.end();
});

View File

@@ -1,45 +1,60 @@
var tape = require("tape")
var tape = require("tape");
var forEachEls = require("../../lib/foreach-els.js")
var forEachEls = require("../../lib/foreach-els.js");
var div = document.createElement("div")
var span = document.createElement("span")
var div = document.createElement("div");
var span = document.createElement("span");
var cb = function(el) {
el.innerHTML = "boom"
}
el.innerHTML = "boom";
};
tape("test forEachEls on one element", function(t) {
div.innerHTML = "div tag"
forEachEls(div, cb)
t.equal(div.innerHTML, "boom", "works correctly on one element")
t.end()
})
div.innerHTML = "div tag";
forEachEls(div, cb);
t.equal(div.innerHTML, "boom", "works correctly on one element");
t.end();
});
tape("test forEachEls on an array", function(t) {
div.innerHTML = "div tag"
span.innerHTML = "span tag"
div.innerHTML = "div tag";
span.innerHTML = "span tag";
forEachEls([div, span], cb)
forEachEls([div, span], cb);
t.equal(div.innerHTML, "boom", "works correctly on the first element of the array")
t.equal(span.innerHTML, "boom", "works correctly on the last element of the array")
t.equal(
div.innerHTML,
"boom",
"works correctly on the first element of the array"
);
t.equal(
span.innerHTML,
"boom",
"works correctly on the last element of the array"
);
t.end()
})
t.end();
});
tape("test forEachEls on a NodeList", function(t) {
div.innerHTML = "div tag"
span.innerHTML = "span tag"
div.innerHTML = "div tag";
span.innerHTML = "span tag";
var frag = document.createDocumentFragment()
frag.appendChild(div)
frag.appendChild(span)
forEachEls(frag.childNodes, cb)
var frag = document.createDocumentFragment();
frag.appendChild(div);
frag.appendChild(span);
forEachEls(frag.childNodes, cb);
t.equal(div.innerHTML, "boom", "works correctly on the first element of the document fragment")
t.equal(span.innerHTML, "boom", "works correctly on the last element of the document fragment")
t.equal(
div.innerHTML,
"boom",
"works correctly on the first element of the document fragment"
);
t.equal(
span.innerHTML,
"boom",
"works correctly on the last element of the document fragment"
);
t.end()
})
t.end();
});

View File

@@ -1,24 +1,40 @@
var tape = require("tape")
var tape = require("tape");
var forEachEls = require("../../lib/foreach-selectors.js")
var forEachEls = require("../../lib/foreach-selectors.js");
var cb = function(el) {
el.className = "modified"
}
el.className = "modified";
};
tape("test forEachSelector", function(t) {
forEachEls(["html", "body"], cb)
forEachEls(["html", "body"], cb);
t.equal(document.documentElement.className, "modified", "callback has been executed on first selector")
t.equal(document.body.className, "modified", "callback has been executed on first selector")
t.equal(
document.documentElement.className,
"modified",
"callback has been executed on first selector"
);
t.equal(
document.body.className,
"modified",
"callback has been executed on first selector"
);
document.documentElement.className = ""
document.body.className = ""
document.documentElement.className = "";
document.body.className = "";
forEachEls(["html", "body"], cb, null, document.documentElement)
forEachEls(["html", "body"], cb, null, document.documentElement);
t.equal(document.documentElement.className, "", "callback has not been executed on first selector when context is used")
t.equal(document.body.className, "modified", "callback has been executed on first selector when context is used")
t.equal(
document.documentElement.className,
"",
"callback has not been executed on first selector when context is used"
);
t.equal(
document.body.className,
"modified",
"callback has been executed on first selector when context is used"
);
t.end()
})
t.end();
});

View File

@@ -1,8 +1,11 @@
var tape = require("tape")
var tape = require("tape");
var isSupported = require("../../lib/is-supported.js")
var isSupported = require("../../lib/is-supported.js");
tape("test isSupported method", function(t) {
t.true(isSupported(), "well, we run test on supported browser, so it should be ok here")
t.end()
})
t.true(
isSupported(),
"well, we run test on supported browser, so it should be ok here"
);
t.end();
});

View File

@@ -0,0 +1,50 @@
var tape = require("tape");
var parseOptions = require("../../lib/parse-options.js");
tape("test parse initalization options function", function(t) {
t.test("- default options", function(t) {
var pjax = {};
pjax.options = parseOptions({});
t.equal(pjax.options.elements, "a[href], form[action]");
t.equal(pjax.options.selectors.length, 2, "selectors length");
t.equal(pjax.options.selectors[0], "title");
t.equal(pjax.options.selectors[1], ".js-Pjax");
t.equal(typeof pjax.options.switches, "object");
t.equal(Object.keys(pjax.options.switches).length, 2); // head and body
t.equal(typeof pjax.options.switchesOptions, "object");
t.equal(Object.keys(pjax.options.switchesOptions).length, 0);
t.equal(pjax.options.history, true);
t.equal(typeof pjax.options.analytics, "function");
t.equal(pjax.options.scrollTo, 0);
t.equal(pjax.options.scrollRestoration, true);
t.equal(pjax.options.cacheBust, true);
t.equal(pjax.options.debug, false);
t.equal(pjax.options.currentUrlFullReload, false);
t.end();
});
// verify analytics always ends up as a function even when passed not a function
t.test("- analytics is a function", function(t) {
var pjax = {};
pjax.options = parseOptions({ analytics: "some string" });
t.deepEqual(typeof pjax.options.analytics, "function");
t.end();
});
// verify that the value false for scrollTo is not squashed
t.test("- scrollTo remains false", function(t) {
var pjax = {};
pjax.options = parseOptions({ scrollTo: false });
t.deepEqual(pjax.options.scrollTo, false);
t.end();
});
t.end();
});

View File

@@ -1,95 +1,231 @@
var tape = require("tape")
var tape = require("tape");
var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger")
var attachForm = require("../../../lib/proto/attach-form")
var on = require("../../../lib/events/on");
var trigger = require("../../../lib/events/trigger");
var attachForm = require("../../../lib/proto/attach-form");
var form = document.createElement("form")
var attr = "data-pjax-click-state"
var preventDefault = function(e) { e.preventDefault() }
var attr = "data-pjax-state";
tape("test attach form prototype method", function(t) {
t.plan(7)
var form = document.createElement("form");
var loadUrlCalled = false;
attachForm.call({
options: {},
reload: function() {
t.equal(form.getAttribute(attr), "reload", "triggering a simple reload will just submit the form")
attachForm.call(
{
options: {
currentUrlFullReload: true
},
loadUrl: function() {
t.equal(form.getAttribute(attr), "submit", "triggering a post to the next page")
loadUrlCalled = true;
}
}, form)
},
form
);
var internalUri = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
var internalUri =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
window.location.search;
form.action = "http://external.com/"
trigger(form, "submit")
t.equal(form.getAttribute(attr), "external", "external url stop behavior")
form.action = "http://external.com/";
trigger(form, "submit");
t.equal(form.getAttribute(attr), "external", "external url stop behavior");
form.action = internalUri + "#anchor"
trigger(form, "submit")
t.equal(form.getAttribute(attr), "anchor-present", "internal anchor stop behavior")
form.action = internalUri + "#anchor";
trigger(form, "submit");
t.equal(form.getAttribute(attr), "anchor", "internal anchor stop behavior");
window.location.hash = "#anchor"
form.action = internalUri + "#another-anchor"
trigger(form, "submit")
t.notEqual(form.getAttribute(attr), "anchor", "differents anchors stop behavior")
window.location.hash = ""
window.location.hash = "#anchor";
form.action = internalUri + "#another-anchor";
trigger(form, "submit");
t.equal(form.getAttribute(attr), "anchor", "different anchors stop behavior");
window.location.hash = "";
form.action = internalUri + "#"
trigger(form, "submit")
t.equal(form.getAttribute(attr), "anchor-empty", "empty anchor stop behavior")
form.action = internalUri + "#";
trigger(form, "submit");
t.equal(
form.getAttribute(attr),
"anchor-empty",
"empty anchor stop behavior"
);
form.action = internalUri
trigger(form, "submit")
// see reload defined above
form.action = window.location.href;
trigger(form, "submit");
t.equal(
form.getAttribute(attr),
"reload",
"submitting when currentUrlFullReload is true will submit normally, without XHR"
);
t.equal(loadUrlCalled, false, "loadUrl() not called");
form.action = window.location.protocol + "//" + window.location.host + "/internal"
form.method = "POST"
trigger(form, "submit")
// see post defined above
form.action =
window.location.protocol + "//" + window.location.host + "/internal";
form.method = "POST";
trigger(form, "submit");
t.equal(
form.getAttribute(attr),
"submit",
"triggering a POST request to the next page"
);
t.equal(loadUrlCalled, true, "loadUrl() called correctly");
form.action = window.location.protocol + "//" + window.location.host + "/internal"
form.method = "GET"
trigger(form, "submit")
// see post defined above
loadUrlCalled = false;
form.setAttribute(attr, "");
form.action =
window.location.protocol + "//" + window.location.host + "/internal";
form.method = "GET";
trigger(form, "submit");
t.equal(
form.getAttribute(attr),
"submit",
"triggering a GET request to the next page"
);
t.equal(loadUrlCalled, true, "loadUrl() called correctly");
t.end()
})
t.end();
});
tape("test attach form preventDefaulted events", function(t) {
var callbacked = false
var form = document.createElement("form")
var loadUrlCalled = false;
var form = document.createElement("form");
attachForm.call({
// This needs to be before the call to attachForm()
on(form, "submit", function(event) {
event.preventDefault();
});
attachForm.call(
{
options: {},
loadUrl: function() {
callbacked = true
loadUrlCalled = true;
}
}, form)
},
form
);
form.action = "#"
on(form, "submit", preventDefault)
trigger(form, "submit")
t.equal(callbacked, false, "events that are preventDefaulted should not fire callback")
form.action = "#";
trigger(form, "submit");
t.equal(
loadUrlCalled,
false,
"events that are preventDefaulted should not fire callback"
);
t.end()
})
t.end();
});
tape("test options are not modified by attachForm", function(t) {
var form = document.createElement("form")
var options = {foo: "bar"}
var loadUrl = function() {}
var form = document.createElement("form");
var options = { foo: "bar" };
var loadUrl = function() {};
attachForm.call({options: options, loadUrl: loadUrl}, form)
attachForm.call({ options: options, loadUrl: loadUrl }, form);
form.action = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
form.method = "GET"
trigger(form, "submit")
form.action =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
window.location.search;
form.method = "GET";
trigger(form, "submit");
t.equal(1, Object.keys(options).length, "options object that is passed in should not be modified")
t.equal("bar", options.foo, "options object that is passed in should not be modified")
t.equal(
1,
Object.keys(options).length,
"options object that is passed in should not be modified"
);
t.equal(
"bar",
options.foo,
"options object that is passed in should not be modified"
);
t.end()
})
t.end();
});
tape("test form elements parsed correctly", function(t) {
t.plan(1);
var form = document.createElement("form");
var input = document.createElement("input");
input.name = "input";
input.value = "value";
form.appendChild(input);
var params = [
{
name: "input",
value: "value"
}
];
var pjax = {
options: {},
loadUrl: function(href, options) {
t.same(
options.requestOptions.requestParams,
params,
"form elements parsed correctly"
);
}
};
attachForm.call(pjax, form);
form.action =
window.location.protocol + "//" + window.location.host + "/internal";
trigger(form, "submit");
// see loadUrl defined above
t.end();
});
tape('test form.enctype="multipart/form-data"', function(t) {
t.plan(4);
var form = document.createElement("form");
form.enctype = "multipart/form-data";
var input = document.createElement("input");
input.name = "input";
input.value = "value";
form.appendChild(input);
var pjax = {
options: {},
loadUrl: function(href, options) {
t.equals(
options.requestOptions.requestParams,
undefined,
"form elements not parsed manually"
);
t.true(
options.requestOptions.formData instanceof FormData,
"requestOptions.formData is a FormData"
);
t.equals(
Array.from(options.requestOptions.formData.entries()).length,
1,
"correct number of FormData elements"
);
t.equals(
options.requestOptions.formData.get("input"),
"value",
"FormData element value set correctly"
);
}
};
attachForm.call(pjax, form);
form.action =
window.location.protocol + "//" + window.location.host + "/internal";
trigger(form, "submit");
// see loadUrl defined above
t.end();
});

View File

@@ -1,94 +1,190 @@
var tape = require("tape")
var tape = require("tape");
var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger")
var attachLink = require("../../../lib/proto/attach-link")
var on = require("../../../lib/events/on");
var trigger = require("../../../lib/events/trigger");
var attachLink = require("../../../lib/proto/attach-link");
var a = document.createElement("a")
var attr = "data-pjax-click-state"
var preventDefault = function(e) { e.preventDefault() }
var attr = "data-pjax-state";
tape("test attach link prototype method", function(t) {
t.plan(7)
var a = document.createElement("a");
var loadUrlCalled = false;
attachLink.call({
attachLink.call(
{
options: {},
reload: function() {
t.equal(a.getAttribute(attr), "reload", "triggering exact same url reload the page")
},
loadUrl: function() {
t.equal(a.getAttribute(attr), "load", "triggering a internal link actually load the page")
loadUrlCalled = true;
}
}, a)
},
a
);
var internalUri = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
var internalUri =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
window.location.search;
a.href = internalUri
on(a, "click", preventDefault) // to avoid link to be open (break testing env)
trigger(a, "click", {metaKey: true})
t.equal(a.getAttribute(attr), "modifier", "event key modifier stop behavior")
a.href = internalUri;
trigger(a, "click", { metaKey: true });
t.equal(a.getAttribute(attr), "modifier", "event key modifier stop behavior");
a.href = "http://external.com/"
trigger(a, "click")
t.equal(a.getAttribute(attr), "external", "external url stop behavior")
a.href = "http://external.com/";
trigger(a, "click");
t.equal(a.getAttribute(attr), "external", "external url stop behavior");
a.href = internalUri + "#anchor"
trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor-present", "internal anchor stop behavior")
window.location.hash = "#anchor";
a.href = internalUri + "#anchor";
trigger(a, "click");
t.equal(a.getAttribute(attr), "anchor", "internal anchor stop behavior");
window.location.hash = "#anchor"
a.href = internalUri + "#another-anchor"
trigger(a, "click")
t.notEqual(a.getAttribute(attr), "anchor", "differents anchors stop behavior")
window.location.hash = ""
a.href = internalUri + "#another-anchor";
trigger(a, "click");
t.equal(a.getAttribute(attr), "anchor", "different anchors stop behavior");
window.location.hash = "";
a.href = internalUri + "#"
trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor-empty", "empty anchor stop behavior")
a.href = internalUri + "#";
trigger(a, "click");
t.equal(a.getAttribute(attr), "anchor-empty", "empty anchor stop behavior");
a.href = internalUri
trigger(a, "click")
// see reload defined above
a.href = window.location.protocol + "//" + window.location.host + "/internal";
trigger(a, "click");
t.equals(
a.getAttribute(attr),
"load",
"triggering an internal link sets the state attribute to 'load'"
);
t.equals(
loadUrlCalled,
true,
"triggering an internal link actually loads the page"
);
a.href = window.location.protocol + "//" + window.location.host + "/internal"
trigger(a, "click")
// see loadUrl defined above
t.end()
})
t.end();
});
tape("test attach link preventDefaulted events", function(t) {
var callbacked = false
var a = document.createElement("a")
var loadUrlCalled = false;
var a = document.createElement("a");
attachLink.call({
// This needs to be before the call to attachLink()
on(a, "click", function(event) {
event.preventDefault();
});
attachLink.call(
{
options: {},
loadUrl: function() {
callbacked = true
loadUrlCalled = true;
}
}, a)
},
a
);
a.href = "#"
on(a, "click", preventDefault)
trigger(a, "click")
t.equal(callbacked, false, "events that are preventDefaulted should not fire callback")
a.href = "#";
trigger(a, "click");
t.equal(
loadUrlCalled,
false,
"events that are preventDefaulted should not fire callback"
);
t.end()
})
t.end();
});
tape("test options are not modified by attachLink", function(t) {
var a = document.createElement("a")
var options = {foo: "bar"}
var loadUrl = function() {}
var a = document.createElement("a");
var options = { foo: "bar" };
var loadUrl = function() {};
attachLink.call({options: options, loadUrl: loadUrl}, a)
attachLink.call({ options: options, loadUrl: loadUrl }, a);
a.href = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
a.href =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
window.location.search;
trigger(a, "click")
trigger(a, "click");
t.equal(1, Object.keys(options).length, "options object that is passed in should not be modified")
t.equal("bar", options.foo, "options object that is passed in should not be modified")
t.equal(
1,
Object.keys(options).length,
"options object that is passed in should not be modified"
);
t.equal(
"bar",
options.foo,
"options object that is passed in should not be modified"
);
t.end()
})
t.end();
});
tape("test link triggered by keyboard", function(t) {
var a = document.createElement("a");
var pjax = {
options: {},
loadUrl: function() {
t.equal(
a.getAttribute(attr),
"load",
"triggering a internal link actually loads the page"
);
}
};
t.plan(3);
attachLink.call(pjax, a);
a.href = window.location.protocol + "//" + window.location.host + "/internal";
trigger(a, "keyup", { keyCode: 14 });
t.equal(
a.getAttribute(attr),
"",
"keycode other than 13 doesn't trigger anything"
);
trigger(a, "keyup", { keyCode: 13, metaKey: true });
t.equal(a.getAttribute(attr), "modifier", "event key modifier stop behavior");
trigger(a, "keyup", { keyCode: 13 });
// see loadUrl defined above
t.end();
});
tape(
"test link with the same URL as the current one, when currentUrlFullReload set to true",
function(t) {
var a = document.createElement("a");
var pjax = {
options: {
currentUrlFullReload: true
},
reload: function() {
t.pass("this.reload() was called correctly");
},
loadUrl: function() {
t.fail("loadUrl() was called wrongly");
}
};
t.plan(2);
attachLink.call(pjax, a);
a.href = window.location.href;
trigger(a, "click");
t.equal(a.getAttribute(attr), "reload", "reload stop behavior");
t.end();
}
);

View File

@@ -0,0 +1,293 @@
var tape = require("tape");
var handleResponse = require("../../../lib/proto/handle-response");
var noop = require("../../../lib/util/noop");
var loadContent = require("../../../index").prototype.loadContent;
var href = "https://example.org/";
var storeEventHandler;
var pjaxErrorEventTriggerTest;
tape("test events triggered when handleResponse(false) is called", function(t) {
t.plan(3);
var pjax = {
options: {
test: 1
}
};
var events = [];
storeEventHandler = function(e) {
events.push(e.type);
t.equal(e.test, 1);
};
document.addEventListener("pjax:complete", storeEventHandler);
document.addEventListener("pjax:error", storeEventHandler);
handleResponse.bind(pjax)(false, null);
t.same(
events,
["pjax:complete", "pjax:error"],
"calling handleResponse(false) triggers 'pjax:complete' and 'pjax:error'"
);
document.removeEventListener("pjax:complete", storeEventHandler);
document.removeEventListener("pjax:error", storeEventHandler);
t.end();
});
tape("test when handleResponse() is called normally", function(t) {
var pjax = {
options: {
test: 1
},
loadContent: noop,
state: {}
};
var request = {
getResponseHeader: noop
};
handleResponse.bind(pjax)("", request, "href");
delete window.history.state.uid;
t.same(
window.history.state,
{
url: href,
title: "",
scrollPos: [0, 0]
},
"window.history.state is set correctly"
);
t.equals(pjax.state.href, "href", "this.state.href is set correctly");
t.equals(
Object.keys(pjax.state.options).length,
2,
"this.state.options is set correctly"
);
t.same(
pjax.state.options.request,
request,
"this.state.options is set correctly"
);
t.equals(pjax.state.options.test, 1, "this.state.options is set correctly");
t.end();
});
tape(
"test when handleResponse() is called normally with request.responseURL",
function(t) {
var pjax = {
options: {},
loadContent: noop,
state: {}
};
var request = {
responseURL: href + "1",
getResponseHeader: noop
};
handleResponse.bind(pjax)("", request, "");
t.equals(
pjax.state.href,
request.responseURL,
"this.state.href is set correctly"
);
t.end();
}
);
tape("test when handleResponse() is called normally with X-PJAX-URL", function(
t
) {
var pjax = {
options: {},
loadContent: noop,
state: {}
};
var request = {
getResponseHeader: function(header) {
if (header === "X-PJAX-URL") {
return href + "2";
}
}
};
handleResponse.bind(pjax)("", request, "");
t.equals(pjax.state.href, href + "2", "this.state.href is set correctly");
t.end();
});
tape(
"test when handleResponse() is called normally with X-XHR-Redirected-To",
function(t) {
var pjax = {
options: {},
loadContent: noop,
state: {}
};
var request = {
getResponseHeader: function(header) {
if (header === "X-XHR-Redirected-To") {
return href + "3";
}
}
};
handleResponse.bind(pjax)("", request, "");
t.equals(pjax.state.href, href + "3", "this.state.href is set correctly");
t.end();
}
);
tape("test when handleResponse() is called normally with a hash", function(t) {
var pjax = {
options: {},
loadContent: noop,
state: {}
};
var request = {
responseURL: href + "2",
getResponseHeader: noop
};
handleResponse.bind(pjax)("", request, href + "1#test");
t.equals(
pjax.state.href,
href + "2#test",
"this.state.href is set correctly"
);
t.end();
});
tape("test try...catch for loadContent() when options.debug is true", function(
t
) {
t.plan(2);
var pjax = {
options: {},
loadContent: noop,
state: {}
};
var request = {
getResponseHeader: noop
};
pjax.loadContent = function() {
throw new Error();
};
pjax.options.debug = true;
document.removeEventListener("pjax:error", storeEventHandler);
pjaxErrorEventTriggerTest = function() {
t.pass("pjax:error event triggered");
};
document.addEventListener("pjax:error", pjaxErrorEventTriggerTest);
t.throws(
function() {
handleResponse.bind(pjax)("", request, "");
},
Error,
"error is rethrown"
);
t.end();
});
tape("test try...catch for loadContent()", function(t) {
t.plan(2);
var pjax = {
options: {},
loadContent: noop,
state: {}
};
var request = {
getResponseHeader: noop
};
pjax.loadContent = function() {
throw new Error();
};
pjax.latestChance = function() {
return true;
};
pjax.options.debug = false;
document.removeEventListener("pjax:error", pjaxErrorEventTriggerTest);
t.doesNotThrow(
function() {
t.equals(
handleResponse.bind(pjax)("", request, ""),
true,
"this.latestChance() is called"
);
},
Error,
"error is not thrown"
);
t.end();
});
tape(
"test events triggered when loadContent() is called with a non-string html argument",
function(t) {
t.plan(3);
var options = {
test: 1
};
var events = [];
storeEventHandler = function(e) {
events.push(e.type);
t.equal(e.test, 1);
};
document.addEventListener("pjax:complete", storeEventHandler);
document.addEventListener("pjax:error", storeEventHandler);
loadContent(null, options);
t.same(
events,
["pjax:complete", "pjax:error"],
"calling loadContent() with a non-string html argument triggers 'pjax:complete' and 'pjax:error'"
);
document.removeEventListener("pjax:complete", storeEventHandler);
document.removeEventListener("pjax:error", storeEventHandler);
t.end();
}
);

View File

@@ -1,21 +1,31 @@
var tape = require("tape")
var tape = require("tape");
var parseElement = require("../../../lib/proto/parse-element")
var protoMock = {
attachLink: function() { return true },
attachForm: function() { return true }
}
var parseElement = require("../../../lib/proto/parse-element");
var pjax = {
attachLink: function() {
return true;
},
attachForm: function() {
return true;
}
};
tape("test parse element prototype method", function(t) {
t.doesNotThrow(function() {
var a = document.createElement("a")
parseElement.call(protoMock, a)
}, "<a> element can be parsed")
var a = document.createElement("a");
parseElement.call(pjax, a);
}, "<a> element can be parsed");
t.doesNotThrow(function() {
var form = document.createElement("form")
parseElement.call(protoMock, form)
}, "<form> element can be parsed")
var form = document.createElement("form");
parseElement.call(pjax, form);
}, "<form> element can be parsed");
t.end()
})
t.throws(function() {
var el = document.createElement("div");
parseElement.call(pjax, el);
}, "<div> element cannot be parsed");
t.end();
});

View File

@@ -1,49 +0,0 @@
var tape = require("tape")
var parseOptions = require("../../../lib/proto/parse-options.js")
tape("test parse initalization options function", function(t) {
t.test("- default options", function(t) {
var pjax = {}
parseOptions.call(pjax, {})
t.equal(pjax.options.elements, "a[href], form[action]")
t.equal(pjax.options.selectors.length, 2, "selectors length")
t.equal(pjax.options.selectors[0], "title")
t.equal(pjax.options.selectors[1], ".js-Pjax")
t.equal(typeof pjax.options.switches, "object")
t.equal(Object.keys(pjax.options.switches).length, 2)// head and body
t.equal(typeof pjax.options.switchesOptions, "object")
t.equal(Object.keys(pjax.options.switchesOptions).length, 0)
t.equal(pjax.options.history, true)
t.equal(typeof pjax.options.analytics, "function")
t.equal(pjax.options.scrollTo, 0)
t.equal(pjax.options.scrollRestoration, true)
t.equal(pjax.options.cacheBust, true)
t.equal(pjax.options.debug, false)
t.equal(pjax.options.currentUrlFullReload, false)
t.end()
})
// verify analytics always ends up as a function even when passed not a function
t.test("- analytics is a function", function(t) {
var pjax = {}
parseOptions.call(pjax, {analytics: "some string"})
t.deepEqual(typeof pjax.options.analytics, "function")
t.end()
})
// verify that the value false for scrollTo is not squashed
t.test("- scrollTo remains false", function(t) {
var pjax = {}
parseOptions.call(pjax, {scrollTo: false})
t.deepEqual(pjax.options.scrollTo, false)
t.end()
})
t.end()
})

View File

@@ -1,50 +1,165 @@
var tape = require("tape")
var tape = require("tape");
var sendRequest = require("../../lib/send-request.js")
var sendRequest = require("../../lib/send-request.js");
// Polyfill responseURL property into XMLHttpRequest if it doesn't exist,
// just for the purposes of this test
// This polyfill is not complete; it won't show the updated location if a
// redirection occurred, but it's fine for our purposes.
if (!("responseURL" in XMLHttpRequest.prototype)) {
var nativeOpen = XMLHttpRequest.prototype.open
var nativeOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this.responseURL = url
return nativeOpen.apply(this, arguments)
}
this.responseURL = url;
return nativeOpen.apply(this, arguments);
};
}
tape("test xhr request", function(t) {
var url = "https://httpbin.org/get"
var url = "https://httpbin.org/get";
t.test("- request is made, gets a result, and is cache-busted", function(t) {
var requestCacheBust = sendRequest.bind({
options: {
cacheBust: true
}
})
var r = requestCacheBust(url, {}, function(result) {
t.equal(r.responseURL.indexOf("?"), url.length, "XHR URL is cache-busted when configured to be")
var r = sendRequest(url, { cacheBust: true }, function(result) {
t.equal(
r.responseURL.indexOf("?"),
url.length,
"XHR URL is cache-busted when configured to be"
);
try {
result = JSON.parse(result)
result = JSON.parse(result);
} catch (e) {
t.fail("xhr doesn't get a JSON response");
}
catch (e) {
t.fail("xhr doesn't get a JSON response")
t.same(typeof result, "object", "xhr request get a result");
t.end();
});
});
t.test("- request is not cache-busted when configured not to be", function(
t
) {
var r = sendRequest(url, {}, function() {
t.equal(r.responseURL, url, "XHR URL is left untouched");
t.end();
});
});
t.end();
});
tape("request headers are sent properly", function(t) {
var url = "https://httpbin.org/headers";
var options = {
selectors: ["div.pjax", "div.container"]
};
sendRequest(url, options, function(responseText) {
var headers = JSON.parse(responseText).headers;
t.equals(
headers["X-Requested-With"],
"XMLHttpRequest",
"X-Requested-With header is set correctly"
);
// Httpbin.org changes the case to 'X-Pjax'
t.equals(headers["X-Pjax"], "true", "X-PJAX header is set correctly");
t.equals(
headers["X-Pjax-Selectors"],
'["div.pjax","div.container"]',
"X-PJAX-Selectors header is set correctly"
);
t.end();
});
});
tape("HTTP status codes other than 200 are handled properly", function(t) {
var url = "https://httpbin.org/status/400";
sendRequest(url, {}, function(responseText, request) {
t.equals(responseText, null, "responseText is null");
t.equals(request.status, 400, "HTTP status code is correct");
t.end();
});
});
tape.skip("XHR error is handled properly", function(t) {
var url = "https://encrypted.google.com/foobar";
sendRequest(url, {}, function(responseText) {
t.equals(responseText, null, "responseText is null");
t.end();
});
});
tape("POST body data is sent properly", function(t) {
var url = "https://httpbin.org/post";
var params = [
{
name: "test",
value: "1"
}
t.same(typeof result, "object", "xhr request get a result")
t.end()
})
})
t.test("- request is not cache-busted when configured not to be", function(t) {
var requestNoCacheBust = sendRequest.bind({
options: {
cacheBust: false
];
var options = {
requestOptions: {
requestMethod: "POST",
requestParams: params
}
})
var r = requestNoCacheBust(url, {}, function() {
t.equal(r.responseURL, url, "XHR URL is left untouched")
t.end()
})
})
t.end()
})
};
sendRequest(url, options, function(responseText) {
var response = JSON.parse(responseText);
t.same(
response.form[params[0].name],
params[0].value,
"requestParams were sent properly"
);
t.equals(
response.headers["Content-Type"],
"application/x-www-form-urlencoded",
"Content-Type header was set properly"
);
t.end();
});
});
tape("GET query data is sent properly", function(t) {
var url = "https://httpbin.org/get";
var params = [
{
name: "test",
value: "1"
}
];
var options = {
requestOptions: {
requestParams: params
}
};
sendRequest(url, options, function(responseText) {
var response = JSON.parse(responseText);
t.same(
response.args[params[0].name],
params[0].value,
"requestParams were sent properly"
);
t.end();
});
});
tape("XHR timeout is handled properly", function(t) {
var url = "https://httpbin.org/delay/5";
var options = {
timeout: 1000
};
sendRequest(url, options, function(responseText) {
t.equals(responseText, null, "responseText is null");
t.end();
});
});

View File

@@ -1,31 +1,33 @@
var tape = require("tape")
var tape = require("tape");
var switchesSelectors = require("../../lib/switches-selectors.js")
var switchesSelectors = require("../../lib/switches-selectors.js");
var noop = require("../../lib/util/noop");
var pjax = {
onSwitch: function() {
console.log("Switched");
},
state: {},
log: noop
};
// @author darylteo
tape("test switchesSelectors", function(t) {
// switchesSelectors relies on a higher level function callback
// should really be passed in instead so I'll leave it here as a TODO:
var pjax = {
onSwitch: function() {
console.log("Switched")
},
state: {}
}
var tmpEl = document.implementation.createHTMLDocument()
var tmpEl = document.implementation.createHTMLDocument();
// a div container is used because swapping the containers
// will generate a new element, so things get weird
// using "body" generates a lot of testling cruft that I don't
// want so let's avoid that
var container = document.createElement("div")
container.innerHTML = "<p>Original Text</p><span>No Change</span>"
document.body.appendChild(container)
var container = document.createElement("div");
container.innerHTML = "<p>Original Text</p><span>No Change</span>";
document.body.appendChild(container);
var container2 = tmpEl.createElement("div")
container2.innerHTML = "<p>New Text</p><span>New Span</span>"
tmpEl.body.appendChild(container2)
var container2 = tmpEl.createElement("div");
container2.innerHTML = "<p>New Text</p><span>New Span</span>";
tmpEl.body.appendChild(container2);
switchesSelectors.bind(pjax)(
{}, // switches
@@ -34,9 +36,49 @@ tape("test switchesSelectors", function(t) {
tmpEl, // fromEl
document, // toEl,
{} // options
)
);
t.equals(container.innerHTML, "<p>New Text</p><span>No Change</span>", "Elements correctly switched")
t.equals(
container.innerHTML,
"<p>New Text</p><span>No Change</span>",
"Elements correctly switched"
);
t.end()
})
t.end();
});
tape("test switchesSelectors when number of elements don't match", function(t) {
var newTempDoc = document.implementation.createHTMLDocument();
var originalTempDoc = document.implementation.createHTMLDocument();
// a div container is used because swapping the containers
// will generate a new element, so things get weird
// using "body" generates a lot of testling cruft that I don't
// want so let's avoid that
var container = originalTempDoc.createElement("div");
container.innerHTML = "<p>Original text</p><span>No change</span>";
originalTempDoc.body.appendChild(container);
var container2 = newTempDoc.createElement("div");
container2.innerHTML =
"<p>New text</p><p>More new text</p><span>New span</span>";
newTempDoc.body.appendChild(container2);
var switchSelectorsFn = switchesSelectors.bind(
pjax,
{}, // switches
{}, // switchesOptions
["p"], // selectors,
newTempDoc, // fromEl
originalTempDoc, // toEl,
{} // options
);
t.throws(
switchSelectorsFn,
null,
"error was thrown properly since number of elements don't match"
);
t.end();
});

94
tests/lib/switches.js Normal file
View File

@@ -0,0 +1,94 @@
var tape = require("tape");
var switches = require("../../lib/switches");
var noop = require("../../lib/util/noop");
tape("test outerHTML switch", function(t) {
var outerHTML = switches.outerHTML;
var doc = document.implementation.createHTMLDocument();
var container = doc.createElement("div");
container.innerHTML = "<p id='p'>Original Text</p>";
doc.body.appendChild(container);
var p = doc.createElement("p");
p.innerHTML = "New Text";
outerHTML.bind({
onSwitch: noop
})(doc.querySelector("p"), p);
t.equals(
doc.querySelector("p").innerHTML,
"New Text",
"Elements correctly switched"
);
t.notEquals(
doc.querySelector("p").id,
"p",
"other attributes overwritten correctly"
);
t.end();
});
tape("test innerHTML switch", function(t) {
var innerHTML = switches.innerHTML;
var doc = document.implementation.createHTMLDocument();
var container = doc.createElement("div");
container.innerHTML = "<p id='p'>Original Text</p>";
doc.body.appendChild(container);
var p = doc.createElement("p");
p.innerHTML = "New Text";
p.className = "p";
innerHTML.bind({
onSwitch: noop
})(doc.querySelector("p"), p);
t.equals(
doc.querySelector("p").innerHTML,
"New Text",
"Elements correctly switched"
);
t.equals(doc.querySelector("p").className, "p", "classname set correctly");
t.equals(doc.querySelector("p").id, "p", "other attributes set correctly");
p.removeAttribute("class");
innerHTML.bind({
onSwitch: noop
})(doc.querySelector("p"), p);
t.equals(doc.querySelector("p").className, "", "classname set correctly");
t.end();
});
tape("test replaceNode switch", function(t) {
var replaceNode = switches.replaceNode;
var doc = document.implementation.createHTMLDocument();
var container = doc.createElement("div");
container.innerHTML = "<p>Original Text</p>";
doc.body.appendChild(container);
var p = doc.createElement("p");
p.innerHTML = "New Text";
replaceNode.bind({
onSwitch: noop
})(doc.querySelector("p"), p);
t.equals(
doc.querySelector("div").innerHTML,
"<p>New Text</p>",
"Elements correctly switched"
);
t.end();
});

View File

@@ -1,12 +1,12 @@
var tape = require("tape")
var tape = require("tape");
var uniqueid = require("../../lib/uniqueid.js")
var uniqueid = require("../../lib/uniqueid.js");
tape("test uniqueid", function(t) {
var a = uniqueid()
var b = uniqueid()
var a = uniqueid();
var b = uniqueid();
t.notEqual(a,b,"Two calls to uniqueid produce different values")
t.notEqual(a, b, "Two calls to uniqueid produce different values");
t.end()
})
t.end();
});

21
tests/lib/util/clone.js Normal file
View File

@@ -0,0 +1,21 @@
var tape = require("tape");
var clone = require("../../../lib/util/clone");
tape("test clone method", function(t) {
var obj = { one: 1, two: 2 };
var cloned = clone(obj);
t.notEqual(obj, cloned, "cloned object isn't the original object");
t.same(obj, cloned, "cloned object has the same values as original object");
cloned.three = 3;
t.notSame(
obj,
cloned,
"modified cloned object doesn't have the same values as original object"
);
t.end();
});

View File

@@ -1,16 +1,25 @@
var tape = require("tape")
var tape = require("tape");
var contains = require("../../../lib/util/contains.js")
var contains = require("../../../lib/util/contains.js");
tape("test contains function", function(t) {
var tempDoc = document.implementation.createHTMLDocument()
tempDoc.body.innerHTML = "<div><p id='el' class='js-Pjax'></p></div><span></span>"
var selectors = ["div"]
var el = tempDoc.body.querySelector("#el")
t.equal(contains(tempDoc, selectors, el), true, "contains() returns true when a selector contains the element")
var tempDoc = document.implementation.createHTMLDocument();
tempDoc.body.innerHTML =
"<div><p id='el' class='js-Pjax'></p></div><span></span>";
var selectors = ["div"];
var el = tempDoc.body.querySelector("#el");
t.equal(
contains(tempDoc, selectors, el),
true,
"contains() returns true when a selector contains the element"
);
selectors = ["span"]
t.equal(contains(tempDoc, selectors, el), false, "contains() returns false when the selectors do not contain the element")
selectors = ["span"];
t.equal(
contains(tempDoc, selectors, el),
false,
"contains() returns false when the selectors do not contain the element"
);
t.end()
})
t.end();
});

25
tests/lib/util/extend.js Normal file
View File

@@ -0,0 +1,25 @@
var tape = require("tape");
var extend = require("../../../lib/util/extend");
tape("test extend method", function(t) {
var obj = { one: 1, two: 2 };
var extended = extend({}, obj, { two: "two", three: 3 });
t.notEqual(obj, extended, "extended object isn't the original object");
t.notSame(
obj,
extended,
"extended object doesn't have the same values as original object"
);
t.notSame(
obj.two,
extended.two,
"extended object value overwrites value from original object"
);
extended = extend(null);
t.equals(extended, null, "passing null returns null");
t.end();
});

9
tests/lib/util/noop.js Normal file
View File

@@ -0,0 +1,9 @@
var tape = require("tape");
var noop = require("../../../lib/util/noop");
tape("test noop function", function(t) {
t.equal(typeof noop, "function", "noop is a function");
t.equal(noop(), undefined, "noop() returns nothing");
t.end();
});

View File

@@ -0,0 +1,33 @@
var tape = require("tape");
var updateQueryString = require("../../../lib/util/update-query-string");
tape("test update query string method", function(t) {
var url = "http://example.com";
var updatedUrl = updateQueryString(url, "foo", "bar");
t.notEqual(url, updatedUrl, "update query string modifies URL");
t.equal(
updatedUrl,
url + "?foo=bar",
"update query string creates new query string when no query string params are set"
);
updatedUrl = updateQueryString(updatedUrl, "foo", "baz");
t.equal(
updatedUrl,
url + "?foo=baz",
"update query string updates existing query string param"
);
updatedUrl = updateQueryString(updatedUrl, "bar", "");
t.equal(
updatedUrl,
url + "?foo=baz&bar=",
"update query string appends to existing query string"
);
t.end();
});

View File

@@ -1,6 +1,6 @@
var jsdomOptions = {
url: "https://example.org/",
runScripts: "dangerously"
}
};
require("jsdom-global")("", jsdomOptions)
require("jsdom-global")("", jsdomOptions);

41
tests/test.ts Normal file
View File

@@ -0,0 +1,41 @@
import Pjax = require("../index");
let options: Pjax.IOptions = {
elements: "a.pjax, form.pjax",
selectors: ["div.pjax"],
switches: {
"a.pjax": (oldEl, newEl) => {
oldEl.parentNode.replaceChild(newEl, oldEl);
this.onSwitch();
},
"form.pjax": Pjax.switches.innerHTML
},
switchesOptions: {},
history: true,
analytics: false,
scrollTo: 1,
scrollRestoration: false,
cacheBust: false,
debug: true,
timeout: 60000,
currentUrlFullReload: true
};
options.analytics = () => {};
options.scrollTo = [1, 1];
options.scrollTo = false;
if (Pjax.isSupported()) {
delete options.switchesOptions;
const pjax = new Pjax(options);
pjax.reload();
pjax.loadUrl("https://example.org", options);
pjax._handleResponse = pjax.handleResponse;
pjax.handleResponse = (requestText: string, request: XMLHttpRequest, href: string) => {
pjax.abortRequest(request);
return pjax._handleResponse(requestText, request, href);
}
}