From 86e5a2281a25c4e5e50c422a2cdbae2e83244685 Mon Sep 17 00:00:00 2001 From: markusfluer Date: Mon, 18 Sep 2017 14:13:39 +0200 Subject: [PATCH] Added support do do a push-state ajax request with forms --- index.js | 6 ++- lib/eval-script.js | 9 ++-- lib/execute-scripts.js | 5 ++ lib/proto/attach-form.js | 93 ++++++++++++++++++++++++++++++++ lib/proto/attach-link.js | 2 +- lib/proto/parse-element.js | 7 ++- lib/request.js | 17 ++++-- package.json | 1 + tests/lib/proto/attach-form.js | 78 +++++++++++++++++++++++++++ tests/lib/proto/parse-element.js | 10 ++-- 10 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 lib/proto/attach-form.js create mode 100644 tests/lib/proto/attach-form.js diff --git a/index.js b/index.js index be13c30..4d6039d 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,7 @@ var Pjax = function(options) { opt.url = st.state.url opt.title = st.state.title opt.history = false - + opt.requestOptions = {}; if (st.state.uid < this.lastUid) { opt.backward = true } @@ -55,6 +55,8 @@ Pjax.prototype = { 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) }, @@ -151,7 +153,7 @@ Pjax.prototype = { trigger(document, "pjax:send", options); // Do the request - this.doRequest(href, function(html) { + this.doRequest(href, options.requestOptions, function(html) { // Fail if unable to load HTML via AJAX if (html === false) { trigger(document,"pjax:complete pjax:error", options) diff --git a/lib/eval-script.js b/lib/eval-script.js index d34436a..9591fd3 100644 --- a/lib/eval-script.js +++ b/lib/eval-script.js @@ -2,7 +2,7 @@ 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 parent = el.parentNode || document.querySelector("head") || document.documentElement var script = document.createElement("script") if (code.match("document.write")) { @@ -22,8 +22,11 @@ module.exports = function(el) { } // execute - head.insertBefore(script, head.firstChild) - head.removeChild(script) // avoid pollution + parent.appendChild(script); + // avoid pollution only in head or body tags + if (["head","body"].indexOf(parent.tagName.toLowerCase()) > 0) { + parent.removeChild(script) + } return true } diff --git a/lib/execute-scripts.js b/lib/execute-scripts.js index d6392b9..04ece08 100644 --- a/lib/execute-scripts.js +++ b/lib/execute-scripts.js @@ -4,6 +4,11 @@ var evalScript = require("./eval-script") // Needed since innerHTML does not run scripts module.exports = function(el) { // console.log("going to execute scripts for ", 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) { diff --git a/lib/proto/attach-form.js b/lib/proto/attach-form.js new file mode 100644 index 0000000..06fa447 --- /dev/null +++ b/lib/proto/attach-form.js @@ -0,0 +1,93 @@ +require("../polyfills/Function.prototype.bind") + +var on = require("../events/on") +var clone = require("../clone") + +var attrClick = "data-pjax-click-state" + +var formAction = function(el, event){ + + this.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', this.options.requestOptions.requestUrl); + + // Ignore external links. + if (virtLinkElement.protocol !== window.location.protocol || virtLinkElement.host !== window.location.host) { + el.setAttribute(attrClick, "external"); + return + } + + // Ignore click if we are on an anchor on the same page + if (virtLinkElement.pathname === window.location.pathname && virtLinkElement.hash.length > 0) { + el.setAttribute(attrClick, "anchor-present"); + return + } + + // Ignore empty anchor "foo.html#" + if (virtLinkElement.href === window.location.href.split("#")[0] + "#") { + el.setAttribute(attrClick, "anchor-empty") + return + } + + // if declared as a full reload, just normally submit the form + if ( this.options.currentUrlFullReload) { + el.setAttribute(attrClick, "reload"); + return; + } + + event.preventDefault() + + var paramObject = []; + for(var elementKey in el.elements) { + var element = el.elements[elementKey]; + if (!!element.name && element.attributes !== undefined && element.tagName.toLowerCase() !== 'button'){ + if ((element.attributes.type !== 'checkbox' && element.attributes.type !== 'radio') || element.checked) { + paramObject.push({ name: encodeURIComponent(element.name), value: encodeURIComponent(element.value)}); + } + } + } + + //Creating a getString + var paramsString = (paramObject.map(function(value){return value.name+"="+value.value;})).join('&'); + + this.options.requestOptions.requestPayload = paramObject; + this.options.requestOptions.requestPayloadString = paramsString; + + el.setAttribute(attrClick, "submit"); + + this.loadUrl(virtLinkElement.href, clone(this.options)) + +}; + +var isDefaultPrevented = function(event) { + return event.defaultPrevented || event.returnValue === false; +}; + + +module.exports = function(el) { + var that = this + + on(el, "submit", function(event) { + if (isDefaultPrevented(event)) { + return + } + + formAction.call(that, el, event) + }) + + on(el, "keyup", function(event) { + if (isDefaultPrevented(event)) { + return + } + + + if (event.keyCode == 13) { + formAction.call(that, el, event) + } + }.bind(this)) +} diff --git a/lib/proto/attach-link.js b/lib/proto/attach-link.js index 73e6986..b093f3a 100644 --- a/lib/proto/attach-link.js +++ b/lib/proto/attach-link.js @@ -51,7 +51,7 @@ var linkAction = function(el, event) { this.reload() return } - + this.options.requestOptions = this.options.requestOptions || {}; el.setAttribute(attrClick, "load") this.loadUrl(el.href, clone(this.options)) } diff --git a/lib/proto/parse-element.js b/lib/proto/parse-element.js index 736fdd7..af88ca3 100644 --- a/lib/proto/parse-element.js +++ b/lib/proto/parse-element.js @@ -7,8 +7,11 @@ module.exports = function(el) { } break - case "form": - throw "Pjax doesnt support
yet." + case "form": + // only attach link if el does not already have link attached + if (!el.hasAttribute('data-pjax-click-state')) { + this.attachForm(el) + } break default: diff --git a/lib/request.js b/lib/request.js index 892e28d..da83e71 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,4 +1,7 @@ -module.exports = function(location, callback) { +module.exports = function(location, options, callback) { + options = options || {}; + var requestMethod = options.requestMethod || "GET"; + var requestPayload = options.requestPayloadString || null; var request = new XMLHttpRequest() request.onreadystatechange = function() { @@ -17,8 +20,16 @@ module.exports = function(location, callback) { location += (!/[?&]/.test(location) ? "?" : "&") + new Date().getTime() } - request.open("GET", location, true) + request.open(requestMethod.toUpperCase(), location, true) request.setRequestHeader("X-Requested-With", "XMLHttpRequest") - request.send(null) + + // Add the request payload if available + if (options.requestPayloadString != undefined && options.requestPayloadString != "") { + // Send the proper header information along with the request + request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + } + + request.send(requestPayload) + return request } diff --git a/package.json b/package.json index 6d1be88..bef1b41 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "scripts": { "lint": "jscs **/*.js && jshint . --exclude-path .gitignore", "standalone": "browserify index.js --standalone Pjax > pjax.js", + "build-debug": "browserify index.js --debug --standalone Pjax > pjax.js", "tests": "testling", "test": "npm run lint && npm run standalone && npm run tests", "test--html": "testling --html > tests/scripts/index.html", diff --git a/tests/lib/proto/attach-form.js b/tests/lib/proto/attach-form.js new file mode 100644 index 0000000..d23a4da --- /dev/null +++ b/tests/lib/proto/attach-form.js @@ -0,0 +1,78 @@ +var tape = require("tape") + +var on = require("../../../lib/events/on") +var trigger = require("../../../lib/events/trigger") +var attachForm = require("../../../lib/proto/attach-form") + +var form = document.createElement("form") +var attr = "data-pjax-click-state" +var preventDefault = function(e) { e.preventDefault() } + +tape("test attach form prototype method", function(t) { + t.plan(7) + + attachForm.call({ + options: {}, + reload: function() { + t.equal(form.getAttribute(attr), "reload", "triggering a simple reload will just submit the form") + }, + loadUrl: function() { + t.equal(form.getAttribute(attr), "submit", "triggering a post to the next page") + } + }, 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-present", "internal anchor stop behavior") + + window.location.hash = "#anchor" + form.action = internalUri + "#another-anchor" + trigger(form, "submit") + t.notEqual(form.getAttribute(attr), "anchor", "differents anchors stop behavior") + window.location.hash = "" + + form.action = internalUri + "#" + trigger(form, "submit") + t.equal(form.getAttribute(attr), "anchor-empty", "empty anchor stop behavior") + + form.action = internalUri + trigger(form, "submit") + // see reload defined above + + form.action = window.location.protocol + "//" + window.location.host + "/internal" + form.method = 'POST' + trigger(form, "submit") + // see post defined above + + form.action = window.location.protocol + "//" + window.location.host + "/internal" + form.method = 'GET' + trigger(form, "submit") + // see post defined above + + t.end() +}) + +tape("test attach form preventDefaulted events", function(t) { + var callbacked = false + var form = document.createElement("form") + + attachForm.call({ + options: {}, + loadUrl: function() { + callbacked = true + } + }, form) + + form.action = "#" + on(form, "submit", preventDefault) + trigger(form, "submit") + t.equal(callbacked, false, "events that are preventDefaulted should not fire callback") + + t.end() +}) diff --git a/tests/lib/proto/parse-element.js b/tests/lib/proto/parse-element.js index f9b760f..082c6ac 100644 --- a/tests/lib/proto/parse-element.js +++ b/tests/lib/proto/parse-element.js @@ -1,17 +1,21 @@ var tape = require("tape") var parseElement = require("../../../lib/proto/parse-element") -var protoMock = {attachLink: function() { return true}} +var protoMock = { + attachLink: function() { return true }, + attachForm: function() { return true } +} + tape("test parse element prototype method", function(t) { t.doesNotThrow(function() { var a = document.createElement("a") parseElement.call(protoMock, a) }, " element can be parsed") - t.throws(function() { + t.doesNotThrow(function() { var form = document.createElement("form") parseElement.call(protoMock, form) - }, " cannot be used (for now)") + }, " element can be parsed") t.end() })