| 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&modestbranding=1&iv_load_policy=3&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 |
|