PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.0.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.0.0
2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / assets / js / admin-components / save.js

save.js in Subscriptions for WooCommerce with Stripe Recurring Payments 2.0.0, at assets/js/admin-components/save.js

164 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPSubsSave — one way to write to the REST API and one way to say what
3 * happened.
4 *
5 * Every admin screen that writes had grown its own copy of the same three
6 * things: a `fetch` wrapper, a button-busy lock, and some way of reporting the
7 * result. The wrappers had already drifted (one handled 204, the others did
8 * not) and the reporting had not settled at all — a banner here, a blocking
9 * `window.alert` there. This is that trio, once.
10 *
11 * Usage:
12 * var save = WPSubsSave.bind({ restUrl: cfg.restUrl, nonce: cfg.nonce, i18n: i18n });
13 *
14 * save.api("PUT", "/terms/12", { status: "active" }); // → Promise
15 *
16 * save.run(btn, function () { // busy + toast
17 * return save.api("PUT", "/relations/3", body);
18 * }, { success: i18n.saved });
19 *
20 * `window.confirm` is deliberately left alone: a toast reports, it does not
21 * ask, and a destructive action still needs a real answer.
22 */
23 (function () {
24 "use strict";
25
26 /**
27 * Say something happened. Falls back to nothing rather than throwing when
28 * the toast component is not on the page.
29 *
30 * @param {string} message Text.
31 * @param {string} [type] "success" (default) or "error".
32 */
33 function notify(message, type) {
34 if (window.WPSubsToast && message) {
35 window.WPSubsToast.show(message, type);
36 }
37 }
38
39 /**
40 * Mark a button busy while its request is in flight, and lock the controls
41 * beside it so the same write cannot be fired twice or abandoned midway.
42 * The `is-loading` class draws the spinner (admin-components/buttons.css).
43 *
44 * @param {HTMLElement} btn Button.
45 * @param {boolean} loading Loading state.
46 */
47 function busy(btn, loading) {
48 if (!btn) {
49 return;
50 }
51 btn.disabled = loading;
52 btn.classList.toggle("is-loading", loading);
53
54 // The row the button sits in: a modal footer, or an inline edit form.
55 var row = btn.closest(".wpsubs-modal__footer") || btn.parentNode;
56 if (row && row.querySelectorAll) {
57 row.querySelectorAll("button, input, select, textarea").forEach(function (el) {
58 if (el !== btn) {
59 el.disabled = loading;
60 }
61 });
62 }
63
64 // Inside a modal, the dismiss affordances go with it. Escape is left
65 // working on purpose, as the way out of a request that never returns.
66 var modal = btn.closest(".wpsubs-modal");
67 if (modal) {
68 var close = modal.querySelector(".wpsubs-modal__close");
69 if (close) {
70 close.disabled = loading;
71 }
72 var backdrop = modal.querySelector(".wpsubs-modal__backdrop");
73 if (backdrop) {
74 backdrop.style.pointerEvents = loading ? "none" : "";
75 }
76 }
77 }
78
79 /**
80 * Bind the helpers to one screen's REST base, nonce and strings.
81 *
82 * @param {Object} cfg { restUrl, nonce, i18n }.
83 * @return {Object} { api, run, busy, notify }.
84 */
85 function bind(cfg) {
86 cfg = cfg || {};
87 var i18n = cfg.i18n || {};
88
89 /**
90 * Call a REST endpoint under this screen's base.
91 *
92 * Rejects with the server's own message when it sends one, so a caller
93 * can report the real reason rather than a generic failure.
94 *
95 * @param {string} method HTTP verb.
96 * @param {string} path Path under the base, e.g. "/groups".
97 * @param {Object} [body] JSON body for writes.
98 * @return {Promise<Object>} Parsed JSON ({} for an empty body).
99 */
100 function api(method, path, body) {
101 return fetch(cfg.restUrl + path, {
102 method: method,
103 credentials: "same-origin",
104 headers: {
105 "Content-Type": "application/json",
106 "X-WP-Nonce": cfg.nonce || "",
107 },
108 body: body ? JSON.stringify(body) : undefined,
109 }).then(function (res) {
110 // 204 and an empty body are both success with nothing to parse;
111 // res.json() would throw on either.
112 return res.text().then(function (text) {
113 var data = {};
114 if (text) {
115 try {
116 data = JSON.parse(text);
117 } catch (e) {
118 data = {};
119 }
120 }
121 if (!res.ok) {
122 throw new Error((data && data.message) || i18n.genericError || "");
123 }
124 return data;
125 });
126 });
127 }
128
129 /**
130 * The whole write: lock the button, do the work, report the outcome,
131 * unlock either way.
132 *
133 * @param {HTMLElement} btn Button that triggered it (may be null).
134 * @param {Function} work Returns a Promise for the write.
135 * @param {Object} [opts] { success, error } toast messages; pass
136 * success: false to stay silent.
137 * @return {Promise} Resolves after the work and any follow-up.
138 */
139 function run(btn, work, opts) {
140 opts = opts || {};
141 busy(btn, true);
142
143 return Promise.resolve()
144 .then(work)
145 .then(function (result) {
146 busy(btn, false);
147 if (false !== opts.success) {
148 notify(opts.success || i18n.saved);
149 }
150 return result;
151 })
152 .catch(function (err) {
153 busy(btn, false);
154 notify(opts.error || (err && err.message) || i18n.genericError, "error");
155 throw err;
156 });
157 }
158
159 return { api: api, run: run, busy: busy, notify: notify };
160 }
161
162 window.WPSubsSave = { bind: bind, busy: busy, notify: notify };
163 })();
164