Compare commits

...

18 Commits

Author SHA1 Message Date
Robin North
5bbeb31ca6 Switch to Yarn for dependency management 2018-05-28 20:44:15 +01: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
38 changed files with 5879 additions and 333 deletions

View File

@@ -1,3 +1,26 @@
# 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 # 0.2.5 - 2018-02-02
- Fixed: Async switch functions now work correctly, because the DOM is now parsed after all the switches finish. - Fixed: Async switch functions now work correctly, because the DOM is now parsed after all the switches finish.

133
README.md
View File

@@ -1,10 +1,10 @@
# Pjax # 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) > Easily enable fast AJAX navigation on any website (using pushState() + XHR)
Pjax is ~~a jQuery plugin~~ **a standalone JavaScript module** that uses Pjax is **a standalone JavaScript module** that uses
AJAX (XmlHttpRequest) and AJAX (XmlHttpRequest) and
[pushState()](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history) [pushState()](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history)
to deliver a fast browsing experience. to deliver a fast browsing experience.
@@ -15,9 +15,7 @@ especially for users with low bandwidth connection._
**No more full page reloads. No more multiple HTTP requests.** **No more full page reloads. No more multiple HTTP requests.**
## Demo _Pjax does not rely on other libraries, like jQuery or similar. It is written entirely in vanilla JS._
[You can see this running on my website](http://moox.io), with sexy CSS animations when switching pages.
## Installation ## Installation
@@ -35,10 +33,6 @@ especially for users with low bandwidth connection._
<script src="https://cdn.jsdelivr.net/npm/pjax@VERSION/pjax.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/pjax@VERSION/pjax.min.js"></script>
``` ```
## No dependencies
_Pjax does not rely on other libraries, like jQuery or similar. It is written entirely in vanilla JS._
## How Pjax works ## 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. 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.
@@ -103,7 +97,7 @@ So we want to update `[".my-Header", ".my-Content", ".my-Sidebar"]`, **without r
We do this by telling Pjax to listen on `a` tags and use CSS selectors defined above (without forgetting minimal meta): We do this by telling Pjax to listen on `a` tags and use CSS selectors defined above (without forgetting minimal meta):
``` javascript ``` javascript
new Pjax({ selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"] }) var pjax = new Pjax({ selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"] })
``` ```
Now, when someone in a Pjax-compatible browser clicks "blah", the content of all selectors will be replaced with the one found in the "blah" content. 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.
@@ -128,12 +122,16 @@ To see if Pjax is actually supported by your browser, use `Pjax.isSupported()`.
## Usage ## Usage
### `new Pjax()` ### Methods
Let's talk more about the most basic way to get started: #### `new Pjax()`
Let's talk more about the most basic way to get started.
When instantiating `Pjax`, you can pass options in to the constructor as an object:
```js ```js
new Pjax({ var pjax = new Pjax({
elements: "a", // default is "a[href], form[action]" elements: "a", // default is "a[href], form[action]"
selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"] selectors: ["title", ".my-Header", ".my-Content", ".my-Sidebar"]
}) })
@@ -149,7 +147,7 @@ In that case, you can do two different things:
```js ```js
// use case 1 // use case 1
new Pjax({ elements: "a.js-Pjax" }) var pjax = new Pjax({ elements: "a.js-Pjax" })
// use case 2 // use case 2
@@ -157,12 +155,52 @@ Pjax.prototype.getElements = function() {
return document.getElementsByClassName(".js-Pjax") 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 loading of a URL:
```js
var pjax = new Pjax()
// use case 1 (without options override)
pjax.loadUrl("/your-url")
// use case 2 (with options override)
pjax.loadUrl("/your-other-url", {timeout: 10})
```
#### `handleResponse(responseText, request, href)`
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()`.
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 JSON response, you could do the following:
```js
var pjax = new Pjax();
pjax._handleResponse = pjax.handleResponse;
pjax.handleResponse = function(responseText, request, href) {
if (request.responseText.match("<html")) {
pjax._handleResponse(responseText, request, href);
} else {
// handle response here
}
}
```
### Options
##### `elements` (String, default: `"a[href], form[action]"`) ##### `elements` (String, default: `"a[href], form[action]"`)
@@ -201,7 +239,7 @@ Keys should be one of the defined selectors.
Examples: Examples:
```js ```js
new Pjax({ var pjax = new Pjax({
selectors: ["title", ".Navbar", ".js-Pjax"], selectors: ["title", ".Navbar", ".js-Pjax"],
switches: { switches: {
// "title": Pjax.switches.outerHTML // default behavior // "title": Pjax.switches.outerHTML // default behavior
@@ -222,6 +260,7 @@ Callbacks are bound to Pjax instance itself to allow you to reuse it (ex: `this.
- `Pjax.switches.outerHTML`: default behavior, replace elements using outerHTML - `Pjax.switches.outerHTML`: default behavior, replace elements using outerHTML
- `Pjax.switches.innerHTML`: replace elements using innerHTML and copy className too - `Pjax.switches.innerHTML`: replace elements using innerHTML and copy className too
- `Pjax.switches.replaceNode`: replace elements using replaceChild
- `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.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)
###### Create a switch callback ###### Create a switch callback
@@ -247,7 +286,7 @@ This is very convenient when you use something like [Animate.css](https://github
with or without [WOW.js](https://github.com/matthieua/WOW). with or without [WOW.js](https://github.com/matthieua/WOW).
```js ```js
new Pjax({ var pjax = new Pjax({
selectors: ["title", ".js-Pjax"], selectors: ["title", ".js-Pjax"],
switches: { switches: {
".js-Pjax": Pjax.switches.sideBySide ".js-Pjax": Pjax.switches.sideBySide
@@ -373,7 +412,7 @@ However, there is almost no use case where you would want to do that.
Internally, this option is used when a `popstate` event triggers Pjax (to not `pushState()` again). 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 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). a pageview with Google Analytics (if it exists on the page).
@@ -381,9 +420,13 @@ It's called every time a page is switched, even for history navigation.
Set to `false` to disable this behavior. 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.
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.
##### `scrollRestoration` (Boolean, default: `true`) ##### `scrollRestoration` (Boolean, default: `true`)
@@ -402,10 +445,14 @@ Enables verbose mode. Useful to debug page layout differences.
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. When set to `false`, clicking on such a link will cause Pjax to load the
current page like any page.
If you want to add some custom behavior, add a click listener to the link, 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. and set `preventDefault` to true, to 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 be called yet.
Here is some sample code: Here is some sample code:
```js ```js
@@ -422,6 +469,8 @@ Here is some sample code:
} }
}) })
} }
var pjax = new Pjax()
``` ```
(Note that if `cacheBust` is set to true, the code that checks if the href (Note that if `cacheBust` is set to true, the code that checks if the href
@@ -441,7 +490,7 @@ All events are fired from the _document_, not the link that was clicked.
* `pjax:send` - Fired after the Pjax request begins. * `pjax:send` - Fired after the Pjax request begins.
* `pjax:complete` - Fired after the Pjax request finishes. * `pjax:complete` - Fired after the Pjax request finishes.
* `pjax:success` - Fired after the Pjax request succeeds. * `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/)) `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,6 +499,36 @@ document.addEventListener('pjax:send', topbar.show)
document.addEventListener('pjax:complete', topbar.hide) document.addEventListener('pjax:complete', topbar.hide)
``` ```
### 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. Use `JSON.parse()`
server-side to deserialize it back to an array.
##### 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
check 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.
#### Note about DOM ready state #### Note about 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. Most of the time, you will have code related to the current DOM that you only execute when the DOM is ready.
@@ -463,7 +542,7 @@ function whenDOMReady() {
whenDOMReady() whenDOMReady()
new Pjax() var pjax = new Pjax()
document.addEventListener("pjax:success", whenDOMReady) document.addEventListener("pjax:success", whenDOMReady)
``` ```
@@ -486,7 +565,7 @@ function whenContainerReady() {
} }
whenContainerReady() whenContainerReady()
new Pjax() var pjax = new Pjax()
document.addEventListener("pjax:success", whenContainerReady) document.addEventListener("pjax:success", whenContainerReady)
``` ```
@@ -566,7 +645,7 @@ Clone this repository and run `npm run example`, which will open the example app
* ⇄ Pull requests and ★ Stars are always welcome. * ⇄ Pull requests and ★ Stars are always welcome.
* For bugs and feature requests, please create an issue. * For bugs and feature requests, please create an issue.
* Pull requests must be accompanied by passing automated tests (`npm test`). * 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) ## [CHANGELOG](CHANGELOG.md)

View File

@@ -1,4 +1,31 @@
/* global Pjax */ /* global Pjax */
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) console.log("Document initialized:", window.location.href)
document.addEventListener("pjax:send", function() { document.addEventListener("pjax:send", function() {
@@ -15,13 +42,20 @@ document.addEventListener("pjax:error", function() {
document.addEventListener("pjax:success", function() { 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() { document.addEventListener("DOMContentLoaded", function() {
var pjax = new Pjax({ // Init Pjax instance
pjax = new Pjax({
elements: [".js-Pjax"], elements: [".js-Pjax"],
selectors: [".body"] selectors: [".body", "title"],
// currentUrlFullReload: true, cacheBust: true
}) })
console.log("Pjax initialized.", pjax) 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> <!doctype html>
<html> <html>
<head> <head>
<meta charset='utf-8'> <meta charset="utf-8">
<title>Hello</title> <title>Hello</title>
<script src='../pjax.js'></script> <script src="../pjax.js"></script>
<script src='example.js'></script> <script src="example.js"></script>
</head> </head>
<body> <body>
<div class='body'> <div class="body">
<h1>Index</h1> <h1>Index</h1>
Hello. 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. 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. 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> </div>
</body> </body>
</html> </html>

View File

@@ -1,15 +1,15 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset='utf-8'> <meta charset="utf-8">
<title>Hello</title> <title>Page 2</title>
<script src='../pjax.js'></script> <script src="../pjax.js"></script>
<script src='example.js'></script> <script src="example.js"></script>
</head> </head>
<body> <body>
<div class='body'> <div class="body">
<h1>Page 2</h1> <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> </div>
</body> </body>
</html> </html>

View File

@@ -1,15 +1,15 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset='utf-8'> <meta charset="utf-8">
<title>Hello</title> <title>Page 3</title>
<script src='../pjax.js'></script> <script src="../pjax.js"></script>
<script src='example.js'></script> <script src="example.js"></script>
</head> </head>
<body> <body>
<div class='body'> <div class="body">
<h1>Page 3</h1> <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> </div>
</body> </body>
</html> </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;

View File

@@ -1,13 +1,15 @@
var clone = require("./lib/clone.js")
var executeScripts = require("./lib/execute-scripts.js") var executeScripts = require("./lib/execute-scripts.js")
var forEachEls = require("./lib/foreach-els.js") var forEachEls = require("./lib/foreach-els.js")
var parseOptions = require("./lib/parse-options.js")
var switches = require("./lib/switches") var switches = require("./lib/switches")
var newUid = require("./lib/uniqueid.js") var newUid = require("./lib/uniqueid.js")
var on = require("./lib/events/on.js") var on = require("./lib/events/on.js")
var trigger = require("./lib/events/trigger.js") var trigger = require("./lib/events/trigger.js")
var clone = require("./lib/util/clone.js")
var contains = require("./lib/util/contains.js") var contains = require("./lib/util/contains.js")
var extend = require("./lib/util/extend.js")
var noop = require("./lib/util/noop") var noop = require("./lib/util/noop")
var Pjax = function(options) { var Pjax = function(options) {
@@ -17,8 +19,8 @@ var Pjax = function(options) {
options: null options: null
} }
var parseOptions = require("./lib/proto/parse-options.js")
parseOptions.call(this,options) this.options = parseOptions(options)
this.log("Pjax options", this.options) this.log("Pjax options", this.options)
if (this.options.scrollRestoration && "scrollRestoration" in history) { if (this.options.scrollRestoration && "scrollRestoration" in history) {
@@ -35,7 +37,6 @@ var Pjax = function(options) {
opt.url = st.state.url opt.url = st.state.url
opt.title = st.state.title opt.title = st.state.title
opt.history = false opt.history = false
opt.requestOptions = {}
opt.scrollPos = st.state.scrollPos opt.scrollPos = st.state.scrollPos
if (st.state.uid < this.lastUid) { if (st.state.uid < this.lastUid) {
opt.backward = true opt.backward = true
@@ -128,7 +129,7 @@ Pjax.prototype = {
this.log("load content", tmpEl.documentElement.attributes, tmpEl.documentElement.innerHTML.length) this.log("load content", tmpEl.documentElement.attributes, tmpEl.documentElement.innerHTML.length)
// Clear out any focused controls before inserting new page contents. // 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 { try {
document.activeElement.blur() document.activeElement.blur()
} catch (e) { } } catch (e) { }
@@ -141,7 +142,13 @@ Pjax.prototype = {
doRequest: require("./lib/send-request.js"), doRequest: require("./lib/send-request.js"),
handleResponse: require("./lib/proto/handle-response.js"),
loadUrl: function(href, options) { loadUrl: function(href, options) {
options = typeof options === "object" ?
extend({}, this.options, options) :
clone(this.options)
this.log("load href", href, options) this.log("load href", href, options)
// Abort any previous request // Abort any previous request
@@ -150,67 +157,7 @@ Pjax.prototype = {
trigger(document, "pjax:send", options) trigger(document, "pjax:send", options)
// Do the request // Do the request
options.requestOptions.timeout = this.options.timeout this.request = this.doRequest(href, options, this.handleResponse.bind(this))
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))
}, },
afterAllSwitches: function() { afterAllSwitches: function() {

View File

@@ -13,6 +13,7 @@ module.exports = function(el) {
script.type = "text/javascript" script.type = "text/javascript"
/* istanbul ignore if */
if (src !== "") { if (src !== "") {
script.src = src script.src = src
script.async = false // force synchronous loading of peripheral JS script.async = false // force synchronous loading of peripheral JS
@@ -23,6 +24,7 @@ module.exports = function(el) {
script.appendChild(document.createTextNode(code)) script.appendChild(document.createTextNode(code))
} }
catch (e) { catch (e) {
/* istanbul ignore next */
// old IEs have funky script nodes // old IEs have funky script nodes
script.text = code script.text = code
} }
@@ -31,7 +33,7 @@ module.exports = function(el) {
// execute // execute
parent.appendChild(script) parent.appendChild(script)
// avoid pollution only in head or body tags // avoid pollution only in head or body tags
if (["head", "body"].indexOf(parent.tagName.toLowerCase()) > 0) { if (parent instanceof HTMLHeadElement || parent instanceof HTMLBodyElement) {
parent.removeChild(script) parent.removeChild(script)
} }

View File

@@ -1,6 +1,6 @@
/* global _gaq: true, ga: true */ /* global _gaq: true, ga: true */
var defaultSwitches = require("../switches") var defaultSwitches = require("./switches")
module.exports = function(options) { module.exports = function(options) {
options = options || {} options = options || {}
@@ -9,16 +9,7 @@ module.exports = function(options) {
options.switches = options.switches || {} options.switches = options.switches || {}
options.switchesOptions = options.switchesOptions || {} options.switchesOptions = options.switchesOptions || {}
options.history = options.history || true options.history = options.history || true
options.analytics = (typeof options.analytics === "function" || options.analytics === false) ? options.analytics = (typeof options.analytics === "function" || options.analytics === false) ? options.analytics : defaultAnalytics
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.scrollTo = (typeof options.scrollTo === "undefined") ? 0 : options.scrollTo
options.scrollRestoration = (typeof options.scrollRestoration !== "undefined") ? options.scrollRestoration : true options.scrollRestoration = (typeof options.scrollRestoration !== "undefined") ? options.scrollRestoration : true
options.cacheBust = (typeof options.cacheBust === "undefined") ? true : options.cacheBust options.cacheBust = (typeof options.cacheBust === "undefined") ? true : options.cacheBust
@@ -36,5 +27,15 @@ module.exports = function(options) {
options.switches.body = defaultSwitches.switchElementsAlt options.switches.body = defaultSwitches.switchElementsAlt
} }
this.options = options 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,9 +1,13 @@
var on = require("../events/on") var on = require("../events/on")
var clone = require("../clone") var clone = require("../util/clone")
var attrClick = "data-pjax-click-state" var attrState = "data-pjax-state"
var formAction = function(el, event) { var formAction = function(el, event) {
if (isDefaultPrevented(event)) {
return
}
// Since loadUrl modifies options and we may add our own modifications below, // Since loadUrl modifies options and we may add our own modifications below,
// clone it so the changes don't persist // clone it so the changes don't persist
var options = clone(this.options) var options = clone(this.options)
@@ -18,56 +22,95 @@ var formAction = function(el, event) {
var virtLinkElement = document.createElement("a") var virtLinkElement = document.createElement("a")
virtLinkElement.setAttribute("href", options.requestOptions.requestUrl) virtLinkElement.setAttribute("href", options.requestOptions.requestUrl)
// Ignore external links. var attrValue = checkIfShouldAbort(virtLinkElement, options)
if (virtLinkElement.protocol !== window.location.protocol || virtLinkElement.host !== window.location.host) { if (attrValue) {
el.setAttribute(attrClick, "external") el.setAttribute(attrState, attrValue)
return
}
// 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
}
// Ignore empty anchor "foo.html#"
if (virtLinkElement.href === window.location.href.split("#")[0] + "#") {
el.setAttribute(attrClick, "anchor-empty")
return
}
// if declared as a full reload, just normally submit the form
if (options.currentUrlFullReload) {
el.setAttribute(attrClick, "reload")
return return
} }
event.preventDefault() event.preventDefault()
var paramObject = [] if (el.enctype === "multipart/form-data") {
for (var elementKey in el.elements) { options.requestOptions.formData = new FormData(el)
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)})
}
} }
else {
options.requestOptions.requestParams = parseFormElements(el)
} }
// Creating a getString el.setAttribute(attrState, "submit")
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 options.triggerElement = el
this.loadUrl(virtLinkElement.href, options) this.loadUrl(virtLinkElement.href, options)
} }
function parseFormElements(el) {
var requestParams = []
for (var elementKey in el.elements) {
if (Number.isNaN(Number(elementKey))) {
continue;
}
var element = el.elements[elementKey]
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 i = 0; i < element.options.length; i++) {
opt = element.options[i]
if (opt.selected) {
values.push(opt.value || opt.text)
}
}
}
else {
values.push(element.value)
}
for (var j = 0; j < values.length; j++) {
requestParams.push({
name: encodeURIComponent(element.name),
value: encodeURIComponent(values[j])
})
}
}
}
}
return requestParams
}
function checkIfShouldAbort(virtLinkElement, options) {
// Ignore external links.
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.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] + "#") {
return "anchor-empty"
}
// if declared as a full reload, just normally submit the form
if (options.currentUrlFullReload && virtLinkElement.href === window.location.href.split("#")[0]) {
return "reload"
}
}
var isDefaultPrevented = function(event) { var isDefaultPrevented = function(event) {
return event.defaultPrevented || event.returnValue === false return event.defaultPrevented || event.returnValue === false
} }
@@ -75,19 +118,13 @@ var isDefaultPrevented = function(event) {
module.exports = function(el) { module.exports = function(el) {
var that = this var that = this
on(el, "submit", function(event) { el.setAttribute(attrState, "")
if (isDefaultPrevented(event)) {
return
}
on(el, "submit", function(event) {
formAction.call(that, el, event) formAction.call(that, el, event)
}) })
on(el, "keyup", function(event) { on(el, "keyup", function(event) {
if (isDefaultPrevented(event)) {
return
}
if (event.keyCode === 13) { if (event.keyCode === 13) {
formAction.call(that, el, event) formAction.call(that, el, event)
} }

View File

@@ -1,47 +1,20 @@
var on = require("../events/on") var on = require("../events/on")
var clone = require("../clone") var clone = require("../util/clone")
var attrClick = "data-pjax-click-state" var attrState = "data-pjax-state"
var attrKey = "data-pjax-keyup-state"
var linkAction = function(el, event) { var linkAction = function(el, event) {
if (isDefaultPrevented(event)) {
return
}
// Since loadUrl modifies options and we may add our own modifications below, // Since loadUrl modifies options and we may add our own modifications below,
// clone it so the changes don't persist // 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 var attrValue = checkIfShouldAbort(el, event)
options.requestOptions = {} if (attrValue) {
el.setAttribute(attrState, attrValue)
// 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
}
// 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 return
} }
@@ -52,17 +25,42 @@ var linkAction = function(el, event) {
this.options.currentUrlFullReload && this.options.currentUrlFullReload &&
el.href === window.location.href.split("#")[0] el.href === window.location.href.split("#")[0]
) { ) {
el.setAttribute(attrClick, "reload") el.setAttribute(attrState, "reload")
this.reload() this.reload()
return return
} }
el.setAttribute(attrClick, "load") el.setAttribute(attrState, "load")
options.triggerElement = el options.triggerElement = el
this.loadUrl(el.href, options) 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) { var isDefaultPrevented = function(event) {
return event.defaultPrevented || event.returnValue === false return event.defaultPrevented || event.returnValue === false
} }
@@ -70,25 +68,13 @@ var isDefaultPrevented = function(event) {
module.exports = function(el) { module.exports = function(el) {
var that = this var that = this
on(el, "click", function(event) { el.setAttribute(attrState, "")
if (isDefaultPrevented(event)) {
return
}
on(el, "click", function(event) {
linkAction.call(that, el, event) linkAction.call(that, el, event)
}) })
on(el, "keyup", function(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
}
if (event.keyCode === 13) { if (event.keyCode === 13) {
linkAction.call(that, el, event) linkAction.call(that, el, event)
} }

View File

@@ -0,0 +1,70 @@
var clone = require("../util/clone.js")
var newUid = require("../uniqueid.js")
var trigger = require("../events/trigger.js")
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)
// 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, this.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,15 +1,17 @@
var attrState = "data-pjax-state"
module.exports = function(el) { module.exports = function(el) {
switch (el.tagName.toLowerCase()) { switch (el.tagName.toLowerCase()) {
case "a": case "a":
// only attach link if el does not already have link attached // only attach link if el does not already have link attached
if (!el.hasAttribute("data-pjax-click-state")) { if (!el.hasAttribute(attrState)) {
this.attachLink(el) this.attachLink(el)
} }
break break
case "form": case "form":
// only attach link if el does not already have link attached // only attach link if el does not already have link attached
if (!el.hasAttribute("data-pjax-click-state")) { if (!el.hasAttribute(attrState)) {
this.attachForm(el) this.attachForm(el)
} }
break break

View File

@@ -1,43 +1,75 @@
var updateQueryString = require("./util/update-query-string");
module.exports = function(location, options, callback) { module.exports = function(location, options, callback) {
options = options || {} options = options || {}
var requestMethod = options.requestMethod || "GET" var queryString
var requestPayload = options.requestPayloadString || null 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 request = new XMLHttpRequest()
var timeout = options.timeout || 0
request.onreadystatechange = function() { request.onreadystatechange = function() {
if (request.readyState === 4) { if (request.readyState === 4) {
if (request.status === 200) { if (request.status === 200) {
callback(request.responseText, request) callback(request.responseText, request, location, options)
} }
else { else if (request.status !== 0) {
callback(null, request) callback(null, request, location, options)
} }
} }
} }
request.onerror = function(e) { request.onerror = function(e) {
console.log(e) console.log(e)
callback(null, request) callback(null, request, location, options)
} }
request.ontimeout = function() { 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 // Add a timestamp as part of the query string if cache busting is enabled
if (this.options.cacheBust) { if (options.cacheBust) {
location += (!/[?&]/.test(location) ? "?" : "&") + new Date().getTime() location = updateQueryString(location, "t", Date.now())
} }
request.open(requestMethod.toUpperCase(), location, true) request.open(requestMethod, location, true)
request.timeout = options.timeout request.timeout = timeout
request.setRequestHeader("X-Requested-With", "XMLHttpRequest") request.setRequestHeader("X-Requested-With", "XMLHttpRequest")
request.setRequestHeader("X-PJAX", "true") request.setRequestHeader("X-PJAX", "true")
request.setRequestHeader("X-PJAX-Selectors", JSON.stringify(options.selectors))
// Add the request payload if available // Send the proper header information for POST forms
if (options.requestPayloadString !== undefined && options.requestPayloadString !== "") { if (requestPayload && requestMethod === "POST") {
// Send the proper header information along with the request request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
} }
request.send(requestPayload) request.send(requestPayload)

View File

@@ -8,7 +8,14 @@ module.exports = {
innerHTML: function(oldEl, newEl) { innerHTML: function(oldEl, newEl) {
oldEl.innerHTML = newEl.innerHTML oldEl.innerHTML = newEl.innerHTML
if (newEl.className === "") {
oldEl.removeAttribute("class")
}
else {
oldEl.className = newEl.className oldEl.className = newEl.className
}
this.onSwitch() this.onSwitch()
}, },
@@ -26,6 +33,12 @@ module.exports = {
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) { sideBySide: function(oldEl, newEl, options, switchOptions) {
var forEach = Array.prototype.forEach var forEach = Array.prototype.forEach
var elsToRemove = [] var elsToRemove = []

View File

@@ -1,4 +1,5 @@
module.exports = function(obj) { module.exports = function(obj) {
/* istanbul ignore if */
if (null === obj || "object" !== typeof obj) { if (null === obj || "object" !== typeof obj) {
return obj return obj
} }

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

@@ -0,0 +1,10 @@
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
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "pjax", "name": "pjax",
"version": "0.2.5", "version": "0.2.6",
"description": "Easily enable fast AJAX navigation on any website (using pushState + XHR)", "description": "Easily enable fast AJAX navigation on any website (using pushState + XHR)",
"keywords": [ "keywords": [
"pjax", "pjax",
@@ -24,6 +24,7 @@
"pjax.js", "pjax.js",
"pjax.min.js" "pjax.min.js"
], ],
"types": "index.d.ts",
"devDependencies": { "devDependencies": {
"browserify": "^15.0.0", "browserify": "^15.0.0",
"jscs": "^3.0.7", "jscs": "^3.0.7",
@@ -42,14 +43,14 @@
"scripts": { "scripts": {
"lint": "jscs . && jshint . --exclude-path .gitignore", "lint": "jscs . && jshint . --exclude-path .gitignore",
"standalone": "browserify index.js --standalone Pjax > pjax.js", "standalone": "browserify index.js --standalone Pjax > pjax.js",
"build": "npm run standalone && uglifyjs pjax.js -o pjax.min.js", "build": "yarn run standalone && uglifyjs pjax.js -o pjax.min.js",
"build-debug": "browserify index.js --debug --standalone Pjax > pjax.js", "build-debug": "browserify index.js --debug --standalone Pjax > pjax.js",
"tests": "tape -r ./tests/setup.js \"./tests/**/*.js\"", "tests": "tape -r ./tests/setup.js \"./tests/**/*.js\"",
"test": "npm run lint && npm run tests | tap-spec", "test": "yarn run lint && yarn run tests | tap-spec",
"coverage-tests": "npm run tests | tap-nyc", "coverage-tests": "yarn run tests | tap-nyc",
"coverage": "nyc -x \"tests/**\" npm run coverage-tests", "coverage": "nyc -x \"tests/**\" yarn run coverage-tests",
"example": "opn http://localhost:3000/example/ && serve -p 3000 .", "example": "opn http://localhost:3000/example/ && serve -p 3000 .",
"prepublish": "npm run build", "prepublish": "yarn run build",
"release": "npmpub" "release": "npmpub"
} }
} }

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

@@ -14,8 +14,8 @@ tape("test evalScript method", function(t) {
t.equal(document.body.className, "executed", "script has been properly executed") t.equal(document.body.className, "executed", "script has been properly executed")
script.innerHTML = "document.write('failure')" script.innerHTML = "document.write('failure')"
document.body.text = "document.write hasn't been executed" var bodyText = "document.write hasn't been executed"
var bodyText = document.body.text document.body.text = bodyText
evalScript(script) evalScript(script)
t.equal(document.body.text, bodyText, "document.write hasn't been executed") t.equal(document.body.text, bodyText, "document.write hasn't been executed")

View File

@@ -2,7 +2,7 @@ var tape = require("tape")
var executeScripts = require("../../lib/execute-scripts") var executeScripts = require("../../lib/execute-scripts")
tape("test executeScripts method", function(t) { tape("test executeScripts method when the script tag is inside a container", function(t) {
document.body.className = "" document.body.className = ""
var container = document.createElement("div") var container = document.createElement("div")
@@ -14,3 +14,16 @@ tape("test executeScripts method", function(t) {
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,10 +1,10 @@
var tape = require("tape") var tape = require("tape")
var parseOptions = require("../../../lib/proto/parse-options.js") var parseOptions = require("../../lib/parse-options.js")
tape("test parse initalization options function", function(t) { tape("test parse initalization options function", function(t) {
t.test("- default options", function(t) { t.test("- default options", function(t) {
var pjax = {} var pjax = {}
parseOptions.call(pjax, {}) pjax.options = parseOptions({})
t.equal(pjax.options.elements, "a[href], form[action]") t.equal(pjax.options.elements, "a[href], form[action]")
t.equal(pjax.options.selectors.length, 2, "selectors length") t.equal(pjax.options.selectors.length, 2, "selectors length")
@@ -30,7 +30,7 @@ tape("test parse initalization options function", function(t) {
// verify analytics always ends up as a function even when passed not a function // verify analytics always ends up as a function even when passed not a function
t.test("- analytics is a function", function(t) { t.test("- analytics is a function", function(t) {
var pjax = {} var pjax = {}
parseOptions.call(pjax, {analytics: "some string"}) pjax.options = parseOptions({analytics: "some string"})
t.deepEqual(typeof pjax.options.analytics, "function") t.deepEqual(typeof pjax.options.analytics, "function")
t.end() t.end()
@@ -39,7 +39,7 @@ tape("test parse initalization options function", function(t) {
// verify that the value false for scrollTo is not squashed // verify that the value false for scrollTo is not squashed
t.test("- scrollTo remains false", function(t) { t.test("- scrollTo remains false", function(t) {
var pjax = {} var pjax = {}
parseOptions.call(pjax, {scrollTo: false}) pjax.options = parseOptions({scrollTo: false})
t.deepEqual(pjax.options.scrollTo, false) t.deepEqual(pjax.options.scrollTo, false)
t.end() t.end()

View File

@@ -4,20 +4,18 @@ var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger") var trigger = require("../../../lib/events/trigger")
var attachForm = require("../../../lib/proto/attach-form") var attachForm = require("../../../lib/proto/attach-form")
var form = document.createElement("form") var attr = "data-pjax-state"
var attr = "data-pjax-click-state"
var preventDefault = function(e) { e.preventDefault() }
tape("test attach form prototype method", function(t) { tape("test attach form prototype method", function(t) {
t.plan(7) var form = document.createElement("form")
var loadUrlCalled = false
attachForm.call({ attachForm.call({
options: {}, options: {
reload: function() { currentUrlFullReload: true
t.equal(form.getAttribute(attr), "reload", "triggering a simple reload will just submit the form")
}, },
loadUrl: function() { loadUrl: function() {
t.equal(form.getAttribute(attr), "submit", "triggering a post to the next page") loadUrlCalled = true
} }
}, form) }, form)
@@ -29,50 +27,57 @@ tape("test attach form prototype method", function(t) {
form.action = internalUri + "#anchor" form.action = internalUri + "#anchor"
trigger(form, "submit") trigger(form, "submit")
t.equal(form.getAttribute(attr), "anchor-present", "internal anchor stop behavior") t.equal(form.getAttribute(attr), "anchor", "internal anchor stop behavior")
window.location.hash = "#anchor" window.location.hash = "#anchor"
form.action = internalUri + "#another-anchor" form.action = internalUri + "#another-anchor"
trigger(form, "submit") trigger(form, "submit")
t.notEqual(form.getAttribute(attr), "anchor", "differents anchors stop behavior") t.equal(form.getAttribute(attr), "anchor", "different anchors stop behavior")
window.location.hash = "" window.location.hash = ""
form.action = internalUri + "#" form.action = internalUri + "#"
trigger(form, "submit") trigger(form, "submit")
t.equal(form.getAttribute(attr), "anchor-empty", "empty anchor stop behavior") t.equal(form.getAttribute(attr), "anchor-empty", "empty anchor stop behavior")
form.action = internalUri form.action = window.location.href
trigger(form, "submit") trigger(form, "submit")
// see reload defined above 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.action = window.location.protocol + "//" + window.location.host + "/internal"
form.method = "POST" form.method = "POST"
trigger(form, "submit") trigger(form, "submit")
// see post defined above t.equal(form.getAttribute(attr), "submit", "triggering a POST request to the next page")
t.equal(loadUrlCalled, true, "loadUrl() called correctly")
loadUrlCalled = false
form.setAttribute(attr, "")
form.action = window.location.protocol + "//" + window.location.host + "/internal" form.action = window.location.protocol + "//" + window.location.host + "/internal"
form.method = "GET" form.method = "GET"
trigger(form, "submit") trigger(form, "submit")
// see post defined above 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) { tape("test attach form preventDefaulted events", function(t) {
var callbacked = false var loadUrlCalled = false
var form = document.createElement("form") var form = document.createElement("form")
// This needs to be before the call to attachForm()
on(form, "submit", function(event) { event.preventDefault() })
attachForm.call({ attachForm.call({
options: {}, options: {},
loadUrl: function() { loadUrl: function() {
callbacked = true loadUrlCalled = true
} }
}, form) }, form)
form.action = "#" form.action = "#"
on(form, "submit", preventDefault)
trigger(form, "submit") trigger(form, "submit")
t.equal(callbacked, false, "events that are preventDefaulted should not fire callback") t.equal(loadUrlCalled, false, "events that are preventDefaulted should not fire callback")
t.end() t.end()
}) })
@@ -93,3 +98,87 @@ tape("test options are not modified by attachForm", function(t) {
t.end() t.end()
}) })
tape("test submit triggered by keyboard", function(t) {
var form = document.createElement("form")
var pjax = {
options: {},
loadUrl: function() {
t.equal(form.getAttribute(attr), "submit", "triggering a internal link actually submits the form")
}
}
t.plan(2)
attachForm.call(pjax, form)
form.action = window.location.protocol + "//" + window.location.host + "/internal"
trigger(form, "keyup", {keyCode: 14})
t.equal(form.getAttribute(attr), "", "keycode other than 13 doesn't trigger anything")
trigger(form, "keyup", {keyCode: 13})
// see loadUrl defined above
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

@@ -4,27 +4,22 @@ var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger") var trigger = require("../../../lib/events/trigger")
var attachLink = require("../../../lib/proto/attach-link") var attachLink = require("../../../lib/proto/attach-link")
var a = document.createElement("a") var attr = "data-pjax-state"
var attr = "data-pjax-click-state"
var preventDefault = function(e) { e.preventDefault() }
tape("test attach link prototype method", function(t) { tape("test attach link prototype method", function(t) {
t.plan(7) var a = document.createElement("a")
var loadUrlCalled = false
attachLink.call({ attachLink.call({
options: {}, options: {},
reload: function() {
t.equal(a.getAttribute(attr), "reload", "triggering exact same url reload the page")
},
loadUrl: function() { 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 a.href = internalUri
on(a, "click", preventDefault) // to avoid link to be open (break testing env)
trigger(a, "click", {metaKey: true}) trigger(a, "click", {metaKey: true})
t.equal(a.getAttribute(attr), "modifier", "event key modifier stop behavior") t.equal(a.getAttribute(attr), "modifier", "event key modifier stop behavior")
@@ -32,46 +27,47 @@ tape("test attach link prototype method", function(t) {
trigger(a, "click") trigger(a, "click")
t.equal(a.getAttribute(attr), "external", "external url stop behavior") t.equal(a.getAttribute(attr), "external", "external url stop behavior")
window.location.hash = "#anchor"
a.href = internalUri + "#anchor" a.href = internalUri + "#anchor"
trigger(a, "click") trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor-present", "internal anchor stop behavior") t.equal(a.getAttribute(attr), "anchor", "internal anchor stop behavior")
window.location.hash = "#anchor"
a.href = internalUri + "#another-anchor" a.href = internalUri + "#another-anchor"
trigger(a, "click") trigger(a, "click")
t.notEqual(a.getAttribute(attr), "anchor", "differents anchors stop behavior") t.equal(a.getAttribute(attr), "anchor", "different anchors stop behavior")
window.location.hash = "" window.location.hash = ""
a.href = internalUri + "#" a.href = internalUri + "#"
trigger(a, "click") trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor-empty", "empty anchor stop behavior") 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" a.href = window.location.protocol + "//" + window.location.host + "/internal"
trigger(a, "click") trigger(a, "click")
// see loadUrl defined above 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")
t.end() t.end()
}) })
tape("test attach link preventDefaulted events", function(t) { tape("test attach link preventDefaulted events", function(t) {
var callbacked = false var loadUrlCalled = false
var a = document.createElement("a") var a = document.createElement("a")
// This needs to be before the call to attachLink()
on(a, "click", function(event) {
event.preventDefault()
})
attachLink.call({ attachLink.call({
options: {}, options: {},
loadUrl: function() { loadUrl: function() {
callbacked = true loadUrlCalled = true
} }
}, a) }, a)
a.href = "#" a.href = "#"
on(a, "click", preventDefault)
trigger(a, "click") trigger(a, "click")
t.equal(callbacked, false, "events that are preventDefaulted should not fire callback") t.equal(loadUrlCalled, false, "events that are preventDefaulted should not fire callback")
t.end() t.end()
}) })
@@ -92,3 +88,56 @@ tape("test options are not modified by attachLink", function(t) {
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,208 @@
var tape = require("tape")
var handleReponse = require("../../../lib/proto/handle-response")
var noop = require("../../../lib/util/noop")
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)
handleReponse.bind(pjax)(false, null)
t.same(events, ["pjax:complete", "pjax:error"], "calling handleResponse(false) triggers 'pjax:complete' and 'pjax:error'")
t.end()
})
tape("test when handleResponse() is called normally", function(t) {
var pjax = {
options: {
test: 1
},
loadContent: noop,
state: {}
}
var request = {
getResponseHeader: noop
}
handleReponse.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
}
handleReponse.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"
}
}
}
handleReponse.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"
}
}
}
handleReponse.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
}
handleReponse.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() {
handleReponse.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(handleReponse.bind(pjax)("", request, ""), true, "this.latestChance() is called")
}, Error, "error is not thrown")
t.end()
})

View File

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

View File

@@ -18,12 +18,7 @@ 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) { t.test("- request is made, gets a result, and is cache-busted", function(t) {
var requestCacheBust = sendRequest.bind({ var r = sendRequest(url, {cacheBust: true}, function(result) {
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") t.equal(r.responseURL.indexOf("?"), url.length, "XHR URL is cache-busted when configured to be")
try { try {
result = JSON.parse(result) result = JSON.parse(result)
@@ -36,15 +31,107 @@ tape("test xhr request", function(t) {
}) })
}) })
t.test("- request is not cache-busted when configured not to be", function(t) { t.test("- request is not cache-busted when configured not to be", function(t) {
var requestNoCacheBust = sendRequest.bind({ var r = sendRequest(url, {}, function() {
options: {
cacheBust: false
}
})
var r = requestNoCacheBust(url, {}, function() {
t.equal(r.responseURL, url, "XHR URL is left untouched") t.equal(r.responseURL, url, "XHR URL is left untouched")
t.end() t.end()
}) })
}) })
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"
}];
var options = {
requestOptions: {
requestMethod: "POST",
requestParams: params
}
}
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,18 +1,20 @@
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 // @author darylteo
tape("test switchesSelectors", function(t) { tape("test switchesSelectors", function(t) {
// switchesSelectors relies on a higher level function callback // switchesSelectors relies on a higher level function callback
// should really be passed in instead so I'll leave it here as a TODO: // 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 // a div container is used because swapping the containers
@@ -40,3 +42,33 @@ tape("test switchesSelectors", function(t) {
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()
})

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

@@ -0,0 +1,78 @@
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()
})

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

@@ -0,0 +1,17 @@
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()
})

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

@@ -0,0 +1,17 @@
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,21 @@
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()
})

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);
}
}

4282
yarn.lock Normal file

File diff suppressed because it is too large Load Diff