@hotwired/turbo 7.2.0-rc.2 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/turbo.es2017-esm.js +41 -40
- package/dist/turbo.es2017-umd.js +41 -40
- package/dist/types/core/drive/navigator.d.ts +4 -4
- package/dist/types/core/drive/visit.d.ts +1 -4
- package/dist/types/core/frames/frame_controller.d.ts +2 -1
- package/dist/types/core/frames/frame_redirector.d.ts +2 -1
- package/dist/types/core/index.d.ts +2 -2
- package/dist/types/core/native/adapter.d.ts +1 -1
- package/dist/types/core/native/browser_adapter.d.ts +1 -1
- package/dist/types/core/session.d.ts +6 -6
- package/dist/types/core/types.d.ts +0 -4
- package/dist/types/http/fetch_request.d.ts +3 -2
- package/dist/types/tests/helpers/page.d.ts +2 -0
- package/dist/types/tests/unit/deprecated_adapter_support_test.d.ts +1 -1
- package/dist/types/tests/unit/export_tests.d.ts +5 -0
- package/dist/types/tests/unit/index.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/turbo.es2017-esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Turbo 7.2.0
|
|
3
|
-
Copyright © 2022
|
|
2
|
+
Turbo 7.2.0
|
|
3
|
+
Copyright © 2022 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
(function () {
|
|
6
6
|
if (window.Reflect === undefined ||
|
|
@@ -1625,7 +1625,6 @@ const defaultOptions = {
|
|
|
1625
1625
|
updateHistory: true,
|
|
1626
1626
|
shouldCacheSnapshot: true,
|
|
1627
1627
|
acceptsStreamResponse: false,
|
|
1628
|
-
initiator: document.documentElement,
|
|
1629
1628
|
};
|
|
1630
1629
|
var SystemStatusCode;
|
|
1631
1630
|
(function (SystemStatusCode) {
|
|
@@ -1635,6 +1634,7 @@ var SystemStatusCode;
|
|
|
1635
1634
|
})(SystemStatusCode || (SystemStatusCode = {}));
|
|
1636
1635
|
class Visit {
|
|
1637
1636
|
constructor(delegate, location, restorationIdentifier, options = {}) {
|
|
1637
|
+
this.identifier = uuid();
|
|
1638
1638
|
this.timingMetrics = {};
|
|
1639
1639
|
this.followedRedirect = false;
|
|
1640
1640
|
this.historyChanged = false;
|
|
@@ -1646,8 +1646,7 @@ class Visit {
|
|
|
1646
1646
|
this.delegate = delegate;
|
|
1647
1647
|
this.location = location;
|
|
1648
1648
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
|
1649
|
-
|
|
1650
|
-
const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, initiator, } = Object.assign(Object.assign({}, defaultOptions), options);
|
|
1649
|
+
const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
|
|
1651
1650
|
this.action = action;
|
|
1652
1651
|
this.historyChanged = historyChanged;
|
|
1653
1652
|
this.referrer = referrer;
|
|
@@ -1660,7 +1659,6 @@ class Visit {
|
|
|
1660
1659
|
this.scrolled = !willRender;
|
|
1661
1660
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
|
1662
1661
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
|
1663
|
-
this.initiator = initiator;
|
|
1664
1662
|
}
|
|
1665
1663
|
get adapter() {
|
|
1666
1664
|
return this.delegate.adapter;
|
|
@@ -1692,7 +1690,6 @@ class Visit {
|
|
|
1692
1690
|
}
|
|
1693
1691
|
this.cancelRender();
|
|
1694
1692
|
this.state = VisitState.canceled;
|
|
1695
|
-
this.resolvingFunctions.reject();
|
|
1696
1693
|
}
|
|
1697
1694
|
}
|
|
1698
1695
|
complete() {
|
|
@@ -1704,14 +1701,12 @@ class Visit {
|
|
|
1704
1701
|
this.adapter.visitCompleted(this);
|
|
1705
1702
|
this.delegate.visitCompleted(this);
|
|
1706
1703
|
}
|
|
1707
|
-
this.resolvingFunctions.resolve();
|
|
1708
1704
|
}
|
|
1709
1705
|
}
|
|
1710
1706
|
fail() {
|
|
1711
1707
|
if (this.state == VisitState.started) {
|
|
1712
1708
|
this.state = VisitState.failed;
|
|
1713
1709
|
this.adapter.visitFailed(this);
|
|
1714
|
-
this.resolvingFunctions.reject();
|
|
1715
1710
|
}
|
|
1716
1711
|
}
|
|
1717
1712
|
changeHistory() {
|
|
@@ -1728,7 +1723,7 @@ class Visit {
|
|
|
1728
1723
|
this.simulateRequest();
|
|
1729
1724
|
}
|
|
1730
1725
|
else if (this.shouldIssueRequest() && !this.request) {
|
|
1731
|
-
this.request = new FetchRequest(this, FetchMethod.get, this.location
|
|
1726
|
+
this.request = new FetchRequest(this, FetchMethod.get, this.location);
|
|
1732
1727
|
this.request.perform();
|
|
1733
1728
|
}
|
|
1734
1729
|
}
|
|
@@ -1974,7 +1969,7 @@ class BrowserAdapter {
|
|
|
1974
1969
|
this.session = session;
|
|
1975
1970
|
}
|
|
1976
1971
|
visitProposedToLocation(location, options) {
|
|
1977
|
-
|
|
1972
|
+
this.navigator.startVisit(location, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options);
|
|
1978
1973
|
}
|
|
1979
1974
|
visitStarted(visit) {
|
|
1980
1975
|
this.location = visit.location;
|
|
@@ -2099,8 +2094,8 @@ class FrameRedirector {
|
|
|
2099
2094
|
this.linkClickObserver.stop();
|
|
2100
2095
|
this.formSubmitObserver.stop();
|
|
2101
2096
|
}
|
|
2102
|
-
willFollowLinkToLocation(element) {
|
|
2103
|
-
return this.shouldRedirect(element);
|
|
2097
|
+
willFollowLinkToLocation(element, location, event) {
|
|
2098
|
+
return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, location, event);
|
|
2104
2099
|
}
|
|
2105
2100
|
followedLinkToLocation(element, url) {
|
|
2106
2101
|
const frame = this.findFrameElement(element);
|
|
@@ -2119,6 +2114,14 @@ class FrameRedirector {
|
|
|
2119
2114
|
frame.delegate.formSubmitted(element, submitter);
|
|
2120
2115
|
}
|
|
2121
2116
|
}
|
|
2117
|
+
frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
|
|
2118
|
+
const event = dispatch("turbo:click", {
|
|
2119
|
+
target,
|
|
2120
|
+
detail: { url, originalEvent },
|
|
2121
|
+
cancelable: true,
|
|
2122
|
+
});
|
|
2123
|
+
return !event.defaultPrevented;
|
|
2124
|
+
}
|
|
2122
2125
|
shouldSubmit(form, submitter) {
|
|
2123
2126
|
var _a;
|
|
2124
2127
|
const action = getAction(form, submitter);
|
|
@@ -2233,25 +2236,20 @@ class Navigator {
|
|
|
2233
2236
|
this.delegate = delegate;
|
|
2234
2237
|
}
|
|
2235
2238
|
proposeVisit(location, options = {}) {
|
|
2236
|
-
if (this.delegate.
|
|
2239
|
+
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
|
2237
2240
|
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
|
|
2238
|
-
|
|
2241
|
+
this.delegate.visitProposedToLocation(location, options);
|
|
2239
2242
|
}
|
|
2240
2243
|
else {
|
|
2241
2244
|
window.location.href = location.toString();
|
|
2242
|
-
return Promise.resolve();
|
|
2243
2245
|
}
|
|
2244
2246
|
}
|
|
2245
|
-
else {
|
|
2246
|
-
return Promise.reject();
|
|
2247
|
-
}
|
|
2248
2247
|
}
|
|
2249
2248
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
|
2250
2249
|
this.lastVisit = this.currentVisit;
|
|
2251
2250
|
this.stop();
|
|
2252
2251
|
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
|
|
2253
2252
|
this.currentVisit.start();
|
|
2254
|
-
return this.currentVisit.promise;
|
|
2255
2253
|
}
|
|
2256
2254
|
submitForm(form, submitter) {
|
|
2257
2255
|
this.stop();
|
|
@@ -2872,10 +2870,10 @@ class Session {
|
|
|
2872
2870
|
const frameElement = options.frame ? document.getElementById(options.frame) : null;
|
|
2873
2871
|
if (frameElement instanceof FrameElement) {
|
|
2874
2872
|
frameElement.src = location.toString();
|
|
2875
|
-
|
|
2873
|
+
frameElement.loaded;
|
|
2876
2874
|
}
|
|
2877
2875
|
else {
|
|
2878
|
-
|
|
2876
|
+
this.navigator.proposeVisit(expandURL(location), options);
|
|
2879
2877
|
}
|
|
2880
2878
|
}
|
|
2881
2879
|
connectStreamSource(source) {
|
|
@@ -2930,15 +2928,14 @@ class Session {
|
|
|
2930
2928
|
followedLinkToLocation(link, location) {
|
|
2931
2929
|
const action = this.getActionForLink(link);
|
|
2932
2930
|
const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
|
|
2933
|
-
this.visit(location.href, { action, acceptsStreamResponse
|
|
2931
|
+
this.visit(location.href, { action, acceptsStreamResponse });
|
|
2934
2932
|
}
|
|
2935
|
-
|
|
2936
|
-
return
|
|
2937
|
-
this.applicationAllowsVisitingLocation(location, options));
|
|
2933
|
+
allowsVisitingLocationWithAction(location, action) {
|
|
2934
|
+
return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
|
|
2938
2935
|
}
|
|
2939
2936
|
visitProposedToLocation(location, options) {
|
|
2940
2937
|
extendURLWithDeprecatedProperties(location);
|
|
2941
|
-
|
|
2938
|
+
this.adapter.visitProposedToLocation(location, options);
|
|
2942
2939
|
}
|
|
2943
2940
|
visitStarted(visit) {
|
|
2944
2941
|
if (!visit.acceptsStreamResponse) {
|
|
@@ -2946,7 +2943,7 @@ class Session {
|
|
|
2946
2943
|
}
|
|
2947
2944
|
extendURLWithDeprecatedProperties(visit.location);
|
|
2948
2945
|
if (!visit.silent) {
|
|
2949
|
-
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action
|
|
2946
|
+
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
|
2950
2947
|
}
|
|
2951
2948
|
}
|
|
2952
2949
|
visitCompleted(visit) {
|
|
@@ -3014,8 +3011,8 @@ class Session {
|
|
|
3014
3011
|
const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
|
|
3015
3012
|
return !event.defaultPrevented;
|
|
3016
3013
|
}
|
|
3017
|
-
applicationAllowsVisitingLocation(location
|
|
3018
|
-
const event = this.notifyApplicationBeforeVisitingLocation(location
|
|
3014
|
+
applicationAllowsVisitingLocation(location) {
|
|
3015
|
+
const event = this.notifyApplicationBeforeVisitingLocation(location);
|
|
3019
3016
|
return !event.defaultPrevented;
|
|
3020
3017
|
}
|
|
3021
3018
|
notifyApplicationAfterClickingLinkToLocation(link, location, event) {
|
|
@@ -3025,18 +3022,14 @@ class Session {
|
|
|
3025
3022
|
cancelable: true,
|
|
3026
3023
|
});
|
|
3027
3024
|
}
|
|
3028
|
-
notifyApplicationBeforeVisitingLocation(location
|
|
3025
|
+
notifyApplicationBeforeVisitingLocation(location) {
|
|
3029
3026
|
return dispatch("turbo:before-visit", {
|
|
3030
|
-
target: element,
|
|
3031
3027
|
detail: { url: location.href },
|
|
3032
3028
|
cancelable: true,
|
|
3033
3029
|
});
|
|
3034
3030
|
}
|
|
3035
|
-
notifyApplicationAfterVisitingLocation(location, action
|
|
3036
|
-
return dispatch("turbo:visit", {
|
|
3037
|
-
target: element,
|
|
3038
|
-
detail: { url: location.href, action },
|
|
3039
|
-
});
|
|
3031
|
+
notifyApplicationAfterVisitingLocation(location, action) {
|
|
3032
|
+
return dispatch("turbo:visit", { detail: { url: location.href, action } });
|
|
3040
3033
|
}
|
|
3041
3034
|
notifyApplicationBeforeCachingSnapshot() {
|
|
3042
3035
|
return dispatch("turbo:before-cache");
|
|
@@ -3181,7 +3174,7 @@ function registerAdapter(adapter) {
|
|
|
3181
3174
|
session.registerAdapter(adapter);
|
|
3182
3175
|
}
|
|
3183
3176
|
function visit(location, options) {
|
|
3184
|
-
|
|
3177
|
+
session.visit(location, options);
|
|
3185
3178
|
}
|
|
3186
3179
|
function connectStreamSource(source) {
|
|
3187
3180
|
session.connectStreamSource(source);
|
|
@@ -3357,8 +3350,8 @@ class FrameController {
|
|
|
3357
3350
|
if (frame)
|
|
3358
3351
|
form.setAttribute("data-turbo-frame", frame.id);
|
|
3359
3352
|
}
|
|
3360
|
-
willFollowLinkToLocation(element) {
|
|
3361
|
-
return this.shouldInterceptNavigation(element);
|
|
3353
|
+
willFollowLinkToLocation(element, location, event) {
|
|
3354
|
+
return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, location, event);
|
|
3362
3355
|
}
|
|
3363
3356
|
followedLinkToLocation(element, location) {
|
|
3364
3357
|
this.navigateFrame(element, location.href);
|
|
@@ -3611,6 +3604,14 @@ class FrameController {
|
|
|
3611
3604
|
const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
|
|
3612
3605
|
return expandURL(root);
|
|
3613
3606
|
}
|
|
3607
|
+
frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
|
|
3608
|
+
const event = dispatch("turbo:click", {
|
|
3609
|
+
target,
|
|
3610
|
+
detail: { url, originalEvent },
|
|
3611
|
+
cancelable: true,
|
|
3612
|
+
});
|
|
3613
|
+
return !event.defaultPrevented;
|
|
3614
|
+
}
|
|
3614
3615
|
isIgnoringChangesTo(attributeName) {
|
|
3615
3616
|
return this.ignoredAttributes.has(attributeName);
|
|
3616
3617
|
}
|
package/dist/turbo.es2017-umd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Turbo 7.2.0
|
|
3
|
-
Copyright © 2022
|
|
2
|
+
Turbo 7.2.0
|
|
3
|
+
Copyright © 2022 37signals LLC
|
|
4
4
|
*/
|
|
5
5
|
(function (global, factory) {
|
|
6
6
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
@@ -1631,7 +1631,6 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1631
1631
|
updateHistory: true,
|
|
1632
1632
|
shouldCacheSnapshot: true,
|
|
1633
1633
|
acceptsStreamResponse: false,
|
|
1634
|
-
initiator: document.documentElement,
|
|
1635
1634
|
};
|
|
1636
1635
|
var SystemStatusCode;
|
|
1637
1636
|
(function (SystemStatusCode) {
|
|
@@ -1641,6 +1640,7 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1641
1640
|
})(SystemStatusCode || (SystemStatusCode = {}));
|
|
1642
1641
|
class Visit {
|
|
1643
1642
|
constructor(delegate, location, restorationIdentifier, options = {}) {
|
|
1643
|
+
this.identifier = uuid();
|
|
1644
1644
|
this.timingMetrics = {};
|
|
1645
1645
|
this.followedRedirect = false;
|
|
1646
1646
|
this.historyChanged = false;
|
|
@@ -1652,8 +1652,7 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1652
1652
|
this.delegate = delegate;
|
|
1653
1653
|
this.location = location;
|
|
1654
1654
|
this.restorationIdentifier = restorationIdentifier || uuid();
|
|
1655
|
-
|
|
1656
|
-
const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, initiator, } = Object.assign(Object.assign({}, defaultOptions), options);
|
|
1655
|
+
const { action, historyChanged, referrer, snapshotHTML, response, visitCachedSnapshot, willRender, updateHistory, shouldCacheSnapshot, acceptsStreamResponse, } = Object.assign(Object.assign({}, defaultOptions), options);
|
|
1657
1656
|
this.action = action;
|
|
1658
1657
|
this.historyChanged = historyChanged;
|
|
1659
1658
|
this.referrer = referrer;
|
|
@@ -1666,7 +1665,6 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1666
1665
|
this.scrolled = !willRender;
|
|
1667
1666
|
this.shouldCacheSnapshot = shouldCacheSnapshot;
|
|
1668
1667
|
this.acceptsStreamResponse = acceptsStreamResponse;
|
|
1669
|
-
this.initiator = initiator;
|
|
1670
1668
|
}
|
|
1671
1669
|
get adapter() {
|
|
1672
1670
|
return this.delegate.adapter;
|
|
@@ -1698,7 +1696,6 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1698
1696
|
}
|
|
1699
1697
|
this.cancelRender();
|
|
1700
1698
|
this.state = VisitState.canceled;
|
|
1701
|
-
this.resolvingFunctions.reject();
|
|
1702
1699
|
}
|
|
1703
1700
|
}
|
|
1704
1701
|
complete() {
|
|
@@ -1710,14 +1707,12 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1710
1707
|
this.adapter.visitCompleted(this);
|
|
1711
1708
|
this.delegate.visitCompleted(this);
|
|
1712
1709
|
}
|
|
1713
|
-
this.resolvingFunctions.resolve();
|
|
1714
1710
|
}
|
|
1715
1711
|
}
|
|
1716
1712
|
fail() {
|
|
1717
1713
|
if (this.state == VisitState.started) {
|
|
1718
1714
|
this.state = VisitState.failed;
|
|
1719
1715
|
this.adapter.visitFailed(this);
|
|
1720
|
-
this.resolvingFunctions.reject();
|
|
1721
1716
|
}
|
|
1722
1717
|
}
|
|
1723
1718
|
changeHistory() {
|
|
@@ -1734,7 +1729,7 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1734
1729
|
this.simulateRequest();
|
|
1735
1730
|
}
|
|
1736
1731
|
else if (this.shouldIssueRequest() && !this.request) {
|
|
1737
|
-
this.request = new FetchRequest(this, FetchMethod.get, this.location
|
|
1732
|
+
this.request = new FetchRequest(this, FetchMethod.get, this.location);
|
|
1738
1733
|
this.request.perform();
|
|
1739
1734
|
}
|
|
1740
1735
|
}
|
|
@@ -1980,7 +1975,7 @@ Copyright © 2022 Basecamp, LLC
|
|
|
1980
1975
|
this.session = session;
|
|
1981
1976
|
}
|
|
1982
1977
|
visitProposedToLocation(location, options) {
|
|
1983
|
-
|
|
1978
|
+
this.navigator.startVisit(location, (options === null || options === void 0 ? void 0 : options.restorationIdentifier) || uuid(), options);
|
|
1984
1979
|
}
|
|
1985
1980
|
visitStarted(visit) {
|
|
1986
1981
|
this.location = visit.location;
|
|
@@ -2105,8 +2100,8 @@ Copyright © 2022 Basecamp, LLC
|
|
|
2105
2100
|
this.linkClickObserver.stop();
|
|
2106
2101
|
this.formSubmitObserver.stop();
|
|
2107
2102
|
}
|
|
2108
|
-
willFollowLinkToLocation(element) {
|
|
2109
|
-
return this.shouldRedirect(element);
|
|
2103
|
+
willFollowLinkToLocation(element, location, event) {
|
|
2104
|
+
return this.shouldRedirect(element) && this.frameAllowsVisitingLocation(element, location, event);
|
|
2110
2105
|
}
|
|
2111
2106
|
followedLinkToLocation(element, url) {
|
|
2112
2107
|
const frame = this.findFrameElement(element);
|
|
@@ -2125,6 +2120,14 @@ Copyright © 2022 Basecamp, LLC
|
|
|
2125
2120
|
frame.delegate.formSubmitted(element, submitter);
|
|
2126
2121
|
}
|
|
2127
2122
|
}
|
|
2123
|
+
frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
|
|
2124
|
+
const event = dispatch("turbo:click", {
|
|
2125
|
+
target,
|
|
2126
|
+
detail: { url, originalEvent },
|
|
2127
|
+
cancelable: true,
|
|
2128
|
+
});
|
|
2129
|
+
return !event.defaultPrevented;
|
|
2130
|
+
}
|
|
2128
2131
|
shouldSubmit(form, submitter) {
|
|
2129
2132
|
var _a;
|
|
2130
2133
|
const action = getAction(form, submitter);
|
|
@@ -2239,25 +2242,20 @@ Copyright © 2022 Basecamp, LLC
|
|
|
2239
2242
|
this.delegate = delegate;
|
|
2240
2243
|
}
|
|
2241
2244
|
proposeVisit(location, options = {}) {
|
|
2242
|
-
if (this.delegate.
|
|
2245
|
+
if (this.delegate.allowsVisitingLocationWithAction(location, options.action)) {
|
|
2243
2246
|
if (locationIsVisitable(location, this.view.snapshot.rootLocation)) {
|
|
2244
|
-
|
|
2247
|
+
this.delegate.visitProposedToLocation(location, options);
|
|
2245
2248
|
}
|
|
2246
2249
|
else {
|
|
2247
2250
|
window.location.href = location.toString();
|
|
2248
|
-
return Promise.resolve();
|
|
2249
2251
|
}
|
|
2250
2252
|
}
|
|
2251
|
-
else {
|
|
2252
|
-
return Promise.reject();
|
|
2253
|
-
}
|
|
2254
2253
|
}
|
|
2255
2254
|
startVisit(locatable, restorationIdentifier, options = {}) {
|
|
2256
2255
|
this.lastVisit = this.currentVisit;
|
|
2257
2256
|
this.stop();
|
|
2258
2257
|
this.currentVisit = new Visit(this, expandURL(locatable), restorationIdentifier, Object.assign({ referrer: this.location }, options));
|
|
2259
2258
|
this.currentVisit.start();
|
|
2260
|
-
return this.currentVisit.promise;
|
|
2261
2259
|
}
|
|
2262
2260
|
submitForm(form, submitter) {
|
|
2263
2261
|
this.stop();
|
|
@@ -2878,10 +2876,10 @@ Copyright © 2022 Basecamp, LLC
|
|
|
2878
2876
|
const frameElement = options.frame ? document.getElementById(options.frame) : null;
|
|
2879
2877
|
if (frameElement instanceof FrameElement) {
|
|
2880
2878
|
frameElement.src = location.toString();
|
|
2881
|
-
|
|
2879
|
+
frameElement.loaded;
|
|
2882
2880
|
}
|
|
2883
2881
|
else {
|
|
2884
|
-
|
|
2882
|
+
this.navigator.proposeVisit(expandURL(location), options);
|
|
2885
2883
|
}
|
|
2886
2884
|
}
|
|
2887
2885
|
connectStreamSource(source) {
|
|
@@ -2936,15 +2934,14 @@ Copyright © 2022 Basecamp, LLC
|
|
|
2936
2934
|
followedLinkToLocation(link, location) {
|
|
2937
2935
|
const action = this.getActionForLink(link);
|
|
2938
2936
|
const acceptsStreamResponse = link.hasAttribute("data-turbo-stream");
|
|
2939
|
-
this.visit(location.href, { action, acceptsStreamResponse
|
|
2937
|
+
this.visit(location.href, { action, acceptsStreamResponse });
|
|
2940
2938
|
}
|
|
2941
|
-
|
|
2942
|
-
return
|
|
2943
|
-
this.applicationAllowsVisitingLocation(location, options));
|
|
2939
|
+
allowsVisitingLocationWithAction(location, action) {
|
|
2940
|
+
return this.locationWithActionIsSamePage(location, action) || this.applicationAllowsVisitingLocation(location);
|
|
2944
2941
|
}
|
|
2945
2942
|
visitProposedToLocation(location, options) {
|
|
2946
2943
|
extendURLWithDeprecatedProperties(location);
|
|
2947
|
-
|
|
2944
|
+
this.adapter.visitProposedToLocation(location, options);
|
|
2948
2945
|
}
|
|
2949
2946
|
visitStarted(visit) {
|
|
2950
2947
|
if (!visit.acceptsStreamResponse) {
|
|
@@ -2952,7 +2949,7 @@ Copyright © 2022 Basecamp, LLC
|
|
|
2952
2949
|
}
|
|
2953
2950
|
extendURLWithDeprecatedProperties(visit.location);
|
|
2954
2951
|
if (!visit.silent) {
|
|
2955
|
-
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action
|
|
2952
|
+
this.notifyApplicationAfterVisitingLocation(visit.location, visit.action);
|
|
2956
2953
|
}
|
|
2957
2954
|
}
|
|
2958
2955
|
visitCompleted(visit) {
|
|
@@ -3020,8 +3017,8 @@ Copyright © 2022 Basecamp, LLC
|
|
|
3020
3017
|
const event = this.notifyApplicationAfterClickingLinkToLocation(link, location, ev);
|
|
3021
3018
|
return !event.defaultPrevented;
|
|
3022
3019
|
}
|
|
3023
|
-
applicationAllowsVisitingLocation(location
|
|
3024
|
-
const event = this.notifyApplicationBeforeVisitingLocation(location
|
|
3020
|
+
applicationAllowsVisitingLocation(location) {
|
|
3021
|
+
const event = this.notifyApplicationBeforeVisitingLocation(location);
|
|
3025
3022
|
return !event.defaultPrevented;
|
|
3026
3023
|
}
|
|
3027
3024
|
notifyApplicationAfterClickingLinkToLocation(link, location, event) {
|
|
@@ -3031,18 +3028,14 @@ Copyright © 2022 Basecamp, LLC
|
|
|
3031
3028
|
cancelable: true,
|
|
3032
3029
|
});
|
|
3033
3030
|
}
|
|
3034
|
-
notifyApplicationBeforeVisitingLocation(location
|
|
3031
|
+
notifyApplicationBeforeVisitingLocation(location) {
|
|
3035
3032
|
return dispatch("turbo:before-visit", {
|
|
3036
|
-
target: element,
|
|
3037
3033
|
detail: { url: location.href },
|
|
3038
3034
|
cancelable: true,
|
|
3039
3035
|
});
|
|
3040
3036
|
}
|
|
3041
|
-
notifyApplicationAfterVisitingLocation(location, action
|
|
3042
|
-
return dispatch("turbo:visit", {
|
|
3043
|
-
target: element,
|
|
3044
|
-
detail: { url: location.href, action },
|
|
3045
|
-
});
|
|
3037
|
+
notifyApplicationAfterVisitingLocation(location, action) {
|
|
3038
|
+
return dispatch("turbo:visit", { detail: { url: location.href, action } });
|
|
3046
3039
|
}
|
|
3047
3040
|
notifyApplicationBeforeCachingSnapshot() {
|
|
3048
3041
|
return dispatch("turbo:before-cache");
|
|
@@ -3187,7 +3180,7 @@ Copyright © 2022 Basecamp, LLC
|
|
|
3187
3180
|
session.registerAdapter(adapter);
|
|
3188
3181
|
}
|
|
3189
3182
|
function visit(location, options) {
|
|
3190
|
-
|
|
3183
|
+
session.visit(location, options);
|
|
3191
3184
|
}
|
|
3192
3185
|
function connectStreamSource(source) {
|
|
3193
3186
|
session.connectStreamSource(source);
|
|
@@ -3363,8 +3356,8 @@ Copyright © 2022 Basecamp, LLC
|
|
|
3363
3356
|
if (frame)
|
|
3364
3357
|
form.setAttribute("data-turbo-frame", frame.id);
|
|
3365
3358
|
}
|
|
3366
|
-
willFollowLinkToLocation(element) {
|
|
3367
|
-
return this.shouldInterceptNavigation(element);
|
|
3359
|
+
willFollowLinkToLocation(element, location, event) {
|
|
3360
|
+
return this.shouldInterceptNavigation(element) && this.frameAllowsVisitingLocation(element, location, event);
|
|
3368
3361
|
}
|
|
3369
3362
|
followedLinkToLocation(element, location) {
|
|
3370
3363
|
this.navigateFrame(element, location.href);
|
|
@@ -3617,6 +3610,14 @@ Copyright © 2022 Basecamp, LLC
|
|
|
3617
3610
|
const root = (_a = meta === null || meta === void 0 ? void 0 : meta.content) !== null && _a !== void 0 ? _a : "/";
|
|
3618
3611
|
return expandURL(root);
|
|
3619
3612
|
}
|
|
3613
|
+
frameAllowsVisitingLocation(target, { href: url }, originalEvent) {
|
|
3614
|
+
const event = dispatch("turbo:click", {
|
|
3615
|
+
target,
|
|
3616
|
+
detail: { url, originalEvent },
|
|
3617
|
+
cancelable: true,
|
|
3618
|
+
});
|
|
3619
|
+
return !event.defaultPrevented;
|
|
3620
|
+
}
|
|
3620
3621
|
isIgnoringChangesTo(attributeName) {
|
|
3621
3622
|
return this.ignoredAttributes.has(attributeName);
|
|
3622
3623
|
}
|
|
@@ -4,8 +4,8 @@ import { FormSubmission } from "./form_submission";
|
|
|
4
4
|
import { Locatable } from "../url";
|
|
5
5
|
import { Visit, VisitDelegate, VisitOptions } from "./visit";
|
|
6
6
|
export declare type NavigatorDelegate = VisitDelegate & {
|
|
7
|
-
|
|
8
|
-
visitProposedToLocation(location: URL, options: Partial<VisitOptions>):
|
|
7
|
+
allowsVisitingLocationWithAction(location: URL, action?: Action): boolean;
|
|
8
|
+
visitProposedToLocation(location: URL, options: Partial<VisitOptions>): void;
|
|
9
9
|
notifyApplicationAfterVisitingSamePageLocation(oldURL: URL, newURL: URL): void;
|
|
10
10
|
};
|
|
11
11
|
export declare class Navigator {
|
|
@@ -14,8 +14,8 @@ export declare class Navigator {
|
|
|
14
14
|
currentVisit?: Visit;
|
|
15
15
|
lastVisit?: Visit;
|
|
16
16
|
constructor(delegate: NavigatorDelegate);
|
|
17
|
-
proposeVisit(location: URL, options?: Partial<VisitOptions>):
|
|
18
|
-
startVisit(locatable: Locatable, restorationIdentifier: string, options?: Partial<VisitOptions>):
|
|
17
|
+
proposeVisit(location: URL, options?: Partial<VisitOptions>): void;
|
|
18
|
+
startVisit(locatable: Locatable, restorationIdentifier: string, options?: Partial<VisitOptions>): void;
|
|
19
19
|
submitForm(form: HTMLFormElement, submitter?: HTMLElement): void;
|
|
20
20
|
stop(): void;
|
|
21
21
|
get adapter(): import("../native/adapter").Adapter;
|
|
@@ -44,7 +44,6 @@ export declare type VisitOptions = {
|
|
|
44
44
|
shouldCacheSnapshot: boolean;
|
|
45
45
|
frame?: string;
|
|
46
46
|
acceptsStreamResponse: boolean;
|
|
47
|
-
initiator: Element;
|
|
48
47
|
};
|
|
49
48
|
export declare type VisitResponse = {
|
|
50
49
|
statusCode: number;
|
|
@@ -58,6 +57,7 @@ export declare enum SystemStatusCode {
|
|
|
58
57
|
}
|
|
59
58
|
export declare class Visit implements FetchRequestDelegate {
|
|
60
59
|
readonly delegate: VisitDelegate;
|
|
60
|
+
readonly identifier: string;
|
|
61
61
|
readonly restorationIdentifier: string;
|
|
62
62
|
readonly action: Action;
|
|
63
63
|
readonly referrer?: URL;
|
|
@@ -65,9 +65,6 @@ export declare class Visit implements FetchRequestDelegate {
|
|
|
65
65
|
readonly visitCachedSnapshot: (snapshot: Snapshot) => void;
|
|
66
66
|
readonly willRender: boolean;
|
|
67
67
|
readonly updateHistory: boolean;
|
|
68
|
-
readonly promise: Promise<void>;
|
|
69
|
-
readonly initiator: Element;
|
|
70
|
-
private resolvingFunctions;
|
|
71
68
|
followedRedirect: boolean;
|
|
72
69
|
frame?: number;
|
|
73
70
|
historyChanged: boolean;
|
|
@@ -47,7 +47,7 @@ export declare class FrameController implements AppearanceObserverDelegate, Fetc
|
|
|
47
47
|
elementAppearedInViewport(_element: Element): void;
|
|
48
48
|
willSubmitFormLinkToLocation(link: Element): boolean;
|
|
49
49
|
submittedFormLinkToLocation(link: Element, _location: URL, form: HTMLFormElement): void;
|
|
50
|
-
willFollowLinkToLocation(element: Element): boolean;
|
|
50
|
+
willFollowLinkToLocation(element: Element, location: URL, event: MouseEvent): boolean;
|
|
51
51
|
followedLinkToLocation(element: Element, location: URL): void;
|
|
52
52
|
willSubmitForm(element: HTMLFormElement, submitter?: HTMLElement): boolean;
|
|
53
53
|
formSubmitted(element: HTMLFormElement, submitter?: HTMLElement): void;
|
|
@@ -89,6 +89,7 @@ export declare class FrameController implements AppearanceObserverDelegate, Fetc
|
|
|
89
89
|
set complete(value: boolean);
|
|
90
90
|
get isActive(): boolean;
|
|
91
91
|
get rootLocation(): URL;
|
|
92
|
+
private frameAllowsVisitingLocation;
|
|
92
93
|
private isIgnoringChangesTo;
|
|
93
94
|
private ignoringChangesToAttribute;
|
|
94
95
|
private withCurrentNavigationElement;
|
|
@@ -9,10 +9,11 @@ export declare class FrameRedirector implements LinkClickObserverDelegate, FormS
|
|
|
9
9
|
constructor(session: Session, element: Element);
|
|
10
10
|
start(): void;
|
|
11
11
|
stop(): void;
|
|
12
|
-
willFollowLinkToLocation(element: Element): boolean;
|
|
12
|
+
willFollowLinkToLocation(element: Element, location: URL, event: MouseEvent): boolean;
|
|
13
13
|
followedLinkToLocation(element: Element, url: URL): void;
|
|
14
14
|
willSubmitForm(element: HTMLFormElement, submitter?: HTMLElement): boolean;
|
|
15
15
|
formSubmitted(element: HTMLFormElement, submitter?: HTMLElement): void;
|
|
16
|
+
private frameAllowsVisitingLocation;
|
|
16
17
|
private shouldSubmit;
|
|
17
18
|
private shouldRedirect;
|
|
18
19
|
private findFrameElement;
|
|
@@ -12,13 +12,13 @@ declare const session: Session;
|
|
|
12
12
|
declare const cache: Cache;
|
|
13
13
|
declare const navigator: import("./drive/navigator").Navigator;
|
|
14
14
|
export { navigator, session, cache, PageRenderer, PageSnapshot, FrameRenderer };
|
|
15
|
-
export { TurboBeforeCacheEvent, TurboBeforeRenderEvent, TurboBeforeVisitEvent, TurboClickEvent, TurboFrameLoadEvent, TurboFrameRenderEvent, TurboLoadEvent, TurboRenderEvent, TurboVisitEvent, } from "./session";
|
|
15
|
+
export { TurboBeforeCacheEvent, TurboBeforeRenderEvent, TurboBeforeVisitEvent, TurboClickEvent, TurboBeforeFrameRenderEvent, TurboFrameLoadEvent, TurboFrameRenderEvent, TurboLoadEvent, TurboRenderEvent, TurboVisitEvent, } from "./session";
|
|
16
16
|
export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission";
|
|
17
17
|
export { TurboFrameMissingEvent } from "./frames/frame_controller";
|
|
18
18
|
export { StreamActions, TurboStreamAction, TurboStreamActions } from "./streams/stream_actions";
|
|
19
19
|
export declare function start(): void;
|
|
20
20
|
export declare function registerAdapter(adapter: Adapter): void;
|
|
21
|
-
export declare function visit(location: Locatable, options?: Partial<VisitOptions>):
|
|
21
|
+
export declare function visit(location: Locatable, options?: Partial<VisitOptions>): void;
|
|
22
22
|
export declare function connectStreamSource(source: StreamSource): void;
|
|
23
23
|
export declare function disconnectStreamSource(source: StreamSource): void;
|
|
24
24
|
export declare function renderStreamMessage(message: StreamMessage | string): void;
|
|
@@ -2,7 +2,7 @@ import { Visit, VisitOptions } from "../drive/visit";
|
|
|
2
2
|
import { FormSubmission } from "../drive/form_submission";
|
|
3
3
|
import { ReloadReason } from "./browser_adapter";
|
|
4
4
|
export interface Adapter {
|
|
5
|
-
visitProposedToLocation(location: URL, options?: Partial<VisitOptions>):
|
|
5
|
+
visitProposedToLocation(location: URL, options?: Partial<VisitOptions>): void;
|
|
6
6
|
visitStarted(visit: Visit): void;
|
|
7
7
|
visitCompleted(visit: Visit): void;
|
|
8
8
|
visitFailed(visit: Visit): void;
|
|
@@ -17,7 +17,7 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
17
17
|
formProgressBarTimeout?: number;
|
|
18
18
|
location?: URL;
|
|
19
19
|
constructor(session: Session);
|
|
20
|
-
visitProposedToLocation(location: URL, options?: Partial<VisitOptions>):
|
|
20
|
+
visitProposedToLocation(location: URL, options?: Partial<VisitOptions>): void;
|
|
21
21
|
visitStarted(visit: Visit): void;
|
|
22
22
|
visitRequestStarted(visit: Visit): void;
|
|
23
23
|
visitRequestCompleted(visit: Visit): void;
|
|
@@ -74,7 +74,7 @@ export declare class Session implements FormSubmitObserverDelegate, HistoryDeleg
|
|
|
74
74
|
disable(): void;
|
|
75
75
|
stop(): void;
|
|
76
76
|
registerAdapter(adapter: Adapter): void;
|
|
77
|
-
visit(location: Locatable, options?: Partial<VisitOptions>):
|
|
77
|
+
visit(location: Locatable, options?: Partial<VisitOptions>): void;
|
|
78
78
|
connectStreamSource(source: StreamSource): void;
|
|
79
79
|
disconnectStreamSource(source: StreamSource): void;
|
|
80
80
|
renderStreamMessage(message: StreamMessage | string): void;
|
|
@@ -89,8 +89,8 @@ export declare class Session implements FormSubmitObserverDelegate, HistoryDeleg
|
|
|
89
89
|
submittedFormLinkToLocation(): void;
|
|
90
90
|
willFollowLinkToLocation(link: Element, location: URL, event: MouseEvent): boolean;
|
|
91
91
|
followedLinkToLocation(link: Element, location: URL): void;
|
|
92
|
-
|
|
93
|
-
visitProposedToLocation(location: URL, options: Partial<VisitOptions>):
|
|
92
|
+
allowsVisitingLocationWithAction(location: URL, action?: Action): boolean;
|
|
93
|
+
visitProposedToLocation(location: URL, options: Partial<VisitOptions>): void;
|
|
94
94
|
visitStarted(visit: Visit): void;
|
|
95
95
|
visitCompleted(visit: Visit): void;
|
|
96
96
|
locationWithActionIsSamePage(location: URL, action?: Action): boolean;
|
|
@@ -109,15 +109,15 @@ export declare class Session implements FormSubmitObserverDelegate, HistoryDeleg
|
|
|
109
109
|
frameLoaded(frame: FrameElement): void;
|
|
110
110
|
frameRendered(fetchResponse: FetchResponse, frame: FrameElement): void;
|
|
111
111
|
applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent): boolean;
|
|
112
|
-
applicationAllowsVisitingLocation(location: URL
|
|
112
|
+
applicationAllowsVisitingLocation(location: URL): boolean;
|
|
113
113
|
notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent): CustomEvent<{
|
|
114
114
|
url: string;
|
|
115
115
|
originalEvent: MouseEvent;
|
|
116
116
|
}>;
|
|
117
|
-
notifyApplicationBeforeVisitingLocation(location: URL
|
|
117
|
+
notifyApplicationBeforeVisitingLocation(location: URL): CustomEvent<{
|
|
118
118
|
url: string;
|
|
119
119
|
}>;
|
|
120
|
-
notifyApplicationAfterVisitingLocation(location: URL, action: Action
|
|
120
|
+
notifyApplicationAfterVisitingLocation(location: URL, action: Action): CustomEvent<{
|
|
121
121
|
url: string;
|
|
122
122
|
action: Action;
|
|
123
123
|
}>;
|
|
@@ -8,7 +8,3 @@ export declare type StreamSource = {
|
|
|
8
8
|
addEventListener(type: "message", listener: (event: MessageEvent) => void, options?: boolean | AddEventListenerOptions): void;
|
|
9
9
|
removeEventListener(type: "message", listener: (event: MessageEvent) => void, options?: boolean | EventListenerOptions): void;
|
|
10
10
|
};
|
|
11
|
-
export declare type ResolvingFunctions<T = unknown> = {
|
|
12
|
-
resolve(value: T | PromiseLike<T>): void;
|
|
13
|
-
reject(reason?: any): void;
|
|
14
|
-
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FetchResponse } from "./fetch_response";
|
|
2
|
+
import { FrameElement } from "../elements/frame_element";
|
|
2
3
|
export declare type TurboBeforeFetchRequestEvent = CustomEvent<{
|
|
3
4
|
fetchOptions: RequestInit;
|
|
4
5
|
url: URL;
|
|
@@ -44,10 +45,10 @@ export declare class FetchRequest {
|
|
|
44
45
|
readonly headers: FetchRequestHeaders;
|
|
45
46
|
readonly url: URL;
|
|
46
47
|
readonly body?: FetchRequestBody;
|
|
47
|
-
readonly target?:
|
|
48
|
+
readonly target?: FrameElement | HTMLFormElement | null;
|
|
48
49
|
readonly abortController: AbortController;
|
|
49
50
|
private resolveRequestPromise;
|
|
50
|
-
constructor(delegate: FetchRequestDelegate, method: FetchMethod, location: URL, body?: FetchRequestBody, target?:
|
|
51
|
+
constructor(delegate: FetchRequestDelegate, method: FetchMethod, location: URL, body?: FetchRequestBody, target?: FrameElement | HTMLFormElement | null);
|
|
51
52
|
get location(): URL;
|
|
52
53
|
get params(): URLSearchParams;
|
|
53
54
|
get entries(): [string, FormDataEntryValue][];
|
|
@@ -7,6 +7,8 @@ declare type MutationAttributeName = string;
|
|
|
7
7
|
declare type MutationAttributeValue = string | null;
|
|
8
8
|
declare type MutationLog = [MutationAttributeName, Target, MutationAttributeValue];
|
|
9
9
|
export declare function attributeForSelector(page: Page, selector: string, attributeName: string): Promise<string | null>;
|
|
10
|
+
declare type CancellableEvent = "turbo:click" | "turbo:before-visit";
|
|
11
|
+
export declare function cancelNextEvent(page: Page, eventName: CancellableEvent): Promise<void>;
|
|
10
12
|
export declare function clickWithoutScrolling(page: Page, selector: string, options?: {}): Promise<false | void>;
|
|
11
13
|
export declare function clearLocalStorage(page: Page): Promise<void>;
|
|
12
14
|
export declare function disposeAll(...handles: JSHandle[]): Promise<void[]>;
|
|
@@ -9,7 +9,7 @@ export declare class DeprecatedAdapterSupportTest extends DOMTestCase implements
|
|
|
9
9
|
teardown(): Promise<void>;
|
|
10
10
|
"test visit proposal location includes deprecated absoluteURL property"(): Promise<void>;
|
|
11
11
|
"test visit start location includes deprecated absoluteURL property"(): Promise<void>;
|
|
12
|
-
visitProposedToLocation(location: URL, _options?: Partial<VisitOptions>):
|
|
12
|
+
visitProposedToLocation(location: URL, _options?: Partial<VisitOptions>): void;
|
|
13
13
|
visitStarted(visit: Visit): void;
|
|
14
14
|
visitCompleted(_visit: Visit): void;
|
|
15
15
|
visitFailed(_visit: Visit): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DOMTestCase } from "../helpers/dom_test_case";
|
|
2
|
+
export { PageRenderer, PageSnapshot, FrameRenderer, FrameElement, StreamActions, StreamElement, StreamSourceElement, TurboBeforeCacheEvent, TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent, TurboBeforeFrameRenderEvent, TurboBeforeRenderEvent, TurboBeforeStreamRenderEvent, TurboBeforeVisitEvent, TurboClickEvent, TurboFetchRequestErrorEvent, TurboFrameLoadEvent, TurboFrameMissingEvent, TurboFrameRenderEvent, TurboLoadEvent, TurboRenderEvent, TurboStreamAction, TurboStreamActions, TurboSubmitEndEvent, TurboSubmitStartEvent, TurboVisitEvent, } from "../../index";
|
|
3
|
+
export declare class ExportTests extends DOMTestCase {
|
|
4
|
+
"test Turbo interface"(): Promise<void>;
|
|
5
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotwired/turbo",
|
|
3
|
-
"version": "7.2.0
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "The speed of a single-page web application without having to write any JavaScript",
|
|
5
5
|
"module": "dist/turbo.es2017-esm.js",
|
|
6
6
|
"main": "dist/turbo.es2017-umd.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"browser",
|
|
21
21
|
"pushstate"
|
|
22
22
|
],
|
|
23
|
-
"author": "
|
|
23
|
+
"author": "37signals LLC",
|
|
24
24
|
"contributors": [
|
|
25
25
|
"Jeffrey Hardy <jeff@basecamp.com>",
|
|
26
26
|
"Javan Makhmali <javan@javan.us>",
|