PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / assets / js / admin / charitable-ai-mcp.js

charitable-ai-mcp.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at assets/js/admin/charitable-ai-mcp.js

358 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Charitable Tools > AI MCP.
3 *
4 * Saves the AI write-access toggle over AJAX. The tab has no form and no Save
5 * Changes button, so this is the only way the setting is written.
6 *
7 * One master switch, matching Charitable Pro. Still bound by CLASS rather than
8 * the `#charitable-ai-mcp-write` id: the handler is delegated from document, and
9 * a class keeps it working if the control is moved or rendered more than once.
10 *
11 * @since 1.8.13
12 */
13 /* global jQuery, charitable_ai_mcp */
14 ( function ( $ ) {
15 'use strict';
16
17 var CharitableAiMcp = {
18
19 /**
20 * Bind up.
21 *
22 * @since 1.8.13
23 */
24 init: function () {
25 $( document ).on( 'change', '.charitable-ai-mcp-write', CharitableAiMcp.onToggle );
26 $( document ).on( 'click', '.charitable-ai-button[data-action="install"], .charitable-ai-button[data-action="activate"]', CharitableAiMcp.onBridgeClick );
27 $( document ).on( 'click', '.charitable-ai-video-link', CharitableAiMcp.onVideoClick );
28 $( document ).on( 'click', '.charitable-ai-activity-toggle', CharitableAiMcp.onActivityToggle );
29 },
30
31 /**
32 * Expand or collapse the AI Activity panel.
33 *
34 * The panel starts collapsed, with `is-collapsed` already in the markup
35 * so nothing is painted and then hidden. This only flips that class and
36 * keeps the button's aria-expanded and its screen-reader label honest -
37 * the showing and hiding is CSS.
38 *
39 * Deliberately NOT persisted. The list is long enough to be worth
40 * collapsing by default on every load, and remembering "expanded" would
41 * put the tallest thing on the screen back at the top of the page for
42 * anyone who opened it once. If that turns out to be wrong it wants a
43 * user meta and a nonce, not a localStorage flag that disagrees with the
44 * server-rendered class.
45 *
46 * @since 1.8.13
47 *
48 * @param {Event} event The click event.
49 */
50 onActivityToggle: function ( event ) {
51
52 event.preventDefault();
53
54 var $button = $( event.currentTarget ),
55 $panel = $button.closest( '.charitable-ai-activity-collapsible' ),
56 collapsing = ! $panel.hasClass( 'is-collapsed' );
57
58 $panel.toggleClass( 'is-collapsed', collapsing );
59 $button.attr( 'aria-expanded', collapsing ? 'false' : 'true' );
60
61 // The chevron is a class swap rather than a CSS rotate(). A transform
62 // was tried first: the rule matched, won on specificity and was in the
63 // loaded stylesheet, but the computed value stayed at identity in
64 // Chrome while the transition was live, so the arrow never turned.
65 // Swapping the glyph has no such edge case. Right = closed, down = open.
66 $button.find( '.charitable-ai-activity-angle' )
67 .toggleClass( 'dashicons-arrow-right-alt2', collapsing )
68 .toggleClass( 'dashicons-arrow-down-alt2', ! collapsing );
69
70 // The visible label is an icon, so the accessible name is the only
71 // thing that can say which way this control now goes.
72 $button.find( '.screen-reader-text' ).text(
73 collapsing ? charitable_ai_mcp.i18n.showActivity : charitable_ai_mcp.i18n.hideActivity
74 );
75 },
76
77 /**
78 * Play the walkthrough in a modal instead of leaving the dashboard.
79 *
80 * Same shape as Charitable Lite's welcome-screen video modal
81 * (charitable-admin-2.0.js, initWelcome) so the two behave alike: a
82 * youtube-nocookie embed with autoplay, opened through jquery-confirm.
83 *
84 * TWO DELIBERATE DIFFERENCES from that one:
85 *
86 * - The options are passed per call rather than written into
87 * `jconfirm.defaults`. Lite mutates the global, which then applies to
88 * every other dialog on the page - including the What's New splash,
89 * which is enqueued on this same screen.
90 * - If jquery-confirm is missing the click is left alone, so the anchor's
91 * own href opens the video on YouTube. The link is never a dead end.
92 *
93 * @since 1.8.13
94 *
95 * @param {Event} event The click event.
96 */
97 onVideoClick: function ( event ) {
98
99 var id = $( event.currentTarget ).data( 'video-id' );
100
101 if ( ! id || 'undefined' === typeof $.dialog ) {
102 return;
103 }
104
105 event.preventDefault();
106
107 $.dialog( {
108 title: false,
109 content: '<div class="charitable-ai-video-frame"><iframe src="https://www.youtube-nocookie.com/embed/' + encodeURIComponent( id ) + '?rel=0&amp;modestbranding=1&amp;iv_load_policy=3&amp;autoplay=1" title="' + charitable_ai_mcp.i18n.videoTitle + '" allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture" allowfullscreen frameborder="0"></iframe></div>',
110 closeIcon: true,
111 backgroundDismiss: true,
112 escapeKey: true,
113 useBootstrap: false,
114 theme: 'modern,charitable-ai-video-modal',
115 boxWidth: '900px',
116 animateFromElement: false
117 } );
118 },
119
120 /**
121 * Persist the new state of the write switch.
122 *
123 * The checkbox is disabled for the duration of the request, so a double
124 * click cannot race two saves against each other and leave the UI showing
125 * the opposite of what was stored.
126 *
127 * There is one switch, matching Charitable Pro. 1.8.13 briefly had three,
128 * which is why this used to post a `scope` alongside `enabled`; the
129 * handler now takes only the new state.
130 *
131 * @since 1.8.13
132 *
133 * @param {Event} event The change event.
134 */
135 onToggle: function ( event ) {
136
137 var $input = $( event.target ),
138 enabled = $input.is( ':checked' ),
139 $wrap = $input.closest( '.charitable-ai-gate' ),
140 $spinner = $wrap.find( '.charitable-ai-gate-spinner' );
141
142 $input.prop( 'disabled', true );
143 $spinner.addClass( 'is-active' );
144
145 $.post(
146 charitable_ai_mcp.ajax_url,
147 {
148 action: 'charitable_ai_mcp_toggle_write',
149 nonce: charitable_ai_mcp.nonce,
150 enabled: enabled ? '1' : '0'
151 }
152 ).done( function ( response ) {
153
154 if ( ! response || ! response.success ) {
155 // Put the control back where it was: the stored value did not move.
156 // ajax_toggle_write() guarantees that - its error path stores nothing,
157 // and it verifies the save by re-reading the option. A failure returned
158 // AFTER the value was written would revert this checkbox to OFF while an
159 // assistant could still make changes.
160 $input.prop( 'checked', ! enabled );
161 window.alert( ( response && response.data && response.data.message ) || charitable_ai_mcp.i18n.error );
162 return;
163 }
164
165 // Saved. If the handler ever has something to say about a save that
166 // landed, say it before reloading or the reader never learns.
167 if ( response.data && response.data.warning ) {
168 window.alert( response.data.warning );
169 }
170
171 // Reload so the read-only banner, and anything else that depends on
172 // the gate, reflects the new state rather than going stale.
173 window.location.reload();
174
175 } ).fail( function () {
176
177 $input.prop( 'checked', ! enabled );
178 window.alert( charitable_ai_mcp.i18n.error );
179
180 } ).always( function () {
181
182 $input.prop( 'disabled', false );
183 $spinner.removeClass( 'is-active' );
184 } );
185 },
186
187 /**
188 * Install and activate WPVibe without leaving the page.
189 *
190 * WPForms' equivalent tab does both in one click, so this chains the two
191 * endpoints rather than making the reader click twice. Charitable's shared
192 * installer JS is deliberately left alone: it drives the Addons, Dashboard
193 * and onboarding screens as a three-step install / activate / setup flow,
194 * and it finishes by retargeting the button to a new tab - which is the
195 * behaviour this button exists to avoid.
196 *
197 * @since 1.8.13
198 *
199 * @param {Event} event The click event.
200 */
201 onBridgeClick: function ( event ) {
202
203 event.preventDefault();
204
205 var $button = $( event.currentTarget );
206
207 // A second click while the first is in flight would install twice.
208 if ( $button.hasClass( 'is-working' ) ) {
209 return;
210 }
211
212 $button.addClass( 'is-working' ).data( 'label', $.trim( $button.text() ) );
213
214 if ( 'activate' === $button.data( 'action' ) ) {
215 CharitableAiMcp.activateBridge( $button, $button.data( 'basename' ) );
216 return;
217 }
218
219 CharitableAiMcp.setLabel( $button, charitable_ai_mcp.i18n.installing );
220
221 $.post(
222 charitable_ai_mcp.ajax_url,
223 {
224 action: 'charitable_install_plugin',
225 nonce: charitable_ai_mcp.plugin_nonce,
226 slug: $button.data( 'slug' )
227 }
228 ).done( function ( response ) {
229
230 // install_plugin() answers {form:...} when it wants filesystem
231 // credentials and {error:...} on failure, and a rejected nonce dies
232 // with a bare -1. None of those carry a success flag, so anything
233 // without one is a failure rather than something to chain from.
234 if ( ! response || ! response.success ) {
235 CharitableAiMcp.bridgeFailed( $button, response );
236 return;
237 }
238
239 // install_plugin() resolves the basename through Charitable's own plugin
240 // registry, which has no vibe-ai entry, so it answers '' here. The
241 // basename rendered into the button is the reliable one.
242 CharitableAiMcp.activateBridge( $button, ( response.data && response.data.basename ) || $button.data( 'basename' ) );
243
244 } ).fail( function () {
245 CharitableAiMcp.bridgeFailed( $button );
246 } );
247 },
248
249 /**
250 * Activate the bridge plugin, then re-render the tab.
251 *
252 * A reload rather than a DOM swap: the note under the button, the
253 * read-only banner and all six connection checks are rendered from the
254 * bridge state in PHP, so re-rendering is the only thing that leaves the
255 * whole tab consistent instead of just this one button.
256 *
257 * @since 1.8.13
258 *
259 * @param {jQuery} $button The button.
260 * @param {string} basename Plugin basename to activate.
261 */
262 activateBridge: function ( $button, basename ) {
263
264 CharitableAiMcp.setLabel( $button, charitable_ai_mcp.i18n.activating );
265
266 $.post(
267 charitable_ai_mcp.ajax_url,
268 {
269 action: 'charitable_activate_plugin',
270 nonce: charitable_ai_mcp.plugin_nonce,
271 basename: basename
272 }
273 ).done( function ( response ) {
274
275 // activate_plugin() only answers JSON when it can resolve a setup or
276 // settings screen for the plugin, which it does from Charitable's own
277 // registry - so for WPVibe a SUCCESSFUL activation falls through to a
278 // bare wp_die() and returns an empty body. Treating that as a failure
279 // is how this reported "could not be installed" for a plugin it had
280 // just activated. So only an explicit error message counts as one,
281 // and otherwise the reloaded tab is the source of truth: it renders
282 // from the real plugin state, and shows "Activate" again if activation
283 // genuinely did not happen.
284 var message = CharitableAiMcp.errorFrom( response );
285
286 if ( message ) {
287 CharitableAiMcp.bridgeFailed( $button, response );
288 return;
289 }
290
291 window.location.reload();
292
293 } ).fail( function () {
294 CharitableAiMcp.bridgeFailed( $button );
295 } );
296 },
297
298 /**
299 * The error message in an AJAX payload, if it carries one.
300 *
301 * These endpoints report failure three different ways - {error}, a
302 * wp_send_json_error() wrapping {error}, and {message} - so all three are
303 * read rather than just the one.
304 *
305 * @since 1.8.13
306 *
307 * @param {Object} response The AJAX payload.
308 * @return {string} The message, or '' when there is none.
309 */
310 errorFrom: function ( response ) {
311
312 if ( ! response ) {
313 return '';
314 }
315
316 return response.error ||
317 ( response.data && ( response.data.error || response.data.message ) ) ||
318 '';
319 },
320
321 /**
322 * Swap the label, keeping the trailing arrow.
323 *
324 * @since 1.8.13
325 *
326 * @param {jQuery} $button The button.
327 * @param {string} text New label.
328 */
329 setLabel: function ( $button, text ) {
330
331 var $arrow = $button.find( '.charitable-ai-button-arrow' ).detach();
332
333 $button.text( text ).append( $arrow );
334 },
335
336 /**
337 * Put the button back and say what went wrong.
338 *
339 * @since 1.8.13
340 *
341 * @param {jQuery} $button The button.
342 * @param {Object} response The AJAX payload, when there was one.
343 */
344 bridgeFailed: function ( $button, response ) {
345
346 var message = CharitableAiMcp.errorFrom( response ) || charitable_ai_mcp.i18n.installError;
347
348 $button.removeClass( 'is-working' );
349 CharitableAiMcp.setLabel( $button, $button.data( 'label' ) );
350
351 window.alert( message );
352 }
353 };
354
355 $( CharitableAiMcp.init );
356
357 }( jQuery ) );
358