turbograft 0.4.8 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -4
- data/lib/assets/javascripts/turbograft/click.js +44 -0
- data/lib/assets/javascripts/turbograft/component_url.js +53 -0
- data/lib/assets/javascripts/turbograft/csrf_token.js +23 -0
- data/lib/assets/javascripts/turbograft/document.js +20 -0
- data/lib/assets/javascripts/turbograft/initializers.js +83 -0
- data/lib/assets/javascripts/turbograft/link.js +58 -0
- data/lib/assets/javascripts/turbograft/page.js +105 -0
- data/lib/assets/javascripts/turbograft/remote.js +248 -0
- data/lib/assets/javascripts/turbograft/response.js +48 -0
- data/lib/assets/javascripts/turbograft/turbohead.js +159 -0
- data/lib/assets/javascripts/turbograft/turbolinks.js +504 -0
- data/lib/assets/javascripts/turbograft.js +37 -0
- data/lib/turbograft/version.rb +1 -1
- metadata +27 -27
- data/lib/assets/javascripts/turbograft/click.coffee +0 -34
- data/lib/assets/javascripts/turbograft/component_url.coffee +0 -24
- data/lib/assets/javascripts/turbograft/csrf_token.coffee +0 -9
- data/lib/assets/javascripts/turbograft/document.coffee +0 -11
- data/lib/assets/javascripts/turbograft/initializers.coffee +0 -67
- data/lib/assets/javascripts/turbograft/link.coffee +0 -41
- data/lib/assets/javascripts/turbograft/page.coffee +0 -77
- data/lib/assets/javascripts/turbograft/remote.coffee +0 -179
- data/lib/assets/javascripts/turbograft/response.coffee +0 -31
- data/lib/assets/javascripts/turbograft/turbohead.coffee +0 -142
- data/lib/assets/javascripts/turbograft/turbolinks.coffee +0 -361
- data/lib/assets/javascripts/turbograft.coffee +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be4f37c2b602a9900e822397af3c01ed23d1467bc2571728824b7b7c5737b126
|
4
|
+
data.tar.gz: e3ea401831228506abb4d2c51dec872095f1a06c3ced6c8550a4a4d5950a4dd3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7f637bf121bde59d9fb009595a588b6b520bf1128cd4beab8b94c28ab2196485cdeb0805290b21ab79ec6ad641c4a8ff95e20f88a9833eea051361257fe38f49
|
7
|
+
data.tar.gz: f852bf2519485f847f3f45b1f475f15296bab053b4369da8b8a503d0cc2c7884484b6030cc7a27ebc9cde4dbb4753e0c20dfb16643298a683341a40d0b945700
|
data/README.md
CHANGED
@@ -20,7 +20,7 @@ Turbograft was built with simplicity in mind. It intends to offer the smallest a
|
|
20
20
|
|
21
21
|
## Status
|
22
22
|
[![Gem Version](https://badge.fury.io/rb/turbograft.svg)](http://badge.fury.io/rb/turbograft)
|
23
|
-
[![Build
|
23
|
+
[![Build](https://github.com/Shopify/turbograft/actions/workflows/build.yml/badge.svg)](https://github.com/Shopify/turbograft/actions/workflows/build.yml)
|
24
24
|
|
25
25
|
## Installation
|
26
26
|
|
@@ -189,9 +189,10 @@ There is an example app that you can boot to play with TurboGraft. Open the con
|
|
189
189
|
|
190
190
|
When turbograft replaces or removes a node it uses native DOM API to do so. If any objects use jQuery to listen to events on a node then these objects will leak when the node is replaced because jQuery will still have references to it. To clean these up you'll need to tell jQuery that they're removed. This can be done with something like:
|
191
191
|
|
192
|
-
```
|
193
|
-
document.addEventListener
|
194
|
-
$(event.data).remove()
|
192
|
+
```js
|
193
|
+
document.addEventListener('page:after-node-removed', function(event) {
|
194
|
+
$(event.data).remove();
|
195
|
+
});
|
195
196
|
```
|
196
197
|
|
197
198
|
## Contributing
|
@@ -0,0 +1,44 @@
|
|
1
|
+
// The Click class handles clicked links, verifying if Turbolinks should
|
2
|
+
// take control by inspecting both the event and the link. If it should,
|
3
|
+
// the page change process is initiated. If not, control is passed back
|
4
|
+
// to the browser for default functionality.
|
5
|
+
window.Click = class Click {
|
6
|
+
static installHandlerLast(event) {
|
7
|
+
if (!event.defaultPrevented) {
|
8
|
+
document.removeEventListener('click', Click.handle, false);
|
9
|
+
document.addEventListener('click', Click.handle, false);
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
static handle(event) {
|
14
|
+
return new Click(event);
|
15
|
+
}
|
16
|
+
|
17
|
+
constructor(event) {
|
18
|
+
this.event = event;
|
19
|
+
if (this.event.defaultPrevented) { return; }
|
20
|
+
this._extractLink();
|
21
|
+
if (this._validForTurbolinks()) {
|
22
|
+
Turbolinks.visit(this.link.href);
|
23
|
+
this.event.preventDefault();
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
_extractLink() {
|
28
|
+
let link = this.event.target;
|
29
|
+
while (!!link.parentNode && (link.nodeName !== 'A')) { link = link.parentNode; }
|
30
|
+
if ((link.nodeName === 'A') && (link.href.length !== 0)) { this.link = new Link(link); }
|
31
|
+
}
|
32
|
+
|
33
|
+
_validForTurbolinks() {
|
34
|
+
return (this.link != null) && !this.link.shouldIgnore() && !this._nonStandardClick();
|
35
|
+
}
|
36
|
+
|
37
|
+
_nonStandardClick() {
|
38
|
+
return (this.event.which > 1) ||
|
39
|
+
this.event.metaKey ||
|
40
|
+
this.event.ctrlKey ||
|
41
|
+
this.event.shiftKey ||
|
42
|
+
this.event.altKey;
|
43
|
+
}
|
44
|
+
};
|
@@ -0,0 +1,53 @@
|
|
1
|
+
/* The ComponentUrl class converts a basic URL string into an object
|
2
|
+
* that behaves similarly to document.location.
|
3
|
+
*
|
4
|
+
* If an instance is created from a relative URL, the current document
|
5
|
+
* is used to fill in the missing attributes (protocol, host, port).
|
6
|
+
*/
|
7
|
+
window.ComponentUrl = class ComponentUrl {
|
8
|
+
constructor(original, link) {
|
9
|
+
if (original == null) {
|
10
|
+
original = document.location.href;
|
11
|
+
}
|
12
|
+
if (link == null) {
|
13
|
+
link = document.createElement('a');
|
14
|
+
}
|
15
|
+
if (original.constructor === ComponentUrl) {
|
16
|
+
return original;
|
17
|
+
}
|
18
|
+
this.original = original;
|
19
|
+
this.link = link;
|
20
|
+
this._parse();
|
21
|
+
}
|
22
|
+
|
23
|
+
withoutHash() {
|
24
|
+
return this.href.replace(this.hash, '');
|
25
|
+
};
|
26
|
+
|
27
|
+
// Intention revealing function alias
|
28
|
+
withoutHashForIE10compatibility() {
|
29
|
+
return this.withoutHash();
|
30
|
+
};
|
31
|
+
|
32
|
+
hasNoHash() {
|
33
|
+
return this.hash.length === 0;
|
34
|
+
};
|
35
|
+
|
36
|
+
_parse() {
|
37
|
+
this.link.href = this.original;
|
38
|
+
this.href = this.link.href;
|
39
|
+
this.protocol = this.link.protocol;
|
40
|
+
this.host = this.link.host;
|
41
|
+
this.hostname = this.link.hostname
|
42
|
+
this.port = this.link.port;
|
43
|
+
this.pathname = this.link.pathname;
|
44
|
+
this.search = this.link.search;
|
45
|
+
this.hash = this.link.hash;
|
46
|
+
this.origin = [this.protocol, '//', this.hostname].join('');
|
47
|
+
if (this.port.length !== 0) {
|
48
|
+
this.origin += ":" + this.port;
|
49
|
+
}
|
50
|
+
this.relative = [this.pathname, this.search, this.hash].join('');
|
51
|
+
return this.absolute = this.href;
|
52
|
+
};
|
53
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
window.CSRFToken = class CSRFToken {
|
2
|
+
static get(doc) {
|
3
|
+
if (!doc) { doc = document; }
|
4
|
+
const tag = doc.querySelector('meta[name="csrf-token"]');
|
5
|
+
|
6
|
+
const object = {
|
7
|
+
node: tag
|
8
|
+
};
|
9
|
+
|
10
|
+
if (tag) {
|
11
|
+
object.token = tag.getAttribute('content');
|
12
|
+
}
|
13
|
+
|
14
|
+
return object;
|
15
|
+
}
|
16
|
+
|
17
|
+
static update(latest) {
|
18
|
+
const current = this.get();
|
19
|
+
if ((current.token != null) && (latest != null) && (current.token !== latest)) {
|
20
|
+
current.node.setAttribute('content', latest);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
/*
|
2
|
+
* decaffeinate suggestions:
|
3
|
+
* DS102: Remove unnecessary code created because of implicit returns
|
4
|
+
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
5
|
+
*/
|
6
|
+
TurboGraft.Document = {
|
7
|
+
create(html) {
|
8
|
+
let doc;
|
9
|
+
if (/<(html|body)/i.test(html)) {
|
10
|
+
doc = document.documentElement.cloneNode();
|
11
|
+
doc.innerHTML = html;
|
12
|
+
} else {
|
13
|
+
doc = document.documentElement.cloneNode(true);
|
14
|
+
doc.querySelector('body').innerHTML = html;
|
15
|
+
}
|
16
|
+
doc.head = doc.querySelector('head');
|
17
|
+
doc.body = doc.querySelector('body');
|
18
|
+
return doc;
|
19
|
+
}
|
20
|
+
};
|
@@ -0,0 +1,83 @@
|
|
1
|
+
/*
|
2
|
+
* decaffeinate suggestions:
|
3
|
+
* DS102: Remove unnecessary code created because of implicit returns
|
4
|
+
* DS207: Consider shorter variations of null checks
|
5
|
+
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
6
|
+
*/
|
7
|
+
const hasClass = (node, search) => node.classList.contains(search);
|
8
|
+
|
9
|
+
const nodeIsDisabled = node => node.getAttribute('disabled') || hasClass(node, 'disabled');
|
10
|
+
|
11
|
+
const setupRemoteFromTarget = function(target, httpRequestType, form = null) {
|
12
|
+
const httpUrl = target.getAttribute('href') || target.getAttribute('action');
|
13
|
+
|
14
|
+
if (!httpUrl) { throw new Error(`Turbograft developer error: You did not provide a URL ('${urlAttribute}' attribute) for data-tg-remote`); }
|
15
|
+
|
16
|
+
if (TurboGraft.getTGAttribute(target, "remote-once")) {
|
17
|
+
TurboGraft.removeTGAttribute(target, "remote-once");
|
18
|
+
TurboGraft.removeTGAttribute(target, "tg-remote");
|
19
|
+
}
|
20
|
+
|
21
|
+
const options = {
|
22
|
+
httpRequestType,
|
23
|
+
httpUrl,
|
24
|
+
fullRefresh: (TurboGraft.getTGAttribute(target, 'full-refresh') != null),
|
25
|
+
refreshOnSuccess: TurboGraft.getTGAttribute(target, 'refresh-on-success'),
|
26
|
+
refreshOnSuccessExcept: TurboGraft.getTGAttribute(target, 'full-refresh-on-success-except'),
|
27
|
+
refreshOnError: TurboGraft.getTGAttribute(target, 'refresh-on-error'),
|
28
|
+
refreshOnErrorExcept: TurboGraft.getTGAttribute(target, 'full-refresh-on-error-except')
|
29
|
+
};
|
30
|
+
|
31
|
+
return new TurboGraft.Remote(options, form, target);
|
32
|
+
};
|
33
|
+
|
34
|
+
TurboGraft.handlers.remoteMethodHandler = function(ev) {
|
35
|
+
const target = ev.clickTarget;
|
36
|
+
const httpRequestType = TurboGraft.getTGAttribute(target, 'tg-remote');
|
37
|
+
|
38
|
+
if (!httpRequestType) { return; }
|
39
|
+
ev.preventDefault();
|
40
|
+
|
41
|
+
const remote = setupRemoteFromTarget(target, httpRequestType);
|
42
|
+
remote.submit();
|
43
|
+
};
|
44
|
+
|
45
|
+
TurboGraft.handlers.remoteFormHandler = function(ev) {
|
46
|
+
const {
|
47
|
+
target
|
48
|
+
} = ev;
|
49
|
+
const method = target.getAttribute('method');
|
50
|
+
|
51
|
+
if (!TurboGraft.hasTGAttribute(target, 'tg-remote')) { return; }
|
52
|
+
ev.preventDefault();
|
53
|
+
|
54
|
+
const remote = setupRemoteFromTarget(target, method, target);
|
55
|
+
remote.submit();
|
56
|
+
};
|
57
|
+
|
58
|
+
const documentListenerForButtons = function(eventType, handler, useCapture) {
|
59
|
+
if (useCapture == null) { useCapture = false; }
|
60
|
+
return document.addEventListener(eventType, function(ev) {
|
61
|
+
let {
|
62
|
+
target
|
63
|
+
} = ev;
|
64
|
+
|
65
|
+
while ((target !== document) && (target != null)) {
|
66
|
+
if ((target.nodeName === "A") || (target.nodeName === "BUTTON")) {
|
67
|
+
var isNodeDisabled = nodeIsDisabled(target);
|
68
|
+
if (isNodeDisabled) { ev.preventDefault(); }
|
69
|
+
if (!isNodeDisabled) {
|
70
|
+
ev.clickTarget = target;
|
71
|
+
handler(ev);
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
target = target.parentNode;
|
77
|
+
}
|
78
|
+
});
|
79
|
+
};
|
80
|
+
|
81
|
+
documentListenerForButtons('click', TurboGraft.handlers.remoteMethodHandler, true);
|
82
|
+
|
83
|
+
document.addEventListener("submit", ev => TurboGraft.handlers.remoteFormHandler(ev));
|
@@ -0,0 +1,58 @@
|
|
1
|
+
/* The Link class derives from the ComponentUrl class, but is built from an
|
2
|
+
* existing link element. Provides verification functionality for Turbolinks
|
3
|
+
* to use in determining whether it should process the link when clicked.
|
4
|
+
*/
|
5
|
+
window.Link = class Link extends ComponentUrl {
|
6
|
+
static initClass() {
|
7
|
+
this.HTML_EXTENSIONS = ['html'];
|
8
|
+
}
|
9
|
+
|
10
|
+
static allowExtensions() {
|
11
|
+
var extension, extensions, i, len;
|
12
|
+
extensions = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
|
13
|
+
for (i = 0, len = extensions.length; i < len; i++) {
|
14
|
+
extension = extensions[i];
|
15
|
+
Link.HTML_EXTENSIONS.push(extension);
|
16
|
+
}
|
17
|
+
return Link.HTML_EXTENSIONS;
|
18
|
+
};
|
19
|
+
|
20
|
+
constructor(link) {
|
21
|
+
if (link.constructor === Link) {
|
22
|
+
return link;
|
23
|
+
}
|
24
|
+
super(link.href, link);
|
25
|
+
}
|
26
|
+
|
27
|
+
shouldIgnore() {
|
28
|
+
return this._crossOrigin() || this._anchored() || this._nonHtml() || this._optOut() || this._target();
|
29
|
+
};
|
30
|
+
|
31
|
+
_crossOrigin() {
|
32
|
+
return this.origin !== (new ComponentUrl).origin;
|
33
|
+
};
|
34
|
+
|
35
|
+
_anchored() {
|
36
|
+
var current;
|
37
|
+
return ((this.hash && this.withoutHash()) === (current = new ComponentUrl).withoutHash()) || (this.href === current.href + '#');
|
38
|
+
};
|
39
|
+
|
40
|
+
_nonHtml() {
|
41
|
+
return this.pathname.match(/\.[a-z]+$/g) && !this.pathname.match(new RegExp("\\.(?:" + (Link.HTML_EXTENSIONS.join('|')) + ")?$", 'g'));
|
42
|
+
};
|
43
|
+
|
44
|
+
_optOut() {
|
45
|
+
var ignore, link;
|
46
|
+
link = this.link;
|
47
|
+
while (!(ignore || link === document || link === null)) {
|
48
|
+
ignore = link.getAttribute('data-no-turbolink') != null;
|
49
|
+
link = link.parentNode;
|
50
|
+
}
|
51
|
+
return ignore;
|
52
|
+
};
|
53
|
+
|
54
|
+
_target() {
|
55
|
+
return this.link.target.length !== 0;
|
56
|
+
};
|
57
|
+
};
|
58
|
+
window.Link.initClass();
|
@@ -0,0 +1,105 @@
|
|
1
|
+
/*
|
2
|
+
* decaffeinate suggestions:
|
3
|
+
* DS101: Remove unnecessary use of Array.from
|
4
|
+
* DS102: Remove unnecessary code created because of implicit returns
|
5
|
+
* DS205: Consider reworking code to avoid use of IIFEs
|
6
|
+
* DS207: Consider shorter variations of null checks
|
7
|
+
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
8
|
+
*/
|
9
|
+
if (!window.Page) { window.Page = {}; }
|
10
|
+
|
11
|
+
Page.visit = function(url, opts) {
|
12
|
+
if (opts == null) { opts = {}; }
|
13
|
+
if (opts.reload) {
|
14
|
+
return window.location = url;
|
15
|
+
} else {
|
16
|
+
return Turbolinks.visit(url);
|
17
|
+
}
|
18
|
+
};
|
19
|
+
|
20
|
+
Page.refresh = function(options, callback) {
|
21
|
+
let xhr;
|
22
|
+
if (options == null) { options = {}; }
|
23
|
+
const newUrl = (() => {
|
24
|
+
if (options.url) {
|
25
|
+
return options.url;
|
26
|
+
} else if (options.queryParams) {
|
27
|
+
let paramString = $.param(options.queryParams);
|
28
|
+
if (paramString) { paramString = `?${paramString}`; }
|
29
|
+
return location.pathname + paramString;
|
30
|
+
} else {
|
31
|
+
return location.href;
|
32
|
+
}
|
33
|
+
})();
|
34
|
+
|
35
|
+
const turboOptions = {
|
36
|
+
partialReplace: true,
|
37
|
+
exceptKeys: options.exceptKeys,
|
38
|
+
onlyKeys: options.onlyKeys,
|
39
|
+
updatePushState: options.updatePushState,
|
40
|
+
callback
|
41
|
+
};
|
42
|
+
|
43
|
+
if ((xhr = options.response)) {
|
44
|
+
return Turbolinks.loadPage(null, xhr, turboOptions);
|
45
|
+
} else {
|
46
|
+
return Turbolinks.visit(newUrl, turboOptions);
|
47
|
+
}
|
48
|
+
};
|
49
|
+
|
50
|
+
Page.open = function() {
|
51
|
+
return window.open(...arguments);
|
52
|
+
};
|
53
|
+
|
54
|
+
// Providing hooks for objects to set up destructors:
|
55
|
+
let onReplaceCallbacks = [];
|
56
|
+
|
57
|
+
// e.g., Page.onReplace(node, unbindListenersFnc)
|
58
|
+
// unbindListenersFnc will be called if the node in question is partially replaced
|
59
|
+
// or if a full replace occurs. It will be called only once
|
60
|
+
Page.onReplace = function(node, callback) {
|
61
|
+
if (!node || !callback) { throw new Error("Page.onReplace: Node and callback must both be specified"); }
|
62
|
+
if (!isFunction(callback)) { throw new Error("Page.onReplace: Callback must be a function"); }
|
63
|
+
return onReplaceCallbacks.push({node, callback});
|
64
|
+
};
|
65
|
+
|
66
|
+
// option C from http://jsperf.com/alternative-isfunction-implementations
|
67
|
+
var isFunction = object => !!(object && object.constructor && object.call && object.apply);
|
68
|
+
|
69
|
+
// roughly based on http://davidwalsh.name/check-parent-node (note, OP is incorrect)
|
70
|
+
const contains = function(parentNode, childNode) {
|
71
|
+
if (parentNode.contains) {
|
72
|
+
return parentNode.contains(childNode);
|
73
|
+
} else { // old browser compatability
|
74
|
+
return !!((parentNode === childNode) || (parentNode.compareDocumentPosition(childNode) & Node.DOCUMENT_POSITION_CONTAINED_BY));
|
75
|
+
}
|
76
|
+
};
|
77
|
+
|
78
|
+
document.addEventListener('page:before-partial-replace', function(event) {
|
79
|
+
const replacedNodes = event.data;
|
80
|
+
|
81
|
+
const unprocessedOnReplaceCallbacks = [];
|
82
|
+
for (var entry of Array.from(onReplaceCallbacks)) {
|
83
|
+
var fired = false;
|
84
|
+
for (var replacedNode of Array.from(replacedNodes)) {
|
85
|
+
if (contains(replacedNode, entry.node)) {
|
86
|
+
entry.callback();
|
87
|
+
fired = true;
|
88
|
+
break;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
if (!fired) {
|
93
|
+
unprocessedOnReplaceCallbacks.push(entry);
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
return onReplaceCallbacks = unprocessedOnReplaceCallbacks;
|
98
|
+
});
|
99
|
+
|
100
|
+
document.addEventListener('page:before-replace', function(event) {
|
101
|
+
for (var entry of Array.from(onReplaceCallbacks)) {
|
102
|
+
entry.callback();
|
103
|
+
}
|
104
|
+
return onReplaceCallbacks = [];
|
105
|
+
});
|
@@ -0,0 +1,248 @@
|
|
1
|
+
/*
|
2
|
+
* decaffeinate suggestions:
|
3
|
+
* DS101: Remove unnecessary use of Array.from
|
4
|
+
* DS102: Remove unnecessary code created because of implicit returns
|
5
|
+
* DS205: Consider reworking code to avoid use of IIFEs
|
6
|
+
* DS207: Consider shorter variations of null checks
|
7
|
+
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
8
|
+
*/
|
9
|
+
TurboGraft.Remote = class Remote {
|
10
|
+
constructor(opts, form, target) {
|
11
|
+
|
12
|
+
this.onSuccess = this.onSuccess.bind(this);
|
13
|
+
this.onError = this.onError.bind(this);
|
14
|
+
this.opts = opts;
|
15
|
+
this.initiator = form || target;
|
16
|
+
|
17
|
+
this.actualRequestType = (this.opts.httpRequestType != null ? this.opts.httpRequestType.toLowerCase() : undefined) === 'get' ? 'GET' : 'POST';
|
18
|
+
this.useNativeEncoding = this.opts.useNativeEncoding;
|
19
|
+
|
20
|
+
this.formData = this.createPayload(form);
|
21
|
+
|
22
|
+
if (this.opts.refreshOnSuccess) { this.refreshOnSuccess = this.opts.refreshOnSuccess.split(" "); }
|
23
|
+
if (this.opts.refreshOnSuccessExcept) { this.refreshOnSuccessExcept = this.opts.refreshOnSuccessExcept.split(" "); }
|
24
|
+
if (this.opts.refreshOnError) { this.refreshOnError = this.opts.refreshOnError.split(" "); }
|
25
|
+
if (this.opts.refreshOnErrorExcept) { this.refreshOnErrorExcept = this.opts.refreshOnErrorExcept.split(" "); }
|
26
|
+
|
27
|
+
const xhr = new XMLHttpRequest;
|
28
|
+
if (this.actualRequestType === 'GET') {
|
29
|
+
const url = this.formData ? this.opts.httpUrl + `?${this.formData}` : this.opts.httpUrl;
|
30
|
+
xhr.open(this.actualRequestType, url, true);
|
31
|
+
} else {
|
32
|
+
xhr.open(this.actualRequestType, this.opts.httpUrl, true);
|
33
|
+
}
|
34
|
+
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
35
|
+
xhr.setRequestHeader('Accept', 'text/html, application/xhtml+xml, application/xml');
|
36
|
+
if (this.contentType) { xhr.setRequestHeader("Content-Type", this.contentType); }
|
37
|
+
xhr.setRequestHeader('X-XHR-Referer', document.location.href);
|
38
|
+
|
39
|
+
const csrfToken = CSRFToken.get().token;
|
40
|
+
if (csrfToken) { xhr.setRequestHeader('X-CSRF-Token', csrfToken); }
|
41
|
+
|
42
|
+
triggerEventFor('turbograft:remote:init', this.initiator, {xhr, initiator: this.initiator});
|
43
|
+
|
44
|
+
xhr.addEventListener('loadstart', () => {
|
45
|
+
return triggerEventFor('turbograft:remote:start', this.initiator,
|
46
|
+
{xhr});
|
47
|
+
});
|
48
|
+
|
49
|
+
xhr.addEventListener('error', this.onError);
|
50
|
+
xhr.addEventListener('load', event => {
|
51
|
+
if (xhr.status < 400) {
|
52
|
+
return this.onSuccess(event);
|
53
|
+
} else {
|
54
|
+
return this.onError(event);
|
55
|
+
}
|
56
|
+
});
|
57
|
+
|
58
|
+
xhr.addEventListener('loadend', () => {
|
59
|
+
if (typeof this.opts.done === 'function') {
|
60
|
+
this.opts.done();
|
61
|
+
}
|
62
|
+
return triggerEventFor('turbograft:remote:always', this.initiator, {
|
63
|
+
initiator: this.initiator,
|
64
|
+
xhr
|
65
|
+
}
|
66
|
+
);
|
67
|
+
});
|
68
|
+
|
69
|
+
this.xhr = xhr;
|
70
|
+
}
|
71
|
+
|
72
|
+
submit() {
|
73
|
+
return this.xhr.send(this.formData);
|
74
|
+
}
|
75
|
+
|
76
|
+
createPayload(form) {
|
77
|
+
let formData;
|
78
|
+
if (form) {
|
79
|
+
if (this.useNativeEncoding || (form.querySelectorAll("[type='file'][name]").length > 0)) {
|
80
|
+
formData = this.nativeEncodeForm(form);
|
81
|
+
} else { // for much smaller payloads
|
82
|
+
formData = this.uriEncodeForm(form);
|
83
|
+
}
|
84
|
+
} else {
|
85
|
+
formData = '';
|
86
|
+
}
|
87
|
+
|
88
|
+
if (!(formData instanceof FormData)) {
|
89
|
+
this.contentType = "application/x-www-form-urlencoded; charset=UTF-8";
|
90
|
+
if ((formData.indexOf("_method") === -1) && this.opts.httpRequestType && (this.actualRequestType !== 'GET')) { formData = this.formAppend(formData, "_method", this.opts.httpRequestType); }
|
91
|
+
}
|
92
|
+
|
93
|
+
return formData;
|
94
|
+
}
|
95
|
+
|
96
|
+
formAppend(uriEncoded, key, value) {
|
97
|
+
if (uriEncoded.length) { uriEncoded += "&"; }
|
98
|
+
return uriEncoded += `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
99
|
+
}
|
100
|
+
|
101
|
+
uriEncodeForm(form) {
|
102
|
+
let formData = "";
|
103
|
+
this._iterateOverFormInputs(form, input => {
|
104
|
+
return formData = this.formAppend(formData, input.name, input.value);
|
105
|
+
});
|
106
|
+
return formData;
|
107
|
+
}
|
108
|
+
|
109
|
+
formDataAppend(formData, input) {
|
110
|
+
if (input.type === 'file') {
|
111
|
+
for (var file of Array.from(input.files)) {
|
112
|
+
formData.append(input.name, file);
|
113
|
+
}
|
114
|
+
} else {
|
115
|
+
formData.append(input.name, input.value);
|
116
|
+
}
|
117
|
+
return formData;
|
118
|
+
}
|
119
|
+
|
120
|
+
nativeEncodeForm(form) {
|
121
|
+
let formData = new FormData;
|
122
|
+
this._iterateOverFormInputs(form, input => {
|
123
|
+
return formData = this.formDataAppend(formData, input);
|
124
|
+
});
|
125
|
+
return formData;
|
126
|
+
}
|
127
|
+
|
128
|
+
_iterateOverFormInputs(form, callback) {
|
129
|
+
const inputs = this._enabledInputs(form);
|
130
|
+
return (() => {
|
131
|
+
const result = [];
|
132
|
+
for (var input of Array.from(inputs)) {
|
133
|
+
var inputEnabled = !input.disabled;
|
134
|
+
var radioOrCheck = ((input.type === 'checkbox') || (input.type === 'radio'));
|
135
|
+
|
136
|
+
if (inputEnabled && input.name) {
|
137
|
+
if ((radioOrCheck && input.checked) || !radioOrCheck) {
|
138
|
+
result.push(callback(input));
|
139
|
+
} else {
|
140
|
+
result.push(undefined);
|
141
|
+
}
|
142
|
+
} else {
|
143
|
+
result.push(undefined);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
return result;
|
147
|
+
})();
|
148
|
+
}
|
149
|
+
|
150
|
+
_enabledInputs(form) {
|
151
|
+
const selector = "input:not([type='reset']):not([type='button']):not([type='submit']):not([type='image']), select, textarea";
|
152
|
+
const inputs = Array.prototype.slice.call(form.querySelectorAll(selector));
|
153
|
+
const disabledNodes = Array.prototype.slice.call(TurboGraft.querySelectorAllTGAttribute(form, 'tg-remote-noserialize'));
|
154
|
+
|
155
|
+
if (!disabledNodes.length) { return inputs; }
|
156
|
+
|
157
|
+
let disabledInputs = disabledNodes;
|
158
|
+
for (var node of Array.from(disabledNodes)) {
|
159
|
+
disabledInputs = disabledInputs.concat(Array.prototype.slice.call(node.querySelectorAll(selector)));
|
160
|
+
}
|
161
|
+
|
162
|
+
const enabledInputs = [];
|
163
|
+
for (var input of Array.from(inputs)) {
|
164
|
+
if (disabledInputs.indexOf(input) < 0) {
|
165
|
+
enabledInputs.push(input);
|
166
|
+
}
|
167
|
+
}
|
168
|
+
return enabledInputs;
|
169
|
+
}
|
170
|
+
|
171
|
+
onSuccess(ev) {
|
172
|
+
let redirect;
|
173
|
+
if (typeof this.opts.success === 'function') {
|
174
|
+
this.opts.success();
|
175
|
+
}
|
176
|
+
|
177
|
+
const xhr = ev.target;
|
178
|
+
triggerEventFor('turbograft:remote:success', this.initiator, {
|
179
|
+
initiator: this.initiator,
|
180
|
+
xhr
|
181
|
+
}
|
182
|
+
);
|
183
|
+
|
184
|
+
if (redirect = xhr.getResponseHeader('X-Next-Redirect')) {
|
185
|
+
Page.visit(redirect, {reload: true});
|
186
|
+
return;
|
187
|
+
}
|
188
|
+
|
189
|
+
if (!TurboGraft.hasTGAttribute(this.initiator, 'tg-remote-norefresh')) {
|
190
|
+
if (this.opts.fullRefresh && this.refreshOnSuccess) {
|
191
|
+
return Page.refresh({onlyKeys: this.refreshOnSuccess});
|
192
|
+
} else if (this.opts.fullRefresh) {
|
193
|
+
return Page.refresh();
|
194
|
+
} else if (this.refreshOnSuccess) {
|
195
|
+
return Page.refresh({
|
196
|
+
response: xhr,
|
197
|
+
onlyKeys: this.refreshOnSuccess
|
198
|
+
});
|
199
|
+
} else if (this.refreshOnSuccessExcept) {
|
200
|
+
return Page.refresh({
|
201
|
+
response: xhr,
|
202
|
+
exceptKeys: this.refreshOnSuccessExcept
|
203
|
+
});
|
204
|
+
} else {
|
205
|
+
return Page.refresh({
|
206
|
+
response: xhr
|
207
|
+
});
|
208
|
+
}
|
209
|
+
}
|
210
|
+
}
|
211
|
+
|
212
|
+
onError(ev) {
|
213
|
+
if (typeof this.opts.fail === 'function') {
|
214
|
+
this.opts.fail();
|
215
|
+
}
|
216
|
+
|
217
|
+
const xhr = ev.target;
|
218
|
+
triggerEventFor('turbograft:remote:fail', this.initiator, {
|
219
|
+
initiator: this.initiator,
|
220
|
+
xhr
|
221
|
+
}
|
222
|
+
);
|
223
|
+
|
224
|
+
if (TurboGraft.hasTGAttribute(this.initiator, 'tg-remote-norefresh')) {
|
225
|
+
return triggerEventFor('turbograft:remote:fail:unhandled', this.initiator,
|
226
|
+
{xhr});
|
227
|
+
} else {
|
228
|
+
if (this.opts.fullRefresh && this.refreshOnError) {
|
229
|
+
return Page.refresh({onlyKeys: this.refreshOnError});
|
230
|
+
} else if (this.opts.fullRefresh) {
|
231
|
+
return Page.refresh();
|
232
|
+
} else if (this.refreshOnError) {
|
233
|
+
return Page.refresh({
|
234
|
+
response: xhr,
|
235
|
+
onlyKeys: this.refreshOnError
|
236
|
+
});
|
237
|
+
} else if (this.refreshOnErrorExcept) {
|
238
|
+
return Page.refresh({
|
239
|
+
response: xhr,
|
240
|
+
exceptKeys: this.refreshOnErrorExcept
|
241
|
+
});
|
242
|
+
} else {
|
243
|
+
return triggerEventFor('turbograft:remote:fail:unhandled', this.initiator,
|
244
|
+
{xhr});
|
245
|
+
}
|
246
|
+
}
|
247
|
+
}
|
248
|
+
};
|