Exploded & tested stuff #20

Merged
MoOx merged 33 commits from testling into master 2014-10-14 01:27:04 -05:00
40 changed files with 13697 additions and 762 deletions

3
.gitignore vendored
View File

@@ -1 +1,4 @@
.DS_Store
node_modules/ node_modules/
dist/
tests/scripts/index.html

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# Pjax # Pjax
<img align="right" src="https://dl.dropboxusercontent.com/u/14108185/memes/mind-blow.gif"> [![browser support](https://ci.testling.com/MoOx/pjax.png)](https://ci.testling.com/MoOx/pjax)
> Easily enable fast Ajax navigation on any website (using pushState + xhr) > Easily enable fast Ajax navigation on any website (using pushState + xhr)
@@ -15,12 +15,9 @@ Especially for user that have low bandwidth connection._
**No more full page reload. No more lots of HTTP request.** **No more full page reload. No more lots of HTTP request.**
## No tests or Demo ? ## Demo
~~There is still some work to make this repo sexy with tests & simple demo.~~ [You can see this running on my website](http://moox.io), with sexy CSS animations when switching pages.
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.
## How Pjax works ## How Pjax works
@@ -135,8 +132,8 @@ Let's talk more about the most basic way to get started:
```js ```js
new 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"]
}) })
``` ```
@@ -203,8 +200,8 @@ Examples:
```js ```js
new 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
@@ -249,23 +246,23 @@ with or without [WOW.js](https://github.com/matthieua/WOW).
```js ```js
new 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
@@ -399,14 +396,6 @@ Pjax behavior, as you wish.
Here is a summary of functions: Here is a summary of functions:
- `Pjax.isSupported` (`function()`): return wheter or not the browser handle pushState correctly
- `Pjax.on` (`function(els, events, listener, useCapture)`): addEventListener, that handles NodeList & supports space separated event name
- `Pjax.off` (`function(els, events, listener, useCapture)`): removeEventListener, that handles NodeList & supports space separated event name
- `Pjax.trigger` (`function(els, events)`): fireEvent, that handles NodeList & supports space separated event name
- `Pjax.clone` (`function(obj)`): clone object
- `Pjax.executeScripts` (`function(el)`): execute scripts that are inside an element (script src or inline scripts through `Pjax.evalScript`)
- `Pjax.evalScript` (`function(el)`): execute inline script. Don't execute a script if it contains `document.write`.
- `Pjax.prototype.log` (`function()`): console.log function that is enable/disabled by `debug` option - `Pjax.prototype.log` (`function()`): console.log function that is enable/disabled by `debug` option
- `Pjax.prototype.getElements` (`function(el)`): retrieve elements to attach Pjax behavior - `Pjax.prototype.getElements` (`function(el)`): retrieve elements to attach Pjax behavior
- `Pjax.prototype.parseDOM` (`function(el)`): parse DOM to attach behavior using `Pjax.prototype.getElements` & `Pjax.prototype.attachLink` - `Pjax.prototype.parseDOM` (`function(el)`): parse DOM to attach behavior using `Pjax.prototype.getElements` & `Pjax.prototype.attachLink`
@@ -495,9 +484,9 @@ wrapper on each page (to avoid differences of DOM between pages)
```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);
@@ -512,9 +501,9 @@ wrapper on each page (to avoid differences of DOM between pages)
<!-- 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 wheter 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) {

256
index.js Normal file
View File

@@ -0,0 +1,256 @@
var newUid = require("./lib/uniqueid.js")
var Pjax = function(options) {
this.firstrun = true
this.options = options
this.options.elements = this.options.elements || "a[href], form[action]"
this.options.selectors = this.options.selectors || ["title", ".js-Pjax"]
this.options.switches = this.options.switches || {}
this.options.switchesOptions = this.options.switchesOptions || {}
this.options.history = this.options.history || true
this.options.analytics = this.options.analytics || function(options) {
// options.backward or options.foward can be true or undefined
// by default, we do track back/foward hit
// https://productforums.google.com/forum/#!topic/analytics/WVwMDjLhXYk
if (window._gaq) {
_gaq.push(["_trackPageview"])
}
if (window.ga) {
ga("send", "pageview", {page: options.url, title: options.title})
}
}
this.options.scrollTo = this.options.scrollTo || 0
this.options.debug = this.options.debug || false
this.maxUid = this.lastUid = newUid()
// we cant replace body.outerHTML or head.outerHTML
// it create a bug where new body or new head are created in the dom
// if you set head.outerHTML, a new body tag is appended, so the dom get 2 body
// & it break the switchFallback which replace head & body
if (!this.options.switches.head) {
this.options.switches.head = this.switchElementsAlt
}
if (!this.options.switches.body) {
this.options.switches.body = this.switchElementsAlt
}
this.log("Pjax options", this.options)
if (typeof options.analytics !== "function") {
options.analytics = function() {}
}
this.parseDOM(document)
Pjax.on(window, "popstate", function(st) {
if (st.state) {
var opt = Pjax.clone(this.options)
opt.url = st.state.url
opt.title = st.state.title
opt.history = false
if (st.state.uid < this.lastUid) {
opt.backward = true
}
else {
opt.forward = true
}
this.lastUid = st.state.uid
// @todo implement history cache here, based on uid
this.loadUrl(st.state.url, opt)
}
}.bind(this))
}
Pjax.prototype = {
log: require("./lib/proto/log.js"),
getElements: require("./lib/proto/get-elements.js"),
parseDOM: require("./lib/proto/parse-dom.js"),
attachLink: require("./lib/proto/attach-link.js"),
forEachSelectors: function(cb, context, DOMcontext) {
return require("./lib/foreach-selectors.js")(this.options.selectors, cb, context, DOMcontext)
},
switchSelectors: function(selectors, fromEl, toEl, options) {
return require("./lib/switch-selectors.js")(this.options.switches, this.options.switchesOptions, selectors, fromEl, toEl, options)
},
// 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>)
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("=")
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.
// we clear focus on non form elements
if (document.activeElement && !document.activeElement.value) {
try {
document.activeElement.blur()
} catch (e) { }
}
// 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: require("./lib/request.js"),
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))
}
}
if (Pjax.isSupported()) {
module.exports = 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
}
}
module.exports = stupidPjax
}

12
lib/clone.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = 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
}

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

@@ -0,0 +1,29 @@
module.exports = function(el) {
// console.log("going to execute script", el)
var code = (el.text || el.textContent || el.innerHTML || "")
var head = 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"
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
}

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 // = new CustomEvent(e) // doesn't everywhere yet
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) {
// THANKS YOU IE (9/10//11 concerned)
// dispatchEvent doesn't work if element is not in the dom
domFix = true
document.body.appendChild(el)
}
el.dispatchEvent(event)
if (domFix) {
el.parentNode.removeChild(el)
}
})
})
}

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

@@ -0,0 +1,15 @@
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) {
// console.log("going to execute scripts for ", el)
forEachEls(el.querySelectorAll("script"), function(script) {
if (!script.type || script.type.toLowerCase() === "text/javascript") {
if (script.parentNode) {
script.parentNode.removeChild(script)
}
evalScript(script)
}
})
}

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

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

View File

@@ -0,0 +1,20 @@
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable")
}
var aArgs = Array.prototype.slice.call(arguments, 1)
var fToBind = this
var fNOP = function () {}
var fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)))
}
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
return fBound
}
}

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

@@ -0,0 +1,76 @@
require("../polyfills/Function.prototype.bind")
var on = require("../events/on")
var clone = require("../clone")
var attrClick = "data-pjax-click-state"
var attrKey = "data-pjax-keyup-state"
var linkAction = function(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) {
el.setAttribute(attrClick, "modifier")
return
}
// we do test on href now to prevent unexpected behavior if for some reason
// user have href that can be dynamically updated
// Ignore external links.
if (el.protocol !== window.location.protocol || el.host !== window.location.host) {
el.setAttribute(attrClick, "external")
return
}
// Ignore click if we are on an anchor on the same page
if (el.pathname === window.location.pathname && el.hash.length > 0) {
el.setAttribute(attrClick, "anchor-present")
return
}
// Ignore anchors on the same page (keep native behavior)
if (el.hash && el.href.replace(el.hash, "") === window.location.href.replace(location.hash, "")) {
el.setAttribute(attrClick, "anchor")
return
}
// Ignore empty anchor "foo.html#"
if (el.href === window.location.href.split("#")[0] + "#") {
el.setAttribute(attrClick, "anchor-empty")
return
}
event.preventDefault()
// dont do "nothing" if user try to reload the page by clicking the same link twice
if (el.href === window.location.href.split("#")[0]) {
el.setAttribute(attrClick, "refresh")
this.refresh()
return
}
el.setAttribute(attrClick, "load")
this.loadUrl(el.href, clone(this.options))
}
module.exports = function(el) {
var instance = this
on(el, "click", function(event) {
linkAction.call(instance, el, event)
})
on(el, "keyup", function(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) {
el.setAttribute(attrKey, "modifier")
return
}
if(event.keyCode == 13) {
linkAction.call(instance, el, event)
}
}.bind(this))
}

View File

@@ -0,0 +1,3 @@
module.exports = function(el) {
return el.querySelectorAll(this.options.elements)
}

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

7
lib/proto/parse-dom.js Normal file
View File

@@ -0,0 +1,7 @@
var forEachEls = require("../foreach-els")
var parseElement = require("../parse-element")
module.exports = function(el) {
forEachEls(this.getElements(el), parseElement }, this)
}

View File

@@ -0,0 +1,14 @@
module.exports = function(el) {
switch (el.tagName.toLowerCase()) {
case "a":
this.attachLink(el)
break
case "form":
throw "Pjax doesnt support <form> yet."
break
default:
throw "Pjax can only be applied on <a> or <form> submit"
}
}

3
lib/refresh.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = function() {
window.location.reload()
}

19
lib/request.js Normal file
View File

@@ -0,0 +1,19 @@
module.exports = function(location, callback) {
var request = new XMLHttpRequest()
request.onreadystatechange = function() {
if (request.readyState === 4) {
if (request.status === 200) {
callback(request.responseText, request)
}
else {
callback(null, request)
}
}
}
request.open("GET", location + (!/[?&]/.test(location) ? "?": "&") + (new Date().getTime()), true)
request.setRequestHeader("X-Requested-With", "XMLHttpRequest")
request.send(null)
return request
}

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

@@ -0,0 +1,35 @@
var forEachEls = require("./foreach-els")
var defaultSwitches = require("./switches")
module.exports = function(switches, switchesOptions, selectors, fromEl, toEl, options) {
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) {
// forEachEls(newEls, function(el) {
// this.log("newEl", el, el.outerHTML)
// }, this)
// 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
}
forEachEls(newEls, function(newEl, i) {
var oldEl = oldEls[i]
if (this.log) {
this.log("newEl", newEl, "oldEl", oldEl)
}
if (switches[selector]) {
switches[selector].bind(this)(oldEl, newEl, options, switchesOptions[selector])
}
else {
defaultSwitches.outerHTML.bind(this)(oldEl, newEl, options)
}
}, this)
}, this)
}

110
lib/switches.js Normal file
View File

@@ -0,0 +1,110 @@
module.exports = {
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
var elsToRemove = []
var elsToAdd = []
var fragToAppend = document.createDocumentFragment()
// height transition are shitty on safari
// so commented for now (until I found something ?)
//var relevantHeight = 0
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")
// 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"
}
}

1
lib/uniqueid.js Normal file
View File

@@ -0,0 +1 @@
module.exports = "pjax" + (new Date().getTime())

View File

@@ -28,12 +28,31 @@
"src" "src"
], ],
"devDependencies": { "devDependencies": {
"browserify": "^3.46.0",
"coverify": "^1.0.6",
"jscs": "^1.6.2", "jscs": "^1.6.2",
"jshint": "^2.5.6", "jshint": "^2.5.6",
"postcss": "^2.2.5" "tape": "^3.0.0",
"testling": "^1.6.1"
}, },
"scripts": { "scripts": {
"lint": "jscs **/*.js && jshint . --exclude-path .gitignore", "lint": "jscs **/*.js && jshint . --exclude-path .gitignore",
"test": "npm run lint" "test": "npm run lint && testling",
"test--html": "testling --html > tests/scripts/index.html",
"coverage": "browserify -t coverify tests/**/*.js | testling | coverify"
},
"testling": {
"files": "tests/**/*.js",
"browsers": [
"ie/10..latest",
"firefox/4.0", "firefox/latest", "firefox/nightly",
"chrome/10", "chrome/latest", "chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
} }
} }

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

12496
tests/index.html Normal file

File diff suppressed because one or more lines are too long

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

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

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

@@ -0,0 +1,22 @@
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.body.text
// evalScript(script)
// t.equal(document.body.text, bodyText, "document.write hasn't been executed")
t.end()
})

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

@@ -0,0 +1,92 @@
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 eventType2 = "resize"
var eventsType = "click resize"
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()
})

View File

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

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,58 @@
var tape = require("tape")
var on = require("../../../lib/events/on")
var trigger = require("../../../lib/events/trigger")
var attachLink = require("../../../lib/proto/attach-link")
var a = document.createElement("a")
var attr = "data-pjax-click-state"
var preventDefault = function(e) { e.preventDefault() }
tape("test attach link prototype method", function(t) {
t.plan(7)
attachLink.call({
options: {},
refresh: function() {
t.equal(a.getAttribute(attr), "refresh", "triggering exact same url refresh the page")
},
loadUrl: function() {
t.equal(a.getAttribute(attr), "load", "triggering a internal link actually load the page")
}
}, a)
var internalUri = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search
a.href = internalUri
on(a, "click", preventDefault) // to avoid link to be open (break testing env)
trigger(a, "click", {metaKey: true})
t.equal(a.getAttribute(attr), "modifier", "event key modifier stop behavior")
a.href = "http://external.com/"
trigger(a, "click")
t.equal(a.getAttribute(attr), "external", "external url stop behavior")
a.href = internalUri + "#anchor"
trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor-present", "internal anchor stop behavior")
window.location.hash = "#anchor"
a.href = internalUri + "#another-anchor"
trigger(a, "click")
t.notEqual(a.getAttribute(attr), "anchor", "differents anchors stop behavior")
window.location.hash = ""
a.href = internalUri + "#"
trigger(a, "click")
t.equal(a.getAttribute(attr), "anchor-empty", "empty anchor stop behavior")
a.href = internalUri
trigger(a, "click")
// see refresh defined above
a.href = window.location.protocol + "//" + window.location.host + "/internal"
trigger(a, "click")
// see loadUrl defined above
t.end()
})

View File

@@ -0,0 +1,18 @@
var tape = require("tape")
var parseElement = require("../../../lib/proto/parse-element")
var protoMock = {attachLink: function() { return true}}
tape("test parse element prototype method", function(t) {
t.doesNotThrow(function() {
var a = document.createElement("a")
parseElement.call(protoMock, a)
}, "<a> element can be parsed")
t.throws(function() {
var form = document.createElement("form")
parseElement.call(protoMock, form)
}, "<form> cannot be used (for now)")
t.end()
})

16
tests/lib/request.js Normal file
View File

@@ -0,0 +1,16 @@
var tape = require("tape")
var request = require("../../lib/request.js")
tape("test xhr request", function(t) {
var xhr = request("https://api.github.com/", function(result) {
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()
})
})

View File

@@ -0,0 +1,9 @@
var tape = require("tape")
var switchesSelectors = require("../../lib/switches-selectors.js")
tape("test switchesSelectors", function(t) {
t.fail()
t.end()
})

View File