Compare commits

..

175 Commits
0.1.4 ... 0.2.6

Author SHA1 Message Date
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
BehindTheMath
2d210c3305 0.2.5 (#121)
* Bump version to 0.2.5

* Update CHANGELOG
2018-02-02 11:12:27 -05:00
BehindTheMath
a72880d205 Code cleanup (#120)
* Lots of code cleanup

* Cleanup parse-options tests
  - Rename objects for clarity and inline unneeded objects
  - Remove unneeded tests
  - Use Object.keys().length instead of a custom function
  - Use typeof === "object" instead of a custom function that checks the prototype tree as well, since we don't expect anything but an object literal.

* Remove old switchFallback code

* Remove polyfill for Function.prototype.bind

* Inline small functions

* Add more documentation and tests for options.currentUrlFullReload
  Closes #17

* Update package.json
2018-02-02 09:52:44 -05:00
BehindTheMath
57aed828ac Merge pull request #118 from MoOx/fix/external-scripts-events
Fix external scripts events
2018-01-31 16:46:28 -05:00
Robin North
63bc2af226 Remove redundant comment 2018-01-29 20:06:33 +00:00
Robin North
20e987929e Merge pull request #116 from MoOx/fix/blur
Fix element blurring (removing focus). Closes #4.
2018-01-29 20:03:24 +00:00
Behind The Math
90d26d641c Remove focus from form elements as well. 2018-01-29 13:51:00 -05:00
BehindTheMath
93c0a5bb9b Merge pull request #119 from MoOx/feature/scroll-restoration
Opt in to manual scroll restoration in browsers that support it
2018-01-29 09:27:50 -05:00
Robin North
8718da35c6 Opt in to manual scroll restoration in browsers that support it 2018-01-28 14:35:25 +00:00
Robin North
31e1e7e405 Enable test for document.write in eval'd scripts 2018-01-26 15:54:07 +00:00
Robin North
05163d9fb5 Stop dispatching extraneous pjax:complete events, correct typo 2018-01-26 15:53:07 +00:00
Robin North
a15c94c55a Merge pull request #113 from MoOx/cleanup/analytics
Cleanup default analytics function
2018-01-25 07:50:59 +00:00
Behind The Math
2d4df39f72 Remove a redundant call to .blur() 2018-01-24 20:19:02 -05:00
Behind The Math
fa05e94f07 Only blur element if it's contained by one of the selectors
Previously, Pjax would blur (remove focus) from the active element
regardless of where it was on the page. This restricts that to
happen only if the active element is contained by one of the
elements represented by options.selectors, because only those are
affected by Pjax.

Fixes #4
2018-01-24 20:18:41 -05:00
BehindTheMath
e3df2b1c3d Merge pull request #114 from MoOx/fix/abort-pending-xhr
Abort previous pending XHR when navigating
2018-01-24 18:54:33 -05:00
Robin North
01fb72ceeb Simplify options assignment 2018-01-24 23:40:05 +00:00
Robin North
f642eec047 Preserve ability to disable analytics behavior, explicitly document this option 2018-01-24 23:38:34 +00:00
Robin North
cc384b9b16 Improve check for analytics function option 2018-01-24 23:35:45 +00:00
Robin North
f559ca2914 Cleanup default analytics function 2018-01-24 23:35:45 +00:00
Robin North
137322543c Explicitly flag if aborting a request fails due to request completing 2018-01-24 23:20:36 +00:00
Robin North
f7f68b2e50 Fix module filename 2018-01-24 23:15:40 +00:00
Robin North
ff62289683 Make use of new noop module 2018-01-24 14:51:16 +00:00
BehindTheMath
3205596f3e Merge pull request #115 from MoOx/feature/minify
Add a minified version of Pjax to NPM
2018-01-24 08:48:23 -05:00
Behind The Math
3c8cd087c5 Use double quotes in CLI arguments
Single quotes don't work in all environments.
2018-01-23 19:40:13 -05:00
Behind The Math
12f3be21fb Exclude pjax.min.js fom JSCS 2018-01-23 19:35:18 -05:00
Behind The Math
b74fbc4178 Add a minified version of Pjax to NPM
Fixes #108.
2018-01-23 17:53:10 -05:00
BehindTheMath
8fcef04c9e Merge pull request #112 from timtrinidad/master
Use a local clone of options when initiating requests, so requestOptions don't persist
2018-01-23 15:44:26 -05:00
Tim Trinidad
feb85382f2 Revert back to using '**' glob for tests, wrap in quotes to force node to parse the args 2018-01-23 15:05:31 -05:00
Tim Trinidad
f196604d73 Add tests to ensure options are not accidentally modified 2018-01-23 13:22:48 -05:00
Tim Trinidad
526a0883a2 Clone options in attach-link to prevent requestOptions changes from persisting 2018-01-23 13:22:31 -05:00
Robin North
486ef0e0ba Abort previous pending XHR when navigating 2018-01-23 15:25:56 +00:00
Behind The Math
6000ad5620 Restore the trigger for the resize and scroll events to onSwitch 2018-01-22 20:42:57 -05:00
BehindTheMath
9845244948 Merge pull request #111 from MoOx/fix/scroll
Enhance scrolling behavior

- Save scroll position with history
- Scroll to element position when URL contains a hash
- Add scrollRestoration option
2018-01-22 20:32:57 -05:00
Behind The Math
e7935d9c74 Add scrollRestoration option 2018-01-22 18:56:22 -05:00
Tim Trinidad
e4b3952589 Clone options before modifying it for form submissions 2018-01-22 18:32:28 -05:00
Behind The Math
546b9abba3 Small bug fix 2018-01-22 17:32:59 -05:00
Behind The Math
bc2432b18c Scroll to element position when URL contains a hash
When the URL contains a hash, try to find the corresponding
element, and if found, scroll to its position.

Based on darylteo/pjax@4893a2a657

Fixes #22.
2018-01-22 11:45:42 -05:00
Behind The Math
37d303ed66 Save scroll position with history
Save scroll position when navigating away from a page, and restore
it when navigating back to that page.

Fixes #30.
2018-01-22 11:27:13 -05:00
BehindTheMath
b5c2120d08 Fix async switches (#110)
If any switches are async, the subsequent code will execute before the switches are finished. This PR moves all that code to a new function, and debounces the calls to onSwitch() so it only executes
once, after all the switches finish.

Fizes #72.
2018-01-22 10:55:29 -05:00
Robin North
ca61c4a840 Fix incorrect main field in npm package (#105)
Point npm package `main` field at module source
2018-01-18 20:55:27 -05:00
Robin North
cd09cc88d1 Replace instances of ES6 const keyword with var 2018-01-18 16:00:24 -05:00
Robin North
b98e3ef914 Fix DOMException in switchElementsAlt()
Clone attribute nodes before setting on `oldEl` to prevent the following error:

```
DOMException: Failed to execute 'setNamedItem' on 'NamedNodeMap': The node provided is an attribute node that is already an attribute of another Element; attribute nodes must be explicitly cloned.
```
2018-01-17 17:07:02 -05:00
Behind The Math
36ed7079b1 Fix tooling for the example
Apparently we do need opn-cli.
2018-01-15 16:36:29 -05:00
BehindTheMath
6fa51e58f8 Add checks for XHR redirects (#101)
Fixes #7

This checks for redirects by looking for the following, in sequence:
- XMLHttpRequest.responseURL
- the X-PJAX-URL header (like jquery-pjax)
- the X-XHR-Redirected-To header (like Turbolinks)
2018-01-10 15:45:55 -05:00
Maxime Thirouin
cb9c37fcb3 Use npmpub like it should since testling is gone 2018-01-09 16:02:09 -05:00
BehindTheMath
92d5e09494 Update tooling (#99)
- Update browserify, serve, and tape to the latest versions
- update the example script to reflect serve's new CLI options
- Remove the uneeded dependency on opn-cli
- Force Travis to use NPM 5.x
- Remove bower.json
2018-01-09 15:59:15 -05:00
Behind The Math
3bd101bb1d Update JSCS to the latest version 2018-01-09 14:09:40 -05:00
Behind The Math
c0d64e41b8 Fix linting errors 2018-01-09 14:09:40 -05:00
Behind The Math
e586440964 Update JSCS includes and excludes 2018-01-09 14:09:40 -05:00
BehindTheMath
a2e6cfc0af Fix tests (#96)
Closes #63

- Switch from testling to jsdom for browser APIs
- Switch from coverify to nyc for coverage reports
- Clean up related dead code and tooling
- Update Travis to use Node v6 and v8, since we need ES6 features for jsdom.
2018-01-08 17:21:18 -05:00
BehindTheMath
6491e32437 Add an option to set a timeout for XHR requests (#95)
Closes #24.
2018-01-07 23:56:11 -05:00
Behind The Math
d3d5ef7a11 Pass the element that triggered Pjax to the pjax:send event
Closes #62.
2017-12-21 13:06:49 -05:00
BehindTheMath
0916c74171 Merge pull request #93 from BehindTheMath/bugfix/add-switchElementsAlt
Add switchElementsAlt() to the default switches

Fixes #83.
2017-12-20 15:58:29 -05:00
Behind The Math
0781f820ee Add switchElementsAlt() to the default switches
Fixes #83.
2017-12-19 15:56:48 -05:00
Maxime Thirouin
a7b584c469 Add direct download link
Ref #57
2017-12-19 14:11:55 +01:00
Oskar
be5d58d550 Asynchronous switch functions (Make sure the DOM is parsed after switching) (#79)
* Make sure the DOM is parsed after switching

* Fix reload-link in the example
2017-12-19 13:58:22 +01:00
Bradley B Smith
1e40a0d70b Send the X-PJAX header with request (#80)
Make it more compatible with the jQuery PJAX and lets the server choose to optimize the response. Sending the list of selectors would be nice, too.
2017-12-19 13:56:30 +01:00
BehindTheMath
75eddfcab6 Add default switches to Pjax.switches (#92)
Fixes #68 and reverts #74
2017-12-19 13:49:26 +01:00
Maxime Thirouin
352e7114b6 Bye codesponsor 😢 2017-12-04 14:53:01 +01:00
markusfluer
af57adaafb Fixed #77 Fails on Internet Explorer 2017-11-02 13:02:17 +01:00
markusfluer
09f14fc86c Added evaluation of remote script tags 2017-11-02 12:51:36 +01:00
markusfluer
b17457f5a2 Fixed tests for request.js 2017-09-18 14:23:44 +02:00
markusfluer
86e5a2281a Added support do do a push-state ajax request with forms 2017-09-18 14:13:45 +02:00
Maxime Thirouin
109e78347f Update README.md 2017-09-14 21:45:39 +02:00
Markus Flür
cb3b6b8a5d Merge pull request #81 from CPTechnikVX/patch-1
Fix bug on IE11 preventing from ajax page refresh
2017-07-20 15:34:50 +02:00
CPTechnikVX
6dffeba21a Fix bug on IE11 preventing from ajax page refresh
It used to be fixed a long time ago, but was perhaps merged away...
2016-11-28 15:39:14 +01:00
Dale
acdd87ef3d README: Modified switches sidebySide example so it doesn't result in a undefined Type Error. (#74) 2016-09-27 07:07:28 +02:00
Maxime Thirouin
917c6f6bcb 0.2.4 2016-07-11 07:11:39 +02:00
Bart Nagel
b201b96a37 Polyfill XHR.responseURL so tests pass in old Safari (#73)
This property is only used in the tests. It doesn't exist in Safari 8
and before, so where it's not available this patch polyfills it,
allowing the test to continue.
2016-07-11 06:55:47 +02:00
Maxime Thirouin
eff7cfd452 Merge pull request #67 from compressed/refresh
Update refresh and handle attributes without values
2016-06-28 07:17:43 +02:00
Bart Nagel
7976f06043 Added: cacheBust option (#71)
* Refactor request test to allow further tests to be added

* Make cache-busting optional

Closes #70
2016-06-28 06:46:14 +02:00
Maxime Thirouin
ee530f4c0a 0.2.3 (bower) 2016-05-03 09:23:50 +02:00
Christopher Brickley
bf71894395 fix(attributes): for attributes without a value, add only the attribute
Some attributes, such as `itemscope` have no corresponding value. This
change allows them to still be set.
2016-04-19 12:15:39 -04:00
Christopher Brickley
beaa21fb3a fix(refresh): use this.parseDOM for refresh
Fix for issue mentioned in #36
2016-04-19 12:13:14 -04:00
Maxime Thirouin
b96b0f41a7 Merge pull request #66 from christophwolff/patch-1
Fix bracket in switches example
2016-04-05 17:20:49 +02:00
Christoph Wolff
6e113b2d06 Fix bracket in switches example 2016-04-05 16:40:52 +02:00
Maxime Thirouin
46912e6797 More logical npm publish (testling don't like to be spawn or something) 2016-03-24 13:34:11 +01:00
Maxime Thirouin
4877bac2ae 0.2.3 2016-03-24 12:39:07 +01:00
Maxime Thirouin
afe0ddb6b9 Fixed: `this.reload` is now a Function
Closes #65
2016-03-24 12:38:15 +01:00
Maxime Thirouin
a5d36d28f8 Update README.md 2016-03-15 11:36:53 +01:00
Maxime Thirouin
7a4056fd77 Update README.md 2016-03-15 11:35:13 +01:00
Maxime Thirouin
ba6ef126c0 Internal: add "npm run release" command
—skip-test is used because of tesling (it does not like to be spawned
or something like that)…
2016-03-12 07:52:52 +01:00
Maxime Thirouin
01536bfbf5 0.2.2
Closes #55
2016-03-12 07:42:37 +01:00
Maxime Thirouin
722ddf2a30 Internal: simple repository url 2016-03-12 07:27:47 +01:00
Maxime Thirouin
34fc00c89d Add standalone file back, no big deal
Closes #57
2016-03-12 07:27:06 +01:00
Maxime Thirouin
c02193c61b Internal: update CHANGELOG #59 2016-03-12 07:26:26 +01:00
Maxime Thirouin
824b229158 Merge pull request #59 from coolhihi/master
Fixed: error when using pjax with google analytics
2016-03-12 07:09:55 +01:00
COoL
e3d0f8cc1b fixed an error when using pjax with google analytics 2016-01-15 17:32:07 +08:00
Maxime Thirouin
8ea8ffad07 Update CHANGELOG 2016-01-05 07:42:00 +01:00
Maxime Thirouin
c12e4cdedd Merge pull request #52 from darylteo/patch-51
Patch 51
2016-01-05 07:15:11 +01:00
Maxime Thirouin
546b7e309a Merge pull request #50 from darylteo/patch-5
Patch 5
2016-01-05 07:14:29 +01:00
Maxime Thirouin
2f4bd760a5 Merge pull request #49 from darylteo/patch-39
[FIX] #39 - events on top level
2016-01-05 07:14:17 +01:00
darylteo
477d967804 Add clone and executeScripts as well 2016-01-05 15:02:39 +11:00
darylteo
e882b8639a Use required forEachElse 2016-01-05 15:02:00 +11:00
darylteo
3d25bee131 Bind required functions to pjax 2016-01-05 14:22:03 +11:00
darylteo
47059bdb04 Add test for switch selectors 2016-01-05 14:12:52 +11:00
darylteo
791400ed20 Tests for #5 2016-01-04 23:45:25 +11:00
darylteo
97c8b2d749 [NEW] #5 Ignore default prevented clicks 2016-01-04 23:19:36 +11:00
darylteo
b156a4f389 [TEST] Test for #39 2016-01-04 22:52:30 +11:00
darylteo
1d292a1a6e [FIX] #39 - events on top level
Events triggered on top level elements (such as window or document)
lead to a HierarchyRequestError due to a fix in IE browsers
where dispatchEvent does not fire if an element is not in the DOM.

The current test is simply to check for the existence of parentNode
However, this means top level elements get added to itself, causing
the error.

The proposed fix simply tests for top level elements in the test.
2016-01-04 22:30:00 +11:00
Maxime Thirouin
aaa2631eb7 Merge pull request #42 from MohamedBoualleg/patch-1
Update README.md
2015-12-18 08:25:55 +01:00
Maxime Thirouin
460dea8a9e Merge pull request #44 from rstacruz/fix/stuff
Add examples, remove failing test
2015-11-14 12:28:26 +01:00
Rico Sta. Cruz
b908621842 Fix lint errors 2015-11-13 23:59:22 +11:00
Rico Sta. Cruz
b20ee2261e Add example 2015-11-13 10:40:59 +11:00
Rico Sta. Cruz
ad6292fffb Add prepublish hook 2015-11-13 10:28:16 +11:00
Mohamed Bouallegue
4ed4577539 Update README.md 2015-10-11 04:06:04 +01:00
Maxime Thirouin
6eafcf8dc6 Add a standalone build to ensure it "should" work 2015-02-19 07:51:42 +01:00
Maxime Thirouin
b244a8cac4 Add files into npm release 2015-02-04 08:41:15 +01:00
Maxime Thirouin
a601e301cd 0.2.0 2015-02-04 08:10:13 +01:00
Maxime Thirouin
c16996d2b2 Update changelog with unreleased stuff 2015-01-29 07:33:26 +01:00
Maxime Thirouin
af8783587c refresh now accept an argument so you can refresh just a part of a page 2015-01-29 07:33:13 +01:00
Maxime Thirouin
14f997c8d1 Merge pull request #36 from pklada/refresh
Add simple refresh method and protect against links being bound more than once.
2015-01-29 07:03:16 +01:00
Peter Lada
9a86044f90 changed existing refresh method to reload. changed the data check to trigger a refresh on the element based on the already bound data attr 2015-01-28 18:27:16 -08:00
Peter Lada
e6a35f38e4 attach data to pjaxified links so they can't be pjaxified twice. add simple refresh method 2015-01-21 00:11:59 -08:00
Isaac Gierard
3d50ae9131 fixed uniqueid so that it is a function as expected by index.js and elsewhere
also added test for uniqueid. only test that two calls to uniqueid return unique enties. had to modify the output slightly and added a counter to the system otherwise two back to back calls would return the same value.
2015-01-16 16:21:26 -08:00
Isaac Gierard
949d1be1a0 fixed calling parseOptions with arrayed arguments 2015-01-16 15:46:31 -08:00
Isaac Gierard
a119033870 fixed some require paths also added isSupported include to index.js 2015-01-16 15:44:57 -08:00
Maxime Thirouin
482ba2c117 Merge pull request #33 from igierard/disableable-scrollTo
prevent scrollTo from being converted from false to 0
2015-01-16 06:56:25 +01:00
Isaac Gierard
7e81f791a9 fixing issues with parse-options and related tests 2014-11-20 16:21:19 -08:00
Isaac Gierard
d4ba34e5ed moved options parsing into separate file and created test
moved the options paring system into it's own function so that it may be testable
2014-11-20 14:39:48 -08:00
Isaac Gierard
ea7d1d2fce prevent scrollTo from being converted from false to 0
In the loadUrl function there is an explicit check for false as a means to disable the scrollTo behavior however if the scrollTo option is passed to the constructor as false the gaud statement was converting false to 0.
2014-11-18 14:45:50 -08:00
Maxime Thirouin
1d087e8b00 Add travis badge 2014-10-14 11:55:48 +02:00
Maxime Thirouin
3bc67f245c Cleanup & travis (should break) 2014-10-14 11:42:36 +02:00
Maxime Thirouin
332cd4e876 Add notes about current wip 2014-10-14 08:36:02 +02:00
Maxime Thirouin
db02366324 remove todo.md
all lines opened as issues
2014-10-14 08:31:31 +02:00
Maxime Thirouin
b5bf998fc6 Merge pull request #20 from MoOx/testling
Exploded & tested stuff
2014-10-14 08:27:04 +02:00
Maxime Thirouin
74e224c018 Merge wip 2014-10-14 08:24:50 +02:00
Maxime Thirouin
165532d43c Relocate all the things 2014-10-14 08:23:56 +02:00
Maxime Thirouin
414650113b Add missing switches methods (untested) 2014-10-14 08:19:44 +02:00
Maxime Thirouin
a6c9b57647 Reintroduce e0d33c9e18
https://github.com/MoOx/pjax/commit/e0d33c9e18ed542cd22c5cab74ae8c061982
2e22
2014-10-14 08:16:26 +02:00
Maxime Thirouin
f4e5ec254a old pjax huge file must die 2014-10-14 08:13:50 +02:00
Maxime Thirouin
f1976c3a10 Remove gulp (overkill) 2014-10-14 08:11:23 +02:00
François Vaux
799dd51597 Typo 2014-10-14 08:04:22 +02:00
Maxime Thirouin
76026cf8d9 Add forEachSelectors method 2014-10-14 08:04:22 +02:00
Maxime Thirouin
b6702a5ea0 Add attachLink method with test 2014-10-14 08:04:22 +02:00
Maxime Thirouin
673c483ccb Add event options as an option for the trigger function 2014-10-14 08:04:22 +02:00
Maxime Thirouin
d1536eb228 Add function.bind polyfill
https://github.com/ariya/phantomjs/issues/10522
2014-10-14 08:04:22 +02:00
Maxime Thirouin
681aa74e1d Add parseDOM & (new) parseElement proto methods 2014-10-14 08:04:22 +02:00
Maxime Thirouin
3f3fa761e1 Add getElements method 2014-10-14 08:04:22 +02:00
Maxime Thirouin
0d6643cf7a Add log method 2014-10-14 08:04:22 +02:00
Maxime Thirouin
4a4fb6fbc6 Add uniqueid helper 2014-10-14 08:04:22 +02:00
Maxime Thirouin
d5395c3d07 Test isSupported
kind of stupid, but I don't want to maintain a list of browser UA or
whatever.
2014-10-14 08:04:22 +02:00
Maxime Thirouin
8c05692004 Fix dispatchEvent for IE9/10/11
dispatchEvent doesn't work if element is not in the dom
This is not the silverbullet since an element can have a parent that is
not in the dom too, but to avoid dom update all the time, it should be
enough for very particular use cases (eg in my case unit test).
Maybe this should be moved into the test directly to be sure there is a
parent, we will see that later.
2014-10-14 08:04:21 +02:00
Maxime Thirouin
13b3485111 add npm run test--html for debugging 2014-10-14 08:04:21 +02:00
Maxime Thirouin
b1de220555 Drop fireEvent since we are supporting IE10 2014-10-14 08:03:43 +02:00
Maxime Thirouin
b4f7aceab6 history is not available before IE10
http://caniuse.com/history
2014-10-14 08:03:43 +02:00
Maxime Thirouin
1cc1aa9d04 bye README gif 2014-10-14 08:03:43 +02:00
Maxime Thirouin
52d7971dc5 Test xhr request 2014-10-14 08:03:43 +02:00
Maxime Thirouin
9ac709b5d8 Test forEachEls 2014-10-14 08:03:43 +02:00
Maxime Thirouin
fa27e05606 Test executeScripts 2014-10-14 08:03:43 +02:00
Maxime Thirouin
106b14b851 Tests events (on, off, trigger) 2014-10-14 08:03:42 +02:00
Maxime Thirouin
2990f93a20 Test evalScript 2014-10-14 08:03:42 +02:00
Maxime Thirouin
5644893c59 Run testling on less browsers 2014-10-14 08:03:42 +02:00
Maxime Thirouin
7631cc92c7 Test clone method 2014-10-14 08:03:42 +02:00
Maxime Thirouin
25a725c858 ignore dist/ 2014-10-14 08:03:42 +02:00
Maxime Thirouin
f93efb0c00 Add Gulp tasks 2014-10-14 08:03:42 +02:00
Maxime Thirouin
cac43b7c59 Add coverage command 2014-10-14 08:03:42 +02:00
Maxime Thirouin
dd590bae28 Setup testling 2014-10-14 08:00:10 +02:00
Maxime Thirouin
48376db0ef Update workflow/conventions/tools 2014-10-14 07:56:42 +02:00
67 changed files with 10356 additions and 988 deletions

5
.gitignore vendored
View File

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

View File

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

132
.jscsrc Normal file
View File

@@ -0,0 +1,132 @@
{
"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,4 +1,9 @@
{ {
"asi": true "newcap": false,
, "laxcomma": true "undef": true,
"unused": true,
"asi": true,
"esnext": true,
"node": true,
"browser": true
} }

7
.travis.yml Normal file
View File

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

View File

@@ -1,22 +1,125 @@
# 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.
([#79](https://github.com/MoOx/pjax/pull/79), [#110](https://github.com/MoOx/pjax/pull/110) - @oskarrough, @BehindTheMath, @robinnorth)
- Fixed: Bug on IE11 preventing AJAX page refresh.
([#81](https://github.com/MoOx/pjax/pull/81) - @CPTechnikVX)
- Fixed: Default switches are now available as `Pjax.switches`.
([#92](https://github.com/MoOx/pjax/pull/92) - @BehindTheMath)
- Fixed: An error that was caused by a missing `switchElementsAlt`.
([#93](https://github.com/MoOx/pjax/pull/93), [#104](https://github.com/MoOx/pjax/pull/104) - @BehindTheMath, @robinnorth)
- Fixed: Incorrect `main` field in npm package
([#105](https://github.com/MoOx/pjax/pull/105) - @robinnorth)
- Fixed: A pending XHR is now aborted if the user navigates somewhere else before the request is finished.
([#114](https://github.com/MoOx/pjax/pull/114) - @robinnorth)
- Fixed: When rendering new content, focus will now be removed only from elements within one of the containers manipulated by Pjax.
([#116](https://github.com/MoOx/pjax/pull/116) - @BehindTheMath)
- Fixed: Stop dispatching extraneous `pjax:complete` events when external scripts load
([#118](https://github.com/MoOx/pjax/pull/118) - @robinnorth)
- Added: Send the `X-PJAX` header with XHR requests.
([#80](https://github.com/MoOx/pjax/pull/80) - @bram1028)
- Added: Direct download link for script tags. (@MoOx)
- Added: Pass the element that triggered Pjax to the `pjax:send` event.
([#94](https://github.com/MoOx/pjax/pull/94) - @BehindTheMath)
- Added: An option to set a timeout for XHR requests.
([#95](https://github.com/MoOx/pjax/pull/95) - @BehindTheMath)
- Added: Checks for XHR redirects
([#101](https://github.com/MoOx/pjax/pull/101) - @BehindTheMath)
- Added: Save scroll position with history, and restore when navigating backwards or forwards.
([#110](https://github.com/MoOx/pjax/pull/110), [#119](https://github.com/MoOx/pjax/pull/119) - @BehindTheMath, @robinnorth)
- Added: Scroll to element position when URL contains a hash
([#110](https://github.com/MoOx/pjax/pull/110) - @BehindTheMath)
- Added: Minified version of the Pjax bundle.
([#115](https://github.com/MoOx/pjax/pull/115) - @BehindTheMath)
- Changed: Miscellaneous code and tests cleanup.
([#96](https://github.com/MoOx/pjax/pull/96), [#98](https://github.com/MoOx/pjax/pull/98), [#99](https://github.com/MoOx/pjax/pull/99), [#100](https://github.com/MoOx/pjax/pull/1070), [#107](https://github.com/MoOx/pjax/pull/107), [#113](https://github.com/MoOx/pjax/pull/113), [#120](https://github.com/MoOx/pjax/pull/120) - @BehindTheMath, @MoOx, @robinnorth)
# 0.2.4 - 2016-06-28
- Fixed: ``refresh`` should now work (use `this.parseDOM` for refresh)
([#67](https://github.com/MoOx/pjax/pull/67) - @compressed)
- Fixed: Some attributes, such as `itemscope` have no corresponding value.
This change allows them to still be set.
([#67](https://github.com/MoOx/pjax/pull/67) - @compressed)
- Added: ``cacheBust`` option
([#71](https://github.com/MoOx/pjax/pull/71) - @tremby)
# 0.2.3 - 2016-03-24
- Fixed: ``currentUrlFullReload`` option now works
- Fixed: ``this.reload`` is now a Function
([#65](https://github.com/MoOx/pjax/issues/65))
# 0.2.2 - 2016-03-12
- Fixed: added back standalone version in `./pjax.js`
([#57](https://github.com/MoOx/pjax/issues/57)
- Fixed: error when using pjax with google analytics (``options`` was undefined)
([#59](https://github.com/MoOx/pjax/pull/59))
- Fixed: HierarchyRequestError error
([#49](https://github.com/MoOx/pjax/pull/49))
- Fixed: ``TypeError: Pjax.forEachEls is not a function``
([#52](https://github.com/MoOx/pjax/pull/52))
- Fixed: ``TypeError: Pjax.executeScripts is not a function``
([#52](https://github.com/MoOx/pjax/pull/52))
- Fixed: ``TypeError: Pjax.clone is not a function``
([#52](https://github.com/MoOx/pjax/pull/52))
- Added: Ignore events with prevented defaults
([#50](https://github.com/MoOx/pjax/pull/50))
# 0.2.1 - 2015-02-04
- Fixed: it's better when a release have actual files.
# 0.2.0 - 2015-02-04
- Fixed: prevent scrollTo from being converted from false to 0 ([#33](https://github.com/MoOx/pjax/pull/33))
- Changed: code exploded in commonjs style
- Added: lots of tests
- Added: `refresh` method to force update a DOM element ([#36](https://github.com/MoOx/pjax/pull/36))
# 0.1.4 - 2014-10-14 # 0.1.4 - 2014-10-14
- allow to load pages without any attributes on `<html>` element (fix [#6](https://github.com/MoOx/pjax/issues/6)) - Fixed: allow to load pages without any attributes on `<html>` element (fix [#6](https://github.com/MoOx/pjax/issues/6))
- make `Pjax.switches.sideBySide` method usable (fix [#13](https://github.com/MoOx/pjax/issues/13)) - Fixed: make `Pjax.switches.sideBySide` method usable (fix [#13](https://github.com/MoOx/pjax/issues/13))
# 0.1.3 - 2014-09-16 # 0.1.3 - 2014-09-16
- clicking on the current url somewhere does not produce a full reload by default (see option `currentUrlFullReload`) - Fixed: clicking on the current url somewhere does not produce a full reload by default (see option `currentUrlFullReload`)
- fix `document.implementation.createHTMLDocument` error (in IE10, ref [#16](https://github.com/MoOx/pjax/pull/16)) - Fixed: `document.implementation.createHTMLDocument` error (in IE10, ref [#16](https://github.com/MoOx/pjax/pull/16))
# 0.1.2 - 2014-04-03 # 0.1.2 - 2014-04-03
- pjax.js relocated in `src/` - Changed: `pjax.js` relocated in `src/`
- <html> attributes of pjaxified document are now available - Fixed: `<html>` attributes of pjaxified document are now available
# 0.1.1 - 2014-04-02 # 0.1.1 - 2014-04-02
- Safer UMD wrapper (fix concat issue) - Fixed: safer UMD wrapper (fix concat issue)
# 0.1.0 - 2014-03-24 # 0.1.0 - 2014-03-24
Initial release Initial release

469
README.md
View File

@@ -1,64 +1,77 @@
# Pjax # Pjax
<img align="right" src="https://dl.dropboxusercontent.com/u/14108185/memes/mind-blow.gif"> [![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.
_It allow you to completely transform user experience of standard websites _It allows you to completely transform the user experience of standard websites
(server side generated or static ones) to make them feel they browse an app. (server-side generated or static ones) to make them feel like they are browsing an app,
Especially for user that have low bandwidth connection._ especially for users with low bandwidth connection._
**No more full page reload. No more lots of HTTP request.** **No more full page reloads. No more multiple HTTP requests.**
## No tests or Demo ? _Pjax does not rely on other libraries, like jQuery or similar. It is written entirely in vanilla JS._
~~There is still some work to make this repo sexy with tests & simple demo.~~ ## Installation
Actually there is a [WIP branch where I'm adding lot of tests](https://github.com/MoOx/pjax/tree/testling)
For now [you can see this running on my website](http://moox.io), with sexy CSS animations when switching pages. - 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):
```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):
```html
<script src="https://cdn.jsdelivr.net/npm/pjax@VERSION/pjax.min.js"></script>
```
## How Pjax works ## How Pjax works
Pjax loads page using ajax & updates the browser's current url using pushState without reloading your page's layout or any resources (js, css), giving a fast page load. 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.
_But under the hood, it's just ONE http request with a pushState() call._
Obviously, for [browsers that don't support pushState()](http://caniuse.com/#search=pushstate) Pjax fully degrades (yeah, it doesn't do anything at all).
It simply works with all permalinks & can update all parts of the page you _But under the hood, it's just ONE HTTP request with a `pushState()` call._
want (including html metas, title, navigation state).
- It's not limited to one container, like jQuery-Pjax is, Obviously, for [browsers that don't support `history.pushState()`](http://caniuse.com/#search=pushstate) Pjax gracefully degrades and does not do anything at all.
- It fully support browser history (back & forward buttons),
- It **will** support keyboard browsing (@todo), It simply works with all permalinks and can update all parts of the page you
- Automatically fallback to classic navigation for externals pages (thanks to Capitain Obvious help), want (including HTML metas, title, and navigation state).
- Automatically fallback to classic navigation for internals pages that will not have the appropriated DOM tree,
- It's not limited to one container, like jQuery-Pjax is.
- It fully supports browser history (back and forward buttons).
- It 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. - You can add pretty cool CSS transitions (animations) very easily.
- It's around 3kb (minified & gzipped). - It's around 4kb (minified and gzipped).
### Under the hood ### Under the hood
- It listen to every clicks on links _you want_ (by default all of them), - It listens to every click on links _you want_ (by default all of them).
- When a internal link hitted, Pjax grabs HTML from your server via ajax, - When an internal link is clicked, Pjax grabs HTML from your server via AJAX.
- Pjax render pages DOM tree (without loading any resources - images, css, js...) - Pjax renders the page's DOM tree (without loading any resources - images, CSS, JS...).
- It check if all defined parts can be replaced: - It checks that all defined parts can be replaced:
- if page doesn't suit requirement, classic navigation used, - If the page doesn't meet the requirements, standard navigation is used.
- if page suits requirement, Pjax does all defined DOM replacements - If the page meets the requirements, Pjax does all defined DOM replacements.
- Then, it updates the browser's current url using pushState - Then it updates the browser's current URL using `pushState()`.
## Overview ## Overview
Pjax is fully automatic. You won't need to setup anything on the existing HTML. 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 just need to designate some elements on your page that will be replaced when
you navigate your site. you navigate your site.
Consider the following page. Consider the following page.
```html ```html
<!doctype html> <!DOCTYPE html>
<html> <html>
<head> <head>
<!-- metas, title, styles, ... --> <!-- metas, title, styles, ... -->
@@ -76,7 +89,7 @@ Consider the following page.
</html> </html>
``` ```
We want Pjax to grab the url `/blah` then replace `.my-Content` with whatever it gets back. 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). 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. And also the `<aside>` please.
So we want to update `[".my-Header", ".my-Content", ".my-Sidebar"]`, **without reloading styles nor scripts**. So we want to update `[".my-Header", ".my-Content", ".my-Sidebar"]`, **without reloading styles nor scripts**.
@@ -84,73 +97,57 @@ 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.
_Magic! For real!_ **There is completely no need to do anything on server side!** _Magic! For real!_ **There is no need to do anything server-side!**
## Differences with [jQuery-pjax](https://github.com/defunkt/jquery-pjax) ## Differences with [jQuery-pjax](https://github.com/defunkt/jquery-pjax)
- No jQuery dependency, - No jQuery dependency
- Not limited to a container, - Not limited to a container
- No server side requirements, - No server-side requirements
- Works for CommonJS environment (browserify), AMD (RequireJS) or even globally, - Works for CommonJS environment (Webpack/Browserify), AMD (RequireJS) or even globally
- Allow page transition with CSS animations, - Allow page transition with CSS animations
- Can be easily hacked since every method is public (so overridable) - Can be easily tweaked, since every method is public (and as a result, overridable)
## Installation
You can install pjax from **npm**
```shell
$ npm install pjax
```
Or using **bower**
```shell
$ bower install pjax
```
Pjax can obviously be downloaded directly.
## No dependencies
_There is nothing you need. No jQuery or something._
## Compatibility ## Compatibility
Pjax only works with [browsers that support the `history.pushState` API](http://caniuse.com/#search=pushstate). Pjax only works with [browsers that support the `history.pushState()` API](http://caniuse.com/#search=pushstate).
When the API isn't supported Pjax goes into fallback mode (it just does nothing). 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()`. 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"]
}) })
``` ```
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", ".my-Header", ".my-Content", ".my-Sidebar"`.
For some reason, you might want to just target some elements to apply Pjax behavior. For some reason, you might want to just target some elements to apply Pjax behavior.
In that case, you can 2 differents things: 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. - Use a custom selector like "a.js-Pjax" or ".js-Pjax a" depending on what you want.
- override `Pjax.prototype.getElements` that just basically `querSelectorAll` the `elements` option. In this function you just need to return a `NodeList`. - Override `Pjax.prototype.getElements` that just basically `querySelectorAll` the `elements` option. In this function you just need to return a `NodeList`.
```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
@@ -158,20 +155,60 @@ Pjax.prototype.getElements = function() {
return document.getElementsByClassName(".js-Pjax") return document.getElementsByClassName(".js-Pjax")
} }
new Pjax({}) var pjax = new Pjax()
``` ```
When instanciating 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:
##### `elements` (String, default "a[href], form[action]") ```js
var pjax = new Pjax()
CSS Selector to use to retrieve links to apply Pjax // use case 1 (without options override)
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)`
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]"`)
CSS selector to use to retrieve links to apply Pjax to.
##### `selectors` (Array, default: `["title", ".js-Pjax"]`)
CSS selectors to replace. If a query returns multiples items, it will just keep the index.
Example of what you can do: Example of what you can do:
@@ -191,20 +228,20 @@ Example of what you can do:
``` ```
This example is correct and should work "as expected". This example is correct and should work "as expected".
_If there is not the same amount of DOM element from current page and new page, _If the current page and new page do not have the same amount of DOM elements,
the Pjax behavior will fallback to normal page load._ Pjax will fall back to normal page load._
##### `switches` (Object, default {}) ##### `switches` (Object, default: `{}`)
Objects containing callbacks that can be used to switch old element with new element. Objects containing callbacks that can be used to switch old elements with new elements.
Keys should be one of the defined selector. 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
".Navbar": function(oldEl, newEl, options) { ".Navbar": function(oldEl, newEl, options) {
// here it's a stupid example since it's the default behavior too // here it's a stupid example since it's the default behavior too
@@ -217,22 +254,23 @@ new Pjax({
}) })
``` ```
Callbacks are binded to Pjax instance itself to allow you to reuse it (ex: `this.onSwitch()`) Callbacks are bound to Pjax instance itself to allow you to reuse it (ex: `this.onSwitch()`)
###### Existing switches callback ###### Existing switches callback
- `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 & copy className too - `Pjax.switches.innerHTML`: replace elements using innerHTML and copy className too
- `Pjax.switches.sideBySide`: smart replacement that allow you to have both elements in the same parent when you want to use CSS animations. Old elements are removed when all childs have been fully animated ([animationEnd](http://www.w3.org/TR/css3-animations/#animationend) event triggered) - `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)
###### Create a switch callback ###### Create a switch callback
Your function can do whatever you want, but you need to: Your function can do whatever you want, but you need to:
- replace oldEl content by newEl content in some fashion - replace `oldEl`'s content with `newEl`'s content in some fashion
- call `this.onSwitch()` to trigger attached callback. - call `this.onSwitch()` to trigger the attached callback.
Here is the default behavior as example Here is the default behavior as an example:
```js ```js
function(oldEl, newEl, pjaxRequestOptions, switchesClasses) { function(oldEl, newEl, pjaxRequestOptions, switchesClasses) {
@@ -241,31 +279,31 @@ function(oldEl, newEl, pjaxRequestOptions, switchesClasses) {
} }
``` ```
##### `switchesOptions` (Object, default {}) ##### `switchesOptions` (Object, default: `{}`)
This are options that can be used during switch by switchers ( for now, only `Pjax.switches.sideBySide` use it). These are options that can be used during switch by switchers (for now, only `Pjax.switches.sideBySide` uses it).
Very convenient when you use something like [Animate.css](https://github.com/daneden/animate.css) 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). 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
} },
, switchesOptions: { switchesOptions: {
".js-Pjax": { ".js-Pjax": {
classNames: { classNames: {
// class added on the element that will be removed // class added on the element that will be removed
remove: "Animated Animated--reverse Animate--fast Animate--noDelay" remove: "Animated Animated--reverse Animate--fast Animate--noDelay",
// class added on the element that will be added // class added on the element that will be added
, add: "Animated" add: "Animated",
// class added on the element when it go backward // class added on the element when it go backward
, backward: "Animate--slideInRight" backward: "Animate--slideInRight",
// class added on the element when it go forward (used for new page too) // class added on the element when it go forward (used for new page too)
, forward: "Animate--slideInLeft" forward: "Animate--slideInLeft"
} },
, callbacks: { callbacks: {
// to make a nice transition with 2 pages as the same time // to make a nice transition with 2 pages as the same time
// we are playing with absolute positioning for element we are removing // we are playing with absolute positioning for element we are removing
// & we need live metrics to have something great // & we need live metrics to have something great
@@ -275,12 +313,13 @@ new Pjax({
} }
} }
} }
}
}) })
``` ```
_Note that remove class include `Animated--reverse` which is a simple way to not have _Note that remove class include `Animated--reverse` which is a simple way to not have
to create duplicate transition for (slideIn + reverse => slideOut)._ to create duplicate transition for (slideIn + reverse => slideOut)._
The following CSS will be required to make something nice The following CSS will be required to make something nice:
```css ```css
/* /*
@@ -338,8 +377,7 @@ The following CSS will be required to make something nice
} }
``` ```
To get understand this CSS, here is a HTML snippet To give context to this CSS, here is an HTML snippet:
```html ```html
<!doctype html> <!doctype html>
@@ -367,113 +405,167 @@ To get understand this CSS, here is a HTML snippet
</html> </html>
``` ```
##### `history` (Boolean, default true) ##### `history` (Boolean, default: `true`)
Enable pushState. Only disable if you are crazy. Enable the use of `pushState()`. Disabling this will prevent Pjax from updating browser history.
Internaly, this option is used when `popstate` is used (to not pushState again). However, there is almost no use case where you would want to do that.
You should forget that option.
##### `analytics` (Function, default to a function that push `_gaq` `trackPageview` or send `ga` `pageview` Internally, this option is used when a `popstate` event triggers Pjax (to not `pushState()` again).
Function that allow you to add behavior for analytics. By default it try to track ##### `analytics` (Function | Boolean, default: a function that pushes `_gaq` `_trackPageview` or sends `ga` `pageview`
a pageview with Google Analytics.
It's called every time a page is switched, even for history buttons.
##### `scrollTo` (Integer, default to 0) 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.
Value (in px) to scrollTo when a page is switched. Set to `false` to disable this behavior.
##### `debug` (Boolean, default to false) ##### `scrollTo` (Integer | \[Integer, Integer\] | False, default: `0`)
Enable verbose mode & doesn't use fallback when there is an error. 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.
Useful to debug page layout differences.
##### `currentUrlFullReload` (Boolean, default to false) When set to an array of 2 integers (\[x, y\]), this is the value to scroll both horizontally and vertically.
When set to true, clicking on a link that point the current url trigger a full page reload. 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.
#### Extend Pjax ##### `scrollRestoration` (Boolean, default: `true`)
Pjax prototype & utilities methods can be used & changed so you can patch or hack When set to true, attempt to restore the scroll position when navigating backwards or forwards.
Pjax behavior, as you wish.
Here is a summary of functions: ##### `cacheBust` (Boolean, default: `true`)
- `Pjax.isSupported` (`function()`): return wheter or not the browser handle pushState correctly When set to true, append a timestamp query string segment to the requested URLs
- `Pjax.on` (`function(els, events, listener, useCapture)`): addEventListener, that handles NodeList & supports space separated event name in order to skip browser cache.
- `Pjax.off` (`function(els, events, listener, useCapture)`): removeEventListener, that handles NodeList & supports space separated event name
- `Pjax.trigger` (`function(els, events)`): fireEvent, that handles NodeList & supports space separated event name
- `Pjax.clone` (`function(obj)`): clone object
- `Pjax.executeScripts` (`function(el)`): execute scripts that are inside an element (script src or inline scripts through `Pjax.evalScript`)
- `Pjax.evalScript` (`function(el)`): execute inline script. Don't execute a script if it contains `document.write`.
- `Pjax.prototype.log` (`function()`): console.log function that is enable/disabled by `debug` option ##### `debug` (Boolean, default: `false`)
- `Pjax.prototype.getElements` (`function(el)`): retrieve elements to attach Pjax behavior
- `Pjax.prototype.parseDOM` (`function(el)`): parse DOM to attach behavior using `Pjax.prototype.getElements` & `Pjax.prototype.attachLink` Enables verbose mode. Useful to debug page layout differences.
- `Pjax.prototype.attachLink` (`function(el)`): attach Pjax behavior to a link
- `Pjax.prototype.forEachSelectors` (`function(cb, context, DOMcontext)`): call a function for each selectors defined ##### `currentUrlFullReload` (Boolean, default: `false`)
- `Pjax.prototype.switchSelectors` (`function(selectors, fromEl, toEl, options)`): loop on selectors to switch elements
- `Pjax.prototype.latestChance` (`function(href)`): when everything is fucked up, it's our only hope (just call `window.location = href`) When set to true, clicking on a link that points to the current URL will trigger a full page reload.
- `Pjax.prototype.onSwitch` (`function()`): callback triggered when elements are switched, for now it's just trigger a window resize event (lots of lib are listening to this event to draw stuff)
- `Pjax.prototype.loadContent` (`function(html, options)`): switch elements for each selectors When set to `false`, clicking on such a link will cause Pjax to load the
- `Pjax.prototype.doRequest` (`function(location, callback)`): make the ajax request to grab page from the server current page like any page.
- `Pjax.prototype.loadUrl` (`function(href, options)`): do the ajax request, handle html results & eventually handle browser history, analytics & scroll. 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.
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:
```js
var links = document.querySelectorAll(".js-Pjax");
for (var i = 0; i < links.length; i++) {
var el = links[i]
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).
##### `timeout` (Integer, default: `0`)
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 its invoked. Pjax fires a number of events regardless of how it's invoked.
All events are fired from the _document_, not the link was clicked. All events are fired from the _document_, not the link that was clicked.
#### Ajax related events
* `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. Returning false will prevent the the fallback redirect. * `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/))
```js ```js
$(document).on('pjax:send', topbar.show) document.addEventListener('pjax:send', topbar.show)
$(document).on('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 have code attached & related to the current DOM, that you only execute when page/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.
Since Pjax doesn't magically rexecute you previous code each time you load a page, you need to make a simple thing to rexecute appropriate code:
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:
```js ```js
function whenDOMReady() { function whenDOMReady() {
// do you stuff // do your stuff
} }
whenDOMReady() whenDOMReady()
new Pjax() var pjax = new Pjax()
document.addEventListener("pjax:success", whenDOMReady) document.addEventListener("pjax:success", whenDOMReady)
``` ```
_Note: Don't create the Pjax in the `whenDOMReady` function._ _Note: Don't create the Pjax instance in the `whenDOMReady` function._
For my concern & usage, I `js-Pjax`ify all body children, including stuff like navigation & footer (to get navigation state easily updated). For my concern and usage, I `js-Pjax`-ify all body children, including stuff like navigation and footer (to get navigation state easily updated).
So attached behavior are rexecuted each time a page is loaded, like in the snippet above.
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 If you want to just update a specific part (it's totally a good idea), you can just
add the DOM related code in a function & rexecute this function when "pjax:success" event is done. add the DOM-related code in a function and re-execute this function when "pjax:success" event is fired.
```js ```js
// do your global stuff // do your global stuff
//... dom ready blah blah //... DOM ready blah blah
function whenContainerReady() { function whenContainerReady() {
// do your container related stuff // do your container related stuff
} }
whenContainerReady() whenContainerReady()
new Pjax() var pjax = new Pjax()
document.addEventListener("pjax:success", whenContainerReady) document.addEventListener("pjax:success", whenContainerReady)
``` ```
@@ -485,19 +577,19 @@ document.addEventListener("pjax:success", whenContainerReady)
### Q: Disqus doesn't work anymore, how can I fix that ? ### Q: Disqus doesn't work anymore, how can I fix that ?
A: There is a few things you need to do: 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` - wrap your Disqus snippet into a DOM element that you will add to the `selector`
arra (or just wrap it with class="js-Pjax") & be sure to have a least the empty 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) wrapper on each page (to avoid differences of DOM between pages)
- edit your disqus snippet like the one below - edit your Disqus snippet like the one below
#### Disqus snippet before pjax integration #### Disqus snippet before Pjax integration
```html ```html
<script> <script>
var disqus_shortname = 'YOURSHORTNAME' var disqus_shortname = 'YOURSHORTNAME'
, disqus_identifier = 'PAGEID' var disqus_identifier = 'PAGEID'
, disqus_url = 'PAGEURL' var disqus_url = 'PAGEURL'
, disqus_script = 'embed.js' var disqus_script = 'embed.js'
(function(d,s) { (function(d,s) {
s = d.createElement('script');s.async=1;s.src = '//' + disqus_shortname + '.disqus.com/'+disqus_script; s = d.createElement('script');s.async=1;s.src = '//' + disqus_shortname + '.disqus.com/'+disqus_script;
(d.getElementsByTagName('head')[0]).appendChild(s); (d.getElementsByTagName('head')[0]).appendChild(s);
@@ -508,13 +600,13 @@ wrapper on each page (to avoid differences of DOM between pages)
#### Disqus snippet after Pjax integration #### Disqus snippet after Pjax integration
```html ```html
<div class="js-Pjax"><!-- need to be here on every pjaxified page, even if empty --> <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 wheter or not you include this script --> <!-- if (blah blah) { // eventual server-side test to know whether or not you include this script -->
<script> <script>
var disqus_shortname = 'YOURSHORTNAME' var disqus_shortname = 'YOURSHORTNAME'
, disqus_identifier = 'PAGEID' var disqus_identifier = 'PAGEID'
, disqus_url = 'PAGEURL' var disqus_url = 'PAGEURL'
, disqus_script = 'embed.js' var disqus_script = 'embed.js'
// here we will only load the disqus <script> if not already loaded // here we will only load the disqus <script> if not already loaded
if (!window.DISQUS) { if (!window.DISQUS) {
@@ -539,19 +631,22 @@ wrapper on each page (to avoid differences of DOM between pages)
</div> </div>
``` ```
**Note: The thing you need to understand is that Pjax handle inline `<script>` only for container you are reloading.** **Note: Pjax only handles inline `<script>` blocks for the container you are switching.**
--- ---
## Contributing ## Examples
Work on a branch, install dev-dependencies, respect coding style & run tests before submitting a bug fix or a feature. Clone this repository and run `npm run example`, which will open the example app in your browser.
$ git clone https://github.com/MoOx/pjax.git ---
$ git checkout -b patch-1
$ npm install
$ npm test
## [Changelog](CHANGELOG.md) ## CONTRIBUTING
## [License](LICENSE) * ⇄ 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)
## [LICENSE](LICENSE)

11
TODO.md
View File

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

View File

@@ -1,31 +0,0 @@
{
"name": "pjax",
"version": "0.1.4",
"description": "Easily enable fast Ajax navigation on any website (using pushState + xhr)",
"keywords": [
"pjax",
"push",
"state",
"ajax",
"navigation",
"transition",
"animation"
],
"main": "src/pjax.js",
"homepage": "https://github.com/MoOx/pjax",
"authors": [
"Maxime Thirouin"
],
"license": "MIT",
"moduleType": [
"amd",
"globals",
"node"
],
"ignore": [
"**/.*",
"node_modules",
"test",
"tests"
]
}

61
example/example.js Normal file
View File

@@ -0,0 +1,61 @@
/* 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)
document.addEventListener("pjax:send", function() {
console.log("Event: pjax:send", arguments)
})
document.addEventListener("pjax:complete", function() {
console.log("Event: pjax:complete", arguments)
})
document.addEventListener("pjax:error", function() {
console.log("Event: pjax:error", arguments)
})
document.addEventListener("pjax:success", function() {
console.log("Event: pjax:success", arguments)
// Init page content
initButtons()
})
document.addEventListener("DOMContentLoaded", function() {
// Init Pjax instance
pjax = new Pjax({
elements: [".js-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>

29
example/index.html Normal file
View File

@@ -0,0 +1,29 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Hello</title>
<script src="../pjax.js"></script>
<script src="example.js"></script>
</head>
<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 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>

15
example/page2.html Normal file
View File

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

15
example/page3.html Normal file
View File

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

279
index.js Normal file
View File

@@ -0,0 +1,279 @@
var executeScripts = require("./lib/execute-scripts.js")
var forEachEls = require("./lib/foreach-els.js")
var parseOptions = require("./lib/parse-options.js")
var switches = require("./lib/switches")
var newUid = require("./lib/uniqueid.js")
var on = require("./lib/events/on.js")
var trigger = require("./lib/events/trigger.js")
var clone = require("./lib/util/clone.js")
var contains = require("./lib/util/contains.js")
var extend = require("./lib/util/extend.js")
var noop = require("./lib/util/noop")
var Pjax = function(options) {
this.state = {
numPendingSwitches: 0,
href: null,
options: null
}
this.options = parseOptions(options)
this.log("Pjax options", this.options)
if (this.options.scrollRestoration && "scrollRestoration" in history) {
history.scrollRestoration = "manual"
}
this.maxUid = this.lastUid = newUid()
this.parseDOM(document)
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.scrollPos = st.state.scrollPos
if (st.state.uid < this.lastUid) {
opt.backward = true
}
else {
opt.forward = true
}
this.lastUid = st.state.uid
// @todo implement history cache here, based on uid
this.loadUrl(st.state.url, opt)
}
}.bind(this))
}
Pjax.switches = switches
Pjax.prototype = {
log: require("./lib/proto/log.js"),
getElements: function(el) {
return el.querySelectorAll(this.options.elements)
},
parseDOM: function(el) {
var parseElement = require("./lib/proto/parse-element")
forEachEls(this.getElements(el), parseElement, this)
},
refresh: function(el) {
this.parseDOM(el || document)
},
reload: function() {
window.location.reload()
},
attachLink: require("./lib/proto/attach-link.js"),
attachForm: require("./lib/proto/attach-form.js"),
forEachSelectors: function(cb, context, DOMcontext) {
return require("./lib/foreach-selectors.js").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)
},
latestChance: function(href) {
window.location = href
},
onSwitch: function() {
trigger(window, "resize scroll")
this.state.numPendingSwitches--
// debounce calls, so we only run this once after all switches are finished.
if (this.state.numPendingSwitches === 0) {
this.afterAllSwitches()
}
},
loadContent: function(html, options) {
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)
if (matches && matches.length) {
matches = matches[0].match(htmlAttribsRegex)
if (matches.length) {
matches.shift()
matches.forEach(function(htmlAttrib) {
var attr = htmlAttrib.trim().split("=")
if (attr.length === 1) {
tmpEl.documentElement.setAttribute(attr[0], true)
}
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)
// Clear out any focused controls before inserting new page contents.
if (document.activeElement && contains(document, this.options.selectors, document.activeElement)) {
try {
document.activeElement.blur()
} catch (e) { }
}
this.switchSelectors(this.options.selectors, tmpEl, document, options)
},
abortRequest: require("./lib/abort-request.js"),
doRequest: require("./lib/send-request.js"),
handleResponse: require("./lib/proto/handle-response.js"),
loadUrl: function(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)
trigger(document, "pjax:send", options)
// Do the request
this.request = this.doRequest(href, options, this.handleResponse.bind(this))
},
afterAllSwitches: function() {
// FF bug: Wont autofocus fields that are inserted via JS.
// This behavior is incorrect. So if theres no current focus, autofocus
// the last field.
//
// http://www.w3.org/html/wg/drafts/html/master/forms.html
var autofocusEl = Array.prototype.slice.call(document.querySelectorAll("[autofocus]")).pop()
if (autofocusEl && document.activeElement !== autofocusEl) {
autofocusEl.focus()
}
// execute scripts when DOM have been completely updated
this.options.selectors.forEach(function(selector) {
forEachEls(document.querySelectorAll(selector), function(el) {
executeScripts(el)
})
})
var state = this.state
if (state.options.history) {
if (!window.history.state) {
this.lastUid = this.maxUid = newUid()
window.history.replaceState({
url: window.location.href,
title: document.title,
uid: this.maxUid,
scrollPos: [0, 0]
},
document.title)
}
// Update browser history
this.lastUid = this.maxUid = newUid()
window.history.pushState({
url: state.href,
title: state.options.title,
uid: this.maxUid,
scrollPos: [0, 0]
},
state.options.title,
state.href)
}
this.forEachSelectors(function(el) {
this.parseDOM(el)
}, this)
// Fire Events
trigger(document,"pjax:complete pjax:success", state.options)
if (typeof state.options.analytics === "function") {
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
if (a.hash) {
var name = a.hash.slice(1)
name = decodeURIComponent(name)
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
target = target.offsetParent
} while (target)
}
}
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)
}
}
}
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")
// arguably could do `if( require("./lib/is-supported.js")()) {` but that might be a little to simple
if (Pjax.isSupported()) {
module.exports = Pjax
}
// if there isnt required browser functions, returning stupid api
else {
var stupidPjax = noop
for (var key in Pjax.prototype) {
if (Pjax.prototype.hasOwnProperty(key) && typeof Pjax.prototype[key] === "function") {
stupidPjax[key] = noop
}
}
module.exports = stupidPjax
}

8
lib/abort-request.js Normal file
View File

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

41
lib/eval-script.js Normal file
View File

@@ -0,0 +1,41 @@
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")
if (code.match("document.write")) {
if (console && console.log) {
console.log("Script contains document.write. Cant be executed correctly. Code skipped ", el)
}
return false
}
script.type = "text/javascript"
/* istanbul ignore if */
if (src !== "") {
script.src = src
script.async = false // force synchronous loading of peripheral JS
}
if (code !== "") {
try {
script.appendChild(document.createTextNode(code))
}
catch (e) {
/* istanbul ignore next */
// old IEs have funky script nodes
script.text = code
}
}
// execute
parent.appendChild(script)
// avoid pollution only in head or body tags
if (parent instanceof HTMLHeadElement || parent instanceof HTMLBodyElement) {
parent.removeChild(script)
}
return true
}

11
lib/events/off.js Normal file
View File

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

11
lib/events/on.js Normal file
View File

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

31
lib/events/trigger.js Normal file
View File

@@ -0,0 +1,31 @@
var forEachEls = require("../foreach-els")
module.exports = function(els, events, opts) {
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
if (opts) {
Object.keys(opts).forEach(function(key) {
event[key] = opts[key]
})
}
forEachEls(els, function(el) {
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)
}
el.dispatchEvent(event)
if (domFix) {
el.parentNode.removeChild(el)
}
})
})
}

18
lib/execute-scripts.js Normal file
View File

@@ -0,0 +1,18 @@
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)
}
forEachEls(el.querySelectorAll("script"), function(script) {
if (!script.type || script.type.toLowerCase() === "text/javascript") {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
evalScript(script)
}
})
}

9
lib/foreach-els.js Normal file
View File

@@ -0,0 +1,9 @@
/* 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)
}
// assume simple DOM element
return fn.call(context, els)
}

8
lib/foreach-selectors.js Normal file
View File

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

8
lib/is-supported.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = function() {
// Borrowed wholesale from https://github.com/defunkt/jquery-pjax
return window.history &&
window.history.pushState &&
window.history.replaceState &&
// pushState isnt reliable on iOS until 5.
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
}

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

@@ -0,0 +1,41 @@
/* 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 : 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})
}
}

132
lib/proto/attach-form.js Normal file
View File

@@ -0,0 +1,132 @@
var on = require("../events/on")
var clone = require("../util/clone")
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)
// 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 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 = []
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) {
return event.defaultPrevented || event.returnValue === false
}
module.exports = function(el) {
var that = this
el.setAttribute(attrState, "")
on(el, "submit", function(event) {
formAction.call(that, el, event)
})
on(el, "keyup", function(event) {
if (event.keyCode === 13) {
formAction.call(that, el, event)
}
}.bind(this))
}

82
lib/proto/attach-link.js Normal file
View File

@@ -0,0 +1,82 @@
var on = require("../events/on")
var clone = require("../util/clone")
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 attrValue = checkIfShouldAbort(el, event)
if (attrValue) {
el.setAttribute(attrState, attrValue)
return
}
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(attrState, "reload")
this.reload()
return
}
el.setAttribute(attrState, "load")
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
}
module.exports = function(el) {
var that = this
el.setAttribute(attrState, "")
on(el, "click", function(event) {
linkAction.call(that, el, event)
})
on(el, "keyup", function(event) {
if (event.keyCode === 13) {
linkAction.call(that, el, event)
}
}.bind(this))
}

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

11
lib/proto/log.js Normal file
View File

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

View File

@@ -0,0 +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(attrState)) {
this.attachLink(el)
}
break
case "form":
// only attach link if el does not already have link attached
if (!el.hasAttribute(attrState)) {
this.attachForm(el)
}
break
default:
throw "Pjax can only be applied on <a> or <form> submit"
}
}

78
lib/send-request.js Normal file
View File

@@ -0,0 +1,78 @@
var updateQueryString = require("./util/update-query-string");
module.exports = function(location, options, callback) {
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, location, options)
}
else if (request.status !== 0) {
callback(null, request, location, options)
}
}
}
request.onerror = function(e) {
console.log(e)
callback(null, request, location, options)
}
request.ontimeout = function() {
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 (options.cacheBust) {
location = updateQueryString(location, "t", Date.now())
}
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))
// Send the proper header information for POST forms
if (requestPayload && requestMethod === "POST") {
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
}
request.send(requestPayload)
return request
}

37
lib/switches-selectors.js Normal file
View File

@@ -0,0 +1,37 @@
var forEachEls = require("./foreach-els")
var defaultSwitches = require("./switches")
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)
if (this.log) {
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
}
forEachEls(newEls, function(newEl, i) {
var oldEl = oldEls[i]
if (this.log) {
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)
switchesQueue.push(callback)
}, this)
}, this)
this.state.numPendingSwitches = switchesQueue.length
switchesQueue.forEach(function(queuedSwitch) {
queuedSwitch()
})
}

122
lib/switches.js Normal file
View File

@@ -0,0 +1,122 @@
var on = require("./events/on.js")
module.exports = {
outerHTML: function(oldEl, newEl) {
oldEl.outerHTML = newEl.outerHTML
this.onSwitch()
},
innerHTML: function(oldEl, newEl) {
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
// Copy attributes from the new element to the old one
if (newEl.hasAttributes()) {
var attrs = newEl.attributes
for (var i = 0; i < attrs.length; i++) {
oldEl.attributes.setNamedItem(attrs[i].cloneNode())
}
}
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 sexyAnimationEnd = function(e) {
if (e.target !== e.currentTarget) {
// end triggered by an animation on a child
return
}
animatedElsNumber--
if (animatedElsNumber <= 0 && elsToRemove) {
elsToRemove.forEach(function(el) {
// browsing quickly can make the el
// already removed by last page update ?
if (el.parentNode) {
el.parentNode.removeChild(el)
}
})
elsToAdd.forEach(function(el) {
el.className = el.className.replace(el.getAttribute("data-pjax-classes"), "")
el.removeAttribute("data-pjax-classes")
})
elsToAdd = null // free memory
elsToRemove = null // free memory
// this is to trigger some repaint (example: picturefill)
this.onSwitch()
}
}.bind(this)
switchOptions = switchOptions || {}
forEach.call(oldEl.childNodes, function(el) {
elsToRemove.push(el)
if (el.classList && !el.classList.contains("js-Pjax-remove")) {
// for fast switch, clean element that just have been added, & not cleaned yet.
if (el.hasAttribute("data-pjax-classes")) {
el.className = el.className.replace(el.getAttribute("data-pjax-classes"), "")
el.removeAttribute("data-pjax-classes")
}
el.classList.add("js-Pjax-remove")
if (switchOptions.callbacks && switchOptions.callbacks.removeElement) {
switchOptions.callbacks.removeElement(el)
}
if (switchOptions.classNames) {
el.className += " " + switchOptions.classNames.remove + " " + (options.backward ? switchOptions.classNames.backward : switchOptions.classNames.forward)
}
animatedElsNumber++
on(el, animationEventNames, sexyAnimationEnd, true)
}
})
forEach.call(newEl.childNodes, function(el) {
if (el.classList) {
var addClasses = ""
if (switchOptions.classNames) {
addClasses = " js-Pjax-add " + switchOptions.classNames.add + " " + (options.backward ? switchOptions.classNames.forward : switchOptions.classNames.backward)
}
if (switchOptions.callbacks && switchOptions.callbacks.addElement) {
switchOptions.callbacks.addElement(el)
}
el.className += addClasses
el.setAttribute("data-pjax-classes", addClasses)
elsToAdd.push(el)
fragToAppend.appendChild(el)
animatedElsNumber++
on(el, animationEventNames, sexyAnimationEnd, true)
}
})
// pass all className of the parent
oldEl.className = newEl.className
oldEl.appendChild(fragToAppend)
}
}

8
lib/uniqueid.js Normal file
View File

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

13
lib/util/clone.js Normal file
View File

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

12
lib/util/contains.js Normal file
View File

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

1
lib/util/noop.js Normal file
View File

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

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

6892
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,56 @@
{ {
"name": "pjax", "name": "pjax",
"version": "0.1.4", "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",
"push", "pushstate",
"state",
"ajax", "ajax",
"navigation", "navigation",
"transition", "transition",
"animation" "animation"
], ],
"repository": { "repository": "https://github.com/MoOx/pjax.git",
"type": "git",
"url": "https://github.com/MoOx/pjax.git"
},
"homepage": "https://github.com/MoOx/pjax",
"bugs": {
"url": "https://github.com/MoOx/pjax/issues"
},
"author": "Maxime Thirouin", "author": "Maxime Thirouin",
"license": "MIT", "contributors": [
"main": "src/pjax.js", "BehindTheMath",
"files": [ "Robin North (http://robinnorth.co.uk)"
"CHANGELOG.md",
"LICENSE",
"src"
], ],
"license": "MIT",
"main": "index.js",
"files": [
"index.js",
"lib",
"pjax.js",
"pjax.min.js"
],
"types": "index.d.ts",
"devDependencies": { "devDependencies": {
"jscs": "^1.6.2", "browserify": "^15.0.0",
"jscs": "^3.0.7",
"jsdom": "^11.5.1",
"jsdom-global": "^3.0.2",
"jshint": "^2.5.6", "jshint": "^2.5.6",
"postcss": "^2.2.5" "npmpub": "^3.1.0",
"nyc": "^11.4.1",
"opn-cli": "^3.1.0",
"serve": "^6.4.4",
"tap-nyc": "^1.0.3",
"tap-spec": "^4.1.1",
"tape": "^4.8.0",
"uglify-js": "^3.3.8"
}, },
"scripts": { "scripts": {
"lint": "jscs **/*.js && jshint . --exclude-path .gitignore", "lint": "jscs . && jshint . --exclude-path .gitignore",
"test": "npm run lint" "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",
"tests": "tape -r ./tests/setup.js \"./tests/**/*.js\"",
"test": "npm run lint && npm run tests | tap-spec",
"coverage-tests": "npm run tests | tap-nyc",
"coverage": "nyc -x \"tests/**\" npm run coverage-tests",
"example": "opn http://localhost:3000/example/ && serve -p 3000 .",
"prepublish": "npm run build",
"release": "npmpub"
} }
} }

View File

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

View File

@@ -0,0 +1,56 @@
var tape = require("tape")
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
XMLHttpRequest.prototype.open = function(method, url) {
this.responseURL = url
return nativeOpen.apply(this, arguments)
}
}
tape("test aborting xhr request", function(t) {
var requestCacheBust = sendRequest.bind({
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.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()
})
})
t.test("- request is not aborted if it is undefined", function(t) {
var r
try {
abortRequest(r)
}
catch (e) {
t.fail("aborting an undefined request threw an error")
}
t.equal(typeof r, "undefined", "undefined xhr was ignored")
t.end()
})
t.end()
})

23
tests/lib/eval-scripts.js Normal file
View File

@@ -0,0 +1,23 @@
var tape = require("tape")
var evalScript = require("../../lib/eval-script")
tape("test evalScript method", function(t) {
document.body.className = ""
var script = document.createElement("script")
script.innerHTML = "document.body.className = 'executed'"
t.equal(document.body.className, "", "script hasn't been executed yet")
evalScript(script)
t.equal(document.body.className, "executed", "script has been properly 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()
})

110
tests/lib/events.js Normal file
View File

@@ -0,0 +1,110 @@
var tape = require("tape")
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 classCb = function() {
this.className += "on"
}
var attrCb = function() {
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 = "off"
off(el, "click", classCb)
trigger(el, "click")
t.equal(el.className, "off", "triggered event didn't fire detached callback")
t.end()
})
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")
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()
})
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")
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")
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()
})
tape("test events on top level elements", function(t) {
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 = window
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")
t.end()
})

View File

@@ -0,0 +1,29 @@
var tape = require("tape")
var executeScripts = require("../../lib/execute-scripts")
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" + ">"
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()
})
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()
})

45
tests/lib/foreach-els.js Normal file
View File

@@ -0,0 +1,45 @@
var tape = require("tape")
var forEachEls = require("../../lib/foreach-els.js")
var div = document.createElement("div")
var span = document.createElement("span")
var cb = function(el) {
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()
})
tape("test forEachEls on an array", function(t) {
div.innerHTML = "div tag"
span.innerHTML = "span tag"
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.end()
})
tape("test forEachEls on a NodeList", function(t) {
div.innerHTML = "div tag"
span.innerHTML = "span tag"
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.end()
})

View File

@@ -0,0 +1,24 @@
var tape = require("tape")
var forEachEls = require("../../lib/foreach-selectors.js")
var cb = function(el) {
el.className = "modified"
}
tape("test forEachSelector", function(t) {
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")
document.documentElement.className = ""
document.body.className = ""
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.end()
})

View File

@@ -0,0 +1,8 @@
var tape = require("tape")
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()
})

View File

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

@@ -0,0 +1,184 @@
var tape = require("tape")
var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger")
var attachForm = require("../../../lib/proto/attach-form")
var attr = "data-pjax-state"
tape("test attach form prototype method", function(t) {
var form = document.createElement("form")
var loadUrlCalled = false
attachForm.call({
options: {
currentUrlFullReload: true
},
loadUrl: function() {
loadUrlCalled = true
}
}, form)
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 = 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.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 = 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")
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.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()
})
tape("test attach form preventDefaulted events", function(t) {
var loadUrlCalled = false
var form = document.createElement("form")
// This needs to be before the call to attachForm()
on(form, "submit", function(event) { event.preventDefault() })
attachForm.call({
options: {},
loadUrl: function() {
loadUrlCalled = true
}
}, form)
form.action = "#"
trigger(form, "submit")
t.equal(loadUrlCalled, false, "events that are preventDefaulted should not fire callback")
t.end()
})
tape("test options are not modified by attachForm", function(t) {
var form = document.createElement("form")
var options = {foo: "bar"}
var loadUrl = function() {}
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")
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()
})
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

@@ -0,0 +1,143 @@
var tape = require("tape")
var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger")
var attachLink = require("../../../lib/proto/attach-link")
var attr = "data-pjax-state"
tape("test attach link prototype method", function(t) {
var a = document.createElement("a")
var loadUrlCalled = false
attachLink.call({
options: {},
loadUrl: function() {
loadUrlCalled = true
}
}, a)
var internalUri = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
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")
window.location.hash = "#anchor"
a.href = internalUri + "#anchor"
trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor", "internal anchor stop behavior")
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 = 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")
t.end()
})
tape("test attach link preventDefaulted events", function(t) {
var loadUrlCalled = false
var a = document.createElement("a")
// This needs to be before the call to attachLink()
on(a, "click", function(event) {
event.preventDefault()
})
attachLink.call({
options: {},
loadUrl: function() {
loadUrlCalled = true
}
}, a)
a.href = "#"
trigger(a, "click")
t.equal(loadUrlCalled, false, "events that are preventDefaulted should not fire callback")
t.end()
})
tape("test options are not modified by attachLink", function(t) {
var a = document.createElement("a")
var options = {foo: "bar"}
var loadUrl = function() {}
attachLink.call({options: options, loadUrl: loadUrl}, a)
a.href = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
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.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

@@ -0,0 +1,27 @@
var tape = require("tape")
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(pjax, a)
}, "<a> element can be parsed")
t.doesNotThrow(function() {
var form = document.createElement("form")
parseElement.call(pjax, form)
}, "<form> element can be parsed")
t.throws(function() {
var el = document.createElement("div")
parseElement.call(pjax, el)
}, "<div> element cannot be parsed")
t.end()
})

137
tests/lib/send-request.js Normal file
View File

@@ -0,0 +1,137 @@
var tape = require("tape")
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
XMLHttpRequest.prototype.open = function(method, url) {
this.responseURL = url
return nativeOpen.apply(this, arguments)
}
}
tape("test xhr request", function(t) {
var url = "https://httpbin.org/get"
t.test("- request is made, gets a result, and is cache-busted", function(t) {
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)
}
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"
}];
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

@@ -0,0 +1,74 @@
var tape = require("tape")
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 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 container2 = tmpEl.createElement("div")
container2.innerHTML = "<p>New Text</p><span>New Span</span>"
tmpEl.body.appendChild(container2)
switchesSelectors.bind(pjax)(
{}, // switches
{}, // switchesOptions
["p"], // selectors,
tmpEl, // fromEl
document, // toEl,
{} // options
)
t.equals(container.innerHTML, "<p>New Text</p><span>No Change</span>", "Elements correctly switched")
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()
})

12
tests/lib/uniqueid.js Normal file
View File

@@ -0,0 +1,12 @@
var tape = require("tape")
var uniqueid = require("../../lib/uniqueid.js")
tape("test uniqueid", function(t) {
var a = uniqueid()
var b = uniqueid()
t.notEqual(a,b,"Two calls to uniqueid produce different values")
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()
})

View File

@@ -0,0 +1,16 @@
var tape = require("tape")
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")
selectors = ["span"]
t.equal(contains(tempDoc, selectors, el), false, "contains() returns false when the selectors do not contain the element")
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()
})

6
tests/setup.js Normal file
View File

@@ -0,0 +1,6 @@
var jsdomOptions = {
url: "https://example.org/",
runScripts: "dangerously"
}
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);
}
}

View File