mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-11 21:09:58 +00:00
1905 lines
70 KiB
JavaScript
1905 lines
70 KiB
JavaScript
/**
|
||
* MCP Tools for Elementor — Admin Settings Scripts
|
||
*
|
||
* @package EMCP_Tools
|
||
* @since 1.0.0
|
||
*/
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
/**
|
||
* Tools tab — Enable/Disable all toggles.
|
||
*/
|
||
function initToolsForm() {
|
||
var form = document.getElementById( 'elementor-mcp-tools-form' );
|
||
if ( ! form ) {
|
||
return;
|
||
}
|
||
|
||
// Global enable/disable all.
|
||
var enableAll = form.querySelector( '.elementor-mcp-enable-all' );
|
||
var disableAll = form.querySelector( '.elementor-mcp-disable-all' );
|
||
|
||
// Scope bulk actions to the per-tool checkboxes only — NOT the separate
|
||
// low-tools-mode toggle, which also lives in this form. (A bare
|
||
// form.querySelectorAll('input[type="checkbox"]') would flip low-tools
|
||
// mode too, silently overriding every individual toggle.)
|
||
var toolCheckboxSelector = '.elementor-mcp-tool-card input[type="checkbox"]';
|
||
|
||
if ( enableAll ) {
|
||
enableAll.addEventListener( 'click', function () {
|
||
form.querySelectorAll( toolCheckboxSelector ).forEach( function ( cb ) {
|
||
if ( ! cb.disabled ) {
|
||
cb.checked = true;
|
||
}
|
||
} );
|
||
updateCards( form );
|
||
} );
|
||
}
|
||
|
||
if ( disableAll ) {
|
||
disableAll.addEventListener( 'click', function () {
|
||
form.querySelectorAll( toolCheckboxSelector ).forEach( function ( cb ) {
|
||
if ( ! cb.disabled ) {
|
||
cb.checked = false;
|
||
}
|
||
} );
|
||
updateCards( form );
|
||
} );
|
||
}
|
||
|
||
// Per-category enable/disable + collapsible section headers.
|
||
// (cat scopes to .elementor-mcp-category, which never contains the
|
||
// low-tools-mode toggle, so the bulk selects below are safe.)
|
||
var COLLAPSE_KEY = 'emcpToolsCollapsed:';
|
||
form.querySelectorAll( '.elementor-mcp-category' ).forEach( function ( cat ) {
|
||
var catEnableAll = cat.querySelector( '.elementor-mcp-cat-enable-all' );
|
||
var catDisableAll = cat.querySelector( '.elementor-mcp-cat-disable-all' );
|
||
|
||
if ( catEnableAll ) {
|
||
catEnableAll.addEventListener( 'click', function () {
|
||
cat.querySelectorAll( 'input[type="checkbox"]' ).forEach( function ( cb ) {
|
||
if ( ! cb.disabled ) {
|
||
cb.checked = true;
|
||
}
|
||
} );
|
||
updateCards( form );
|
||
} );
|
||
}
|
||
|
||
if ( catDisableAll ) {
|
||
catDisableAll.addEventListener( 'click', function () {
|
||
cat.querySelectorAll( 'input[type="checkbox"]' ).forEach( function ( cb ) {
|
||
if ( ! cb.disabled ) {
|
||
cb.checked = false;
|
||
}
|
||
} );
|
||
updateCards( form );
|
||
} );
|
||
}
|
||
|
||
// Collapse/expand the section, persisting state per category.
|
||
var toggle = cat.querySelector( '.elementor-mcp-category-toggle' );
|
||
if ( toggle ) {
|
||
var key = COLLAPSE_KEY + ( cat.getAttribute( 'data-category' ) || '' );
|
||
var stored = null;
|
||
try {
|
||
stored = window.localStorage.getItem( key );
|
||
} catch ( e ) {}
|
||
if ( '1' === stored ) {
|
||
cat.classList.add( 'is-collapsed' );
|
||
toggle.setAttribute( 'aria-expanded', 'false' );
|
||
}
|
||
toggle.addEventListener( 'click', function () {
|
||
var collapsed = cat.classList.toggle( 'is-collapsed' );
|
||
toggle.setAttribute( 'aria-expanded', collapsed ? 'false' : 'true' );
|
||
try {
|
||
window.localStorage.setItem( key, collapsed ? '1' : '0' );
|
||
} catch ( e ) {}
|
||
} );
|
||
}
|
||
} );
|
||
|
||
// Toggle card visual state on checkbox change.
|
||
form.addEventListener( 'change', function ( e ) {
|
||
if ( e.target.type === 'checkbox' ) {
|
||
updateCards( form );
|
||
}
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Recompute the summary + per-category counts from the live checkbox state.
|
||
*
|
||
* @param {HTMLElement} form The tools form.
|
||
*/
|
||
function updateToolCounts( form ) {
|
||
var enabled = 0;
|
||
form.querySelectorAll( '.elementor-mcp-tool-card input[type="checkbox"]' ).forEach( function ( cb ) {
|
||
if ( cb.checked ) {
|
||
enabled++;
|
||
}
|
||
} );
|
||
var strong = form.querySelector( '.elementor-mcp-tools-summary strong' );
|
||
if ( strong ) {
|
||
// Replace just the leading "enabled" number, keeping the localized
|
||
// "N of M" wording intact.
|
||
strong.textContent = strong.textContent.replace( /^\s*\d+/, enabled );
|
||
}
|
||
form.querySelectorAll( '.elementor-mcp-category' ).forEach( function ( cat ) {
|
||
var cbs = cat.querySelectorAll( 'input[type="checkbox"]' );
|
||
var ce = 0;
|
||
cbs.forEach( function ( cb ) {
|
||
if ( cb.checked ) {
|
||
ce++;
|
||
}
|
||
} );
|
||
var el = cat.querySelector( '.elementor-mcp-category-count' );
|
||
if ( el ) {
|
||
el.textContent = ce + ' / ' + cbs.length;
|
||
}
|
||
} );
|
||
// Plugin-group counts (Plugins tab): sum across the group's plugin cards.
|
||
form.querySelectorAll( '.elementor-mcp-plugin-group' ).forEach( function ( grp ) {
|
||
var cbs = grp.querySelectorAll( '.elementor-mcp-tool-card input[type="checkbox"]' );
|
||
var ge = 0;
|
||
cbs.forEach( function ( cb ) {
|
||
if ( cb.checked ) {
|
||
ge++;
|
||
}
|
||
} );
|
||
var el = grp.querySelector( '.elementor-mcp-plugin-group-count' );
|
||
if ( el ) {
|
||
el.textContent = ge + ' / ' + cbs.length;
|
||
}
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Update card visual state based on checkbox.
|
||
*
|
||
* @param {HTMLElement} form The form element.
|
||
*/
|
||
function updateCards( form ) {
|
||
form.querySelectorAll( '.elementor-mcp-tool-card' ).forEach( function ( card ) {
|
||
var cb = card.querySelector( 'input[type="checkbox"]' );
|
||
card.classList.toggle( 'is-enabled', cb.checked );
|
||
card.classList.toggle( 'is-disabled', ! cb.checked );
|
||
} );
|
||
}
|
||
|
||
// Tools-page platform sub-tabs (Elementor / WordPress). Presentation only —
|
||
// hidden panels keep their checkboxes in the form, so switching tabs never
|
||
// affects what gets saved.
|
||
( function initToolSubtabs() {
|
||
var tabs = document.querySelectorAll( '.elementor-mcp-subtab' );
|
||
var panels = document.querySelectorAll( '.elementor-mcp-tabpanel' );
|
||
if ( ! tabs.length || ! panels.length ) {
|
||
return;
|
||
}
|
||
// Per-page storage key so different sub-tab groups (Tools vs Connection)
|
||
// don't overwrite each other's remembered tab. Falls back to the legacy
|
||
// key when the tablist doesn't declare one.
|
||
var tablist = document.querySelector( '.elementor-mcp-subtabs' );
|
||
var STORAGE_KEY = ( tablist && tablist.getAttribute( 'data-subtab-key' ) ) || 'emcpToolsActiveTab';
|
||
|
||
function activate( tabId ) {
|
||
var matched = false;
|
||
panels.forEach( function ( panel ) {
|
||
var on = panel.getAttribute( 'data-tab' ) === tabId;
|
||
panel.classList.toggle( 'is-active', on );
|
||
if ( on ) { matched = true; }
|
||
} );
|
||
if ( ! matched ) {
|
||
return; // unknown stored id (e.g. a removed tab) — leave server default.
|
||
}
|
||
tabs.forEach( function ( tab ) {
|
||
var on = tab.getAttribute( 'data-tab' ) === tabId;
|
||
tab.classList.toggle( 'is-active', on );
|
||
tab.setAttribute( 'aria-selected', on ? 'true' : 'false' );
|
||
} );
|
||
// Per-panel header actions (e.g. a Save button in the sub-tab row that
|
||
// submits the active panel's form). Only the matching one is shown.
|
||
document.querySelectorAll( '[data-save-for]' ).forEach( function ( el ) {
|
||
el.hidden = el.getAttribute( 'data-save-for' ) !== tabId;
|
||
} );
|
||
try { window.localStorage.setItem( STORAGE_KEY, tabId ); } catch ( e ) {}
|
||
}
|
||
|
||
tabs.forEach( function ( tab ) {
|
||
tab.addEventListener( 'click', function () {
|
||
activate( tab.getAttribute( 'data-tab' ) );
|
||
} );
|
||
} );
|
||
|
||
// Restore the last-used tab (falls back to the server-rendered active one).
|
||
var stored = null;
|
||
try { stored = window.localStorage.getItem( STORAGE_KEY ); } catch ( e ) {}
|
||
if ( stored ) {
|
||
activate( stored );
|
||
}
|
||
} )();
|
||
|
||
/**
|
||
* Populate a code block and its hidden copy source.
|
||
*
|
||
* @param {string} codeId The ID of the <code> element.
|
||
* @param {string} copyId The ID of the <textarea> copy source.
|
||
* @param {string} json The JSON string to display.
|
||
*/
|
||
function setConfigBlock( codeId, copyId, json ) {
|
||
var codeEl = document.getElementById( codeId );
|
||
var copyEl = document.getElementById( copyId );
|
||
if ( codeEl ) {
|
||
codeEl.textContent = json;
|
||
}
|
||
if ( copyEl ) {
|
||
copyEl.value = json;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Connection tab — Generate credentials and populate all HTTP config blocks.
|
||
*/
|
||
function initBase64Generator() {
|
||
var generateBtn = document.getElementById( 'elementor-mcp-generate-b64' );
|
||
if ( ! generateBtn ) {
|
||
return;
|
||
}
|
||
|
||
// Endpoint is available without generating credentials (OAuth mode needs it).
|
||
if ( typeof emcpToolsAdmin !== 'undefined' && emcpToolsAdmin.mcpEndpoint && ! window.emcpConn ) {
|
||
window.emcpConn = { endpoint: emcpToolsAdmin.mcpEndpoint, siteUrl: emcpToolsAdmin.siteUrl || '' };
|
||
}
|
||
|
||
// Authentication-method chooser: toggle the flow + re-render the selected
|
||
// client for the chosen method (OAuth = sign-in, app-password = configs).
|
||
var authRadios = document.querySelectorAll( 'input[name="emcp_auth_method"]' );
|
||
function emcpApplyAuthMethod() {
|
||
document.body.setAttribute( 'data-emcp-auth', emcpAuthMethod() );
|
||
var sel = document.querySelector( '.elementor-mcp-client-card.is-selected' );
|
||
if ( sel ) { emcpSelectClient( sel.getAttribute( 'data-client' ) ); }
|
||
}
|
||
for ( var ai = 0; ai < authRadios.length; ai++ ) {
|
||
authRadios[ ai ].addEventListener( 'change', emcpApplyAuthMethod );
|
||
}
|
||
document.body.setAttribute( 'data-emcp-auth', emcpAuthMethod() );
|
||
|
||
// The client picker is always visible now; auto-select so steps show.
|
||
var picker = document.getElementById( 'elementor-mcp-client-picker' );
|
||
if ( picker ) {
|
||
picker.style.display = '';
|
||
var savedClient = window.localStorage.getItem( 'emcpConnClient' );
|
||
var firstCard = document.querySelector( '.elementor-mcp-client-card' );
|
||
var pick = savedClient || ( firstCard ? firstCard.getAttribute( 'data-client' ) : '' );
|
||
if ( pick ) { emcpSelectClient( pick ); }
|
||
}
|
||
|
||
// Credentials are held in memory only so the server can run a complete
|
||
// initialize → initialized → tools/list diagnostic against the public MCP
|
||
// endpoint. They are never persisted by this page.
|
||
var emcpTestCredentials = null;
|
||
|
||
// Full connection self-test. The previous `/wp/v2/users/me` probe could
|
||
// pass while the MCP route, session lifecycle, or tools/list still failed.
|
||
var authBtn = document.getElementById( 'elementor-mcp-authtest-btn' );
|
||
if ( authBtn ) {
|
||
authBtn.addEventListener( 'click', function () {
|
||
if ( ! emcpTestCredentials || typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl || ! emcpToolsAdmin.testConnNonce ) {
|
||
return;
|
||
}
|
||
var statusEl = document.getElementById( 'elementor-mcp-authtest-status' );
|
||
var fixEl = document.getElementById( 'elementor-mcp-authtest-fix' );
|
||
if ( statusEl ) {
|
||
statusEl.style.display = '';
|
||
statusEl.className = 'description';
|
||
statusEl.textContent = emcpToolsAdmin.authTesting || 'Testing…';
|
||
}
|
||
if ( fixEl ) {
|
||
fixEl.style.display = 'none';
|
||
}
|
||
authBtn.disabled = true;
|
||
|
||
var payload = new FormData();
|
||
payload.append( 'action', 'emcp_tools_test_connection' );
|
||
payload.append( 'nonce', emcpToolsAdmin.testConnNonce );
|
||
payload.append( 'username', emcpTestCredentials.username );
|
||
payload.append( 'password', emcpTestCredentials.password );
|
||
|
||
/* global fetch */
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
body: payload
|
||
} ).then( function ( response ) {
|
||
return response.json();
|
||
} ).then( function ( result ) {
|
||
authBtn.disabled = false;
|
||
if ( ! statusEl ) { return; }
|
||
if ( result && result.success ) {
|
||
statusEl.className = 'description elementor-mcp-authtest-ok';
|
||
var count = result.data && typeof result.data.tool_count === 'number' ? ' (' + result.data.tool_count + ' tools found)' : '';
|
||
statusEl.textContent = ( emcpToolsAdmin.authOk || 'Full MCP handshake succeeded.' ) + count;
|
||
if ( fixEl ) {
|
||
fixEl.style.display = 'none';
|
||
}
|
||
} else {
|
||
statusEl.className = 'description elementor-mcp-authtest-bad';
|
||
statusEl.textContent = result && result.data && result.data.message ? result.data.message : ( emcpToolsAdmin.authError || 'Could not run the MCP connection test.' );
|
||
if ( fixEl ) {
|
||
fixEl.style.display = result && result.data && result.data.stage === 'initialize' ? '' : 'none';
|
||
}
|
||
}
|
||
} ).catch( function () {
|
||
authBtn.disabled = false;
|
||
if ( statusEl ) {
|
||
statusEl.className = 'description elementor-mcp-authtest-bad';
|
||
statusEl.textContent = emcpToolsAdmin.authError || 'Could not reach the REST API.';
|
||
}
|
||
if ( fixEl ) {
|
||
fixEl.style.display = '';
|
||
}
|
||
} );
|
||
} );
|
||
}
|
||
|
||
// OAuth discovery diagnostic: the request runs from WordPress through the
|
||
// public hostname, which exposes CDN/host interception that an internal
|
||
// route check would miss.
|
||
var oauthTestBtn = document.getElementById( 'elementor-mcp-oauth-test-btn' );
|
||
if ( oauthTestBtn ) {
|
||
oauthTestBtn.addEventListener( 'click', function () {
|
||
if ( typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl || ! emcpToolsAdmin.testOAuthNonce ) { return; }
|
||
var oauthStatus = document.getElementById( 'elementor-mcp-oauth-test-status' );
|
||
var oauthPayload = new FormData();
|
||
oauthPayload.append( 'action', 'emcp_tools_test_oauth_discovery' );
|
||
oauthPayload.append( 'nonce', emcpToolsAdmin.testOAuthNonce );
|
||
oauthTestBtn.disabled = true;
|
||
if ( oauthStatus ) {
|
||
oauthStatus.style.display = '';
|
||
oauthStatus.className = 'description';
|
||
oauthStatus.textContent = emcpToolsAdmin.oauthTesting || 'Checking public OAuth discovery endpoints…';
|
||
}
|
||
fetch( emcpToolsAdmin.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: oauthPayload } )
|
||
.then( function ( response ) { return response.json(); } )
|
||
.then( function ( result ) {
|
||
oauthTestBtn.disabled = false;
|
||
if ( ! oauthStatus ) { return; }
|
||
oauthStatus.className = result && result.success ? 'description elementor-mcp-authtest-ok' : 'description elementor-mcp-authtest-bad';
|
||
var oauthMessage = result && result.data && result.data.message ? result.data.message : 'Could not run the OAuth discovery test.';
|
||
if ( result && result.data && result.data.checks ) {
|
||
var failedChecks = [];
|
||
Object.keys( result.data.checks ).forEach( function ( key ) {
|
||
var check = result.data.checks[ key ];
|
||
if ( check && ! check.ok ) {
|
||
failedChecks.push( key.replace( /_/g, ' ' ) + ': HTTP ' + check.status );
|
||
}
|
||
} );
|
||
if ( failedChecks.length ) { oauthMessage += ' Failed: ' + failedChecks.join( '; ' ) + '.'; }
|
||
}
|
||
oauthStatus.textContent = oauthMessage;
|
||
} )
|
||
.catch( function () {
|
||
oauthTestBtn.disabled = false;
|
||
if ( oauthStatus ) {
|
||
oauthStatus.className = 'description elementor-mcp-authtest-bad';
|
||
oauthStatus.textContent = 'Could not run the OAuth discovery test.';
|
||
}
|
||
} );
|
||
} );
|
||
}
|
||
|
||
generateBtn.addEventListener( 'click', function () {
|
||
var usernameEl = document.getElementById( 'elementor-mcp-b64-username' );
|
||
if ( ! usernameEl || ! usernameEl.value ) {
|
||
/* global alert */
|
||
alert( 'Please select an administrator account.' );
|
||
return;
|
||
}
|
||
|
||
var selectedOption = usernameEl.options[ usernameEl.selectedIndex ];
|
||
var selectedLogin = selectedOption ? ( selectedOption.getAttribute( 'data-login' ) || '' ) : '';
|
||
var manualEl = document.getElementById( 'elementor-mcp-b64-app-password' );
|
||
var manualPassword = manualEl ? manualEl.value.trim() : '';
|
||
|
||
// If an existing password is supplied, use it directly and skip creation.
|
||
if ( manualPassword ) {
|
||
renderConfigs( selectedLogin, manualPassword );
|
||
return;
|
||
}
|
||
|
||
if ( typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl || ! emcpToolsAdmin.createPwNonce ) {
|
||
setCredStatus( 'Cannot create an application password automatically. Enter one manually below.', true );
|
||
return;
|
||
}
|
||
|
||
var origLabel = generateBtn.textContent;
|
||
generateBtn.disabled = true;
|
||
generateBtn.textContent = emcpToolsAdmin.generating || 'Generating…';
|
||
setCredStatus( '', false );
|
||
|
||
var payload = new FormData();
|
||
payload.append( 'action', 'emcp_tools_create_app_password' );
|
||
payload.append( 'nonce', emcpToolsAdmin.createPwNonce );
|
||
payload.append( 'user_id', usernameEl.value );
|
||
|
||
/* global fetch */
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
body: payload
|
||
} ).then( function ( response ) {
|
||
return response.json();
|
||
} ).then( function ( result ) {
|
||
generateBtn.disabled = false;
|
||
generateBtn.textContent = origLabel;
|
||
|
||
if ( ! result || ! result.success || ! result.data || ! result.data.password ) {
|
||
var message = ( result && result.data && result.data.message ) ? result.data.message : 'Could not create an application password.';
|
||
setCredStatus( message, true );
|
||
return;
|
||
}
|
||
|
||
setCredStatus( emcpToolsAdmin.pwCreated || 'Application password created — save it below, it is shown only once.', false );
|
||
renderGeneratedPassword( result.data.password );
|
||
renderConfigs( result.data.username, result.data.password );
|
||
} ).catch( function () {
|
||
generateBtn.disabled = false;
|
||
generateBtn.textContent = origLabel;
|
||
setCredStatus( 'Network error while creating the application password.', true );
|
||
} );
|
||
} );
|
||
|
||
/**
|
||
* Shows an inline status message under the credential form.
|
||
*
|
||
* @param {string} message Message text ('' hides it).
|
||
* @param {boolean} isError Whether to style it as an error.
|
||
*/
|
||
function setCredStatus( message, isError ) {
|
||
var statusEl = document.getElementById( 'elementor-mcp-cred-status' );
|
||
if ( ! statusEl ) {
|
||
return;
|
||
}
|
||
statusEl.style.display = message ? '' : 'none';
|
||
statusEl.textContent = message || '';
|
||
statusEl.style.color = isError ? '#b32d2e' : '';
|
||
}
|
||
|
||
/**
|
||
* Reveals and fills the generated application password field.
|
||
*
|
||
* @param {string} password The newly created application password.
|
||
*/
|
||
function renderGeneratedPassword( password ) {
|
||
var row = document.getElementById( 'elementor-mcp-generated-pw-row' );
|
||
var code = document.getElementById( 'elementor-mcp-generated-pw' );
|
||
var copy = document.getElementById( 'elementor-mcp-generated-pw-copy' );
|
||
if ( row && code && copy ) {
|
||
row.style.display = '';
|
||
code.textContent = password;
|
||
copy.value = password;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Builds every client config block from a username + application password.
|
||
*
|
||
* @param {string} rawUsername WordPress username.
|
||
* @param {string} rawAppPassword Application password.
|
||
*/
|
||
function renderConfigs( rawUsername, rawAppPassword ) {
|
||
var headerValue = 'Basic ' + btoa( rawUsername + ':' + rawAppPassword );
|
||
|
||
// Show the result row.
|
||
var resultRow = document.getElementById( 'elementor-mcp-b64-result-row' );
|
||
var resultCode = document.getElementById( 'elementor-mcp-b64-result' );
|
||
var resultCopy = document.getElementById( 'elementor-mcp-b64-result-copy' );
|
||
|
||
if ( resultRow && resultCode && resultCopy ) {
|
||
resultRow.style.display = '';
|
||
resultCode.textContent = headerValue;
|
||
resultCopy.value = headerValue;
|
||
}
|
||
|
||
// Arm the full MCP self-test with these credentials (memory only).
|
||
emcpTestCredentials = { username: rawUsername, password: rawAppPassword };
|
||
var authRow = document.getElementById( 'elementor-mcp-authtest-row' );
|
||
if ( authRow ) {
|
||
authRow.style.display = '';
|
||
}
|
||
|
||
if ( typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.mcpEndpoint ) {
|
||
return;
|
||
}
|
||
|
||
// Stash for the client picker (Step 2/3).
|
||
window.emcpConn = {
|
||
endpoint: emcpToolsAdmin.mcpEndpoint,
|
||
siteUrl: emcpToolsAdmin.siteUrl || '',
|
||
username: rawUsername,
|
||
appPassword: rawAppPassword,
|
||
userId: ( document.getElementById( 'elementor-mcp-b64-username' ) || {} ).value || '',
|
||
b64: btoa( rawUsername + ':' + rawAppPassword )
|
||
};
|
||
var picker = document.getElementById( 'elementor-mcp-client-picker' );
|
||
if ( picker ) { picker.style.display = ''; }
|
||
// Re-select a remembered client if any.
|
||
var saved = window.localStorage.getItem( 'emcpConnClient' );
|
||
if ( saved ) { emcpSelectClient( saved ); }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Copy text to clipboard with fallback for non-HTTPS contexts.
|
||
*
|
||
* @param {string} text The text to copy.
|
||
* @returns {Promise} Resolves when copied.
|
||
*/
|
||
function copyToClipboard( text ) {
|
||
if ( navigator.clipboard && navigator.clipboard.writeText ) {
|
||
return navigator.clipboard.writeText( text ).catch( function () {
|
||
return copyToClipboardFallback( text );
|
||
} );
|
||
}
|
||
|
||
return copyToClipboardFallback( text );
|
||
}
|
||
|
||
/**
|
||
* Legacy clipboard fallback that rejects when the browser reports failure.
|
||
*
|
||
* @param {string} text The text to copy.
|
||
* @returns {Promise} Resolves only when the copy command succeeds.
|
||
*/
|
||
function copyToClipboardFallback( text ) {
|
||
return new Promise( function ( resolve, reject ) {
|
||
var textarea;
|
||
try {
|
||
textarea = document.createElement( 'textarea' );
|
||
textarea.value = text;
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.opacity = '0';
|
||
document.body.appendChild( textarea );
|
||
textarea.select();
|
||
if ( ! document.execCommand( 'copy' ) ) {
|
||
throw new Error( 'Clipboard copy command failed.' );
|
||
}
|
||
resolve();
|
||
} catch ( error ) {
|
||
reject( error );
|
||
} finally {
|
||
if ( textarea && textarea.parentNode ) {
|
||
textarea.parentNode.removeChild( textarea );
|
||
}
|
||
}
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Copy-to-clipboard buttons (Connection tab + every prompt card).
|
||
*
|
||
* Single delegated listener on document — avoids attaching 50+ listeners on the
|
||
* Prompts page, which used to slow first paint and inflate memory.
|
||
*/
|
||
function initCopyButtons() {
|
||
document.addEventListener( 'click', function ( e ) {
|
||
var btn = e.target.closest( '.elementor-mcp-copy-btn' );
|
||
if ( ! btn ) {
|
||
return;
|
||
}
|
||
var targetId = btn.getAttribute( 'data-target' );
|
||
var source = targetId ? document.getElementById( targetId ) : null;
|
||
if ( ! source ) {
|
||
return;
|
||
}
|
||
|
||
var copiedText = ( typeof emcpToolsAdmin !== 'undefined' && emcpToolsAdmin.copied ) ? emcpToolsAdmin.copied : 'Copied!';
|
||
|
||
var original = btn.textContent;
|
||
var card = btn.closest( '.elementor-mcp-pro-prompt-card' );
|
||
copyToClipboard( source.value ).then( function () {
|
||
btn.textContent = copiedText;
|
||
// Count a premium prompt copy only after the browser confirms success.
|
||
trackProPromptCopy( card );
|
||
setTimeout( function () {
|
||
btn.textContent = original;
|
||
}, 2000 );
|
||
} ).catch( function () {
|
||
btn.textContent = ( typeof emcpToolsAdmin !== 'undefined' && emcpToolsAdmin.copyFailed ) ? emcpToolsAdmin.copyFailed : 'Copy failed';
|
||
setTimeout( function () {
|
||
btn.textContent = original;
|
||
}, 2000 );
|
||
} );
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Fire a fire-and-forget "prompt copied" event for a Pro prompt card.
|
||
* No-ops for free/bundled prompts (no card / no slug) or missing nonce.
|
||
*/
|
||
function trackProPromptCopy( card ) {
|
||
if ( ! card || typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.trackPromptNonce ) {
|
||
return;
|
||
}
|
||
var slug = card.getAttribute( 'data-prompt-slug' );
|
||
var category = card.getAttribute( 'data-category' );
|
||
if ( ! slug || ! category ) {
|
||
return;
|
||
}
|
||
var body = new FormData();
|
||
body.append( 'action', 'emcp_tools_track_prompt_copy' );
|
||
body.append( 'nonce', emcpToolsAdmin.trackPromptNonce );
|
||
body.append( 'prompt_slug', slug );
|
||
body.append( 'category_slug', category );
|
||
fetch( emcpToolsAdmin.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: body } ).catch( function () {} );
|
||
}
|
||
|
||
/**
|
||
* Reusable, filter-aware client-side pagination for a card grid.
|
||
*
|
||
* Owns BOTH the category filter pills and the pager, so the two stay in
|
||
* sync: changing the filter recomputes the matching set and resets to page
|
||
* one. Only the cards on the current page are shown; the rest are
|
||
* display:none, which keeps the DOM light and the page responsive even with
|
||
* 50+ cards. Safe to call on any page — it no-ops when the grid is absent.
|
||
*
|
||
* @param {Object} opts
|
||
* @param {string} opts.gridSelector Selector for the grid container.
|
||
* @param {string} opts.cardSelector Selector for cards within the grid.
|
||
* @param {string} [opts.filterSelector] Selector for the filter-pill bar.
|
||
* @param {number} [opts.pageSize] Cards per page (default 12).
|
||
* @param {string} [opts.label] Noun for the status line (e.g. 'prompts').
|
||
*/
|
||
function initGridPagination( opts ) {
|
||
var grid = document.querySelector( opts.gridSelector );
|
||
if ( ! grid ) {
|
||
return;
|
||
}
|
||
var cards = Array.prototype.slice.call( grid.querySelectorAll( opts.cardSelector ) );
|
||
if ( ! cards.length ) {
|
||
return;
|
||
}
|
||
|
||
var pageSize = opts.pageSize || 12;
|
||
var label = opts.label || 'items';
|
||
var filterBar = opts.filterSelector ? document.querySelector( opts.filterSelector ) : null;
|
||
var activeCategory = 'all';
|
||
var currentPage = 1;
|
||
|
||
// Pager container lives directly after the grid.
|
||
var pager = document.createElement( 'nav' );
|
||
pager.className = 'elementor-mcp-pager';
|
||
pager.setAttribute( 'aria-label', 'Pagination' );
|
||
grid.parentNode.insertBefore( pager, grid.nextSibling );
|
||
|
||
function matching() {
|
||
return cards.filter( function ( card ) {
|
||
return 'all' === activeCategory || card.getAttribute( 'data-category' ) === activeCategory;
|
||
} );
|
||
}
|
||
|
||
function makeBtn( text, page, opt ) {
|
||
opt = opt || {};
|
||
var btn = document.createElement( 'button' );
|
||
btn.type = 'button';
|
||
btn.className = 'elementor-mcp-pager-btn' + ( opt.current ? ' is-current' : '' );
|
||
btn.textContent = text;
|
||
if ( opt.disabled ) {
|
||
btn.disabled = true;
|
||
}
|
||
if ( opt.current ) {
|
||
btn.setAttribute( 'aria-current', 'page' );
|
||
}
|
||
if ( opt.ariaLabel ) {
|
||
btn.setAttribute( 'aria-label', opt.ariaLabel );
|
||
}
|
||
if ( ! opt.disabled && ! opt.current ) {
|
||
btn.addEventListener( 'click', function () {
|
||
currentPage = page;
|
||
render();
|
||
} );
|
||
}
|
||
return btn;
|
||
}
|
||
|
||
// Windowed page list with ellipses: 1 … 4 5 [6] 7 8 … 20.
|
||
function pageList( total ) {
|
||
var pages = [];
|
||
var add = function ( p ) { if ( pages.indexOf( p ) === -1 ) { pages.push( p ); } };
|
||
add( 1 );
|
||
add( total );
|
||
for ( var p = currentPage - 1; p <= currentPage + 1; p++ ) {
|
||
if ( p >= 1 && p <= total ) {
|
||
add( p );
|
||
}
|
||
}
|
||
pages.sort( function ( a, b ) { return a - b; } );
|
||
var withGaps = [];
|
||
for ( var i = 0; i < pages.length; i++ ) {
|
||
if ( i > 0 && pages[ i ] - pages[ i - 1 ] > 1 ) {
|
||
withGaps.push( '…' );
|
||
}
|
||
withGaps.push( pages[ i ] );
|
||
}
|
||
return withGaps;
|
||
}
|
||
|
||
function render() {
|
||
var list = matching();
|
||
var totalPages = Math.max( 1, Math.ceil( list.length / pageSize ) );
|
||
if ( currentPage > totalPages ) {
|
||
currentPage = totalPages;
|
||
}
|
||
var start = ( currentPage - 1 ) * pageSize;
|
||
var end = start + pageSize;
|
||
|
||
cards.forEach( function ( card ) { card.style.display = 'none'; } );
|
||
list.slice( start, end ).forEach( function ( card ) { card.style.display = ''; } );
|
||
|
||
pager.innerHTML = '';
|
||
if ( totalPages <= 1 ) {
|
||
return;
|
||
}
|
||
|
||
pager.appendChild( makeBtn( '‹', currentPage - 1, {
|
||
disabled: currentPage === 1,
|
||
ariaLabel: 'Previous page'
|
||
} ) );
|
||
|
||
pageList( totalPages ).forEach( function ( item ) {
|
||
if ( '…' === item ) {
|
||
var span = document.createElement( 'span' );
|
||
span.className = 'elementor-mcp-pager-ellipsis';
|
||
span.textContent = '…';
|
||
pager.appendChild( span );
|
||
} else {
|
||
pager.appendChild( makeBtn( String( item ), item, {
|
||
current: item === currentPage,
|
||
ariaLabel: 'Page ' + item
|
||
} ) );
|
||
}
|
||
} );
|
||
|
||
pager.appendChild( makeBtn( '›', currentPage + 1, {
|
||
disabled: currentPage === totalPages,
|
||
ariaLabel: 'Next page'
|
||
} ) );
|
||
|
||
var status = document.createElement( 'p' );
|
||
status.className = 'elementor-mcp-pager-status';
|
||
status.textContent = 'Showing ' + ( start + 1 ) + '–' + Math.min( end, list.length ) +
|
||
' of ' + list.length + ' ' + label;
|
||
pager.appendChild( status );
|
||
}
|
||
|
||
// Own the category filter pills (active state + reset to page 1).
|
||
if ( filterBar ) {
|
||
filterBar.addEventListener( 'click', function ( e ) {
|
||
var btn = e.target.closest( '.elementor-mcp-pro-filter' );
|
||
if ( ! btn ) {
|
||
return;
|
||
}
|
||
activeCategory = btn.getAttribute( 'data-category' ) || 'all';
|
||
filterBar.querySelectorAll( '.elementor-mcp-pro-filter' ).forEach( function ( b ) {
|
||
b.classList.toggle( 'is-active', b === btn );
|
||
} );
|
||
currentPage = 1;
|
||
render();
|
||
} );
|
||
}
|
||
|
||
render();
|
||
}
|
||
|
||
/**
|
||
* Premium prompts — "Sync Library" button.
|
||
*/
|
||
function initProSync() {
|
||
document.querySelectorAll( '.elementor-mcp-pro-sync-btn' ).forEach( function ( btn ) {
|
||
btn.addEventListener( 'click', function () {
|
||
if ( typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl ) {
|
||
return;
|
||
}
|
||
var original = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="dashicons dashicons-update spin" aria-hidden="true"></span> ' + ( emcpToolsAdmin.syncing || 'Syncing…' );
|
||
|
||
// Action override via data-sync-action lets the same button
|
||
// pattern work for prompts and templates. Falls back to the
|
||
// prompts action for backwards compat with the existing UI.
|
||
var action = btn.getAttribute( 'data-sync-action' ) || 'emcp_tools_sync_pro_prompts';
|
||
var body = new URLSearchParams();
|
||
body.append( 'action', action );
|
||
body.append( 'nonce', btn.getAttribute( 'data-nonce' ) || '' );
|
||
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: body.toString(),
|
||
} )
|
||
.then( function ( r ) { return r.json(); } )
|
||
.then( function ( res ) {
|
||
if ( res && res.success ) {
|
||
window.location.reload();
|
||
} else {
|
||
var msg = ( res && res.data && res.data.message ) ? res.data.message : 'Sync failed.';
|
||
window.alert( msg );
|
||
btn.disabled = false;
|
||
btn.innerHTML = original;
|
||
}
|
||
} )
|
||
.catch( function () {
|
||
window.alert( 'Sync failed. Check your connection and try again.' );
|
||
btn.disabled = false;
|
||
btn.innerHTML = original;
|
||
} );
|
||
} );
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Templates page — Apply-to-New-Page and Import-to-Library buttons.
|
||
* Single delegated click handler routes to whichever AJAX action the
|
||
* button is configured for. Both open the result in a new tab on success.
|
||
*/
|
||
function initProTemplateActions() {
|
||
var grid = document.querySelector( '.elementor-mcp-template-grid' );
|
||
if ( ! grid ) {
|
||
return;
|
||
}
|
||
var applyNonce = grid.getAttribute( 'data-apply-nonce' ) || '';
|
||
var importNonce = grid.getAttribute( 'data-import-nonce' ) || '';
|
||
|
||
grid.addEventListener( 'click', function ( e ) {
|
||
var applyBtn = e.target.closest( '.elementor-mcp-template-apply' );
|
||
var importBtn = e.target.closest( '.elementor-mcp-template-import' );
|
||
var btn = applyBtn || importBtn;
|
||
if ( ! btn ) {
|
||
return;
|
||
}
|
||
if ( typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl ) {
|
||
return;
|
||
}
|
||
|
||
var isApply = !! applyBtn;
|
||
var action = isApply ? 'emcp_tools_apply_pro_template' : 'emcp_tools_import_pro_template';
|
||
var nonce = isApply ? applyNonce : importNonce;
|
||
var pending = isApply ? 'Creating…' : 'Importing…';
|
||
var failMsg = isApply ? 'Create failed.' : 'Import failed.';
|
||
|
||
var category = btn.getAttribute( 'data-category-slug' ) || '';
|
||
var template = btn.getAttribute( 'data-template-slug' ) || '';
|
||
var original = btn.innerHTML;
|
||
btn.disabled = true;
|
||
btn.textContent = pending;
|
||
|
||
var body = new URLSearchParams();
|
||
body.append( 'action', action );
|
||
body.append( 'nonce', nonce );
|
||
body.append( 'category_slug', category );
|
||
body.append( 'template_slug', template );
|
||
if ( isApply ) {
|
||
body.append( 'target_post_id', '0' );
|
||
}
|
||
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: body.toString(),
|
||
} )
|
||
.then( function ( r ) { return r.json(); } )
|
||
.then( function ( res ) {
|
||
var ok = res && res.success && res.data;
|
||
// Apply → open the new page's Elementor editor.
|
||
// Import → open the Saved Templates library list so the
|
||
// user can see the new entry.
|
||
var openUrl = ok ? ( isApply ? res.data.edit_url : res.data.library_url ) : '';
|
||
if ( openUrl ) {
|
||
window.open( openUrl, '_blank', 'noopener' );
|
||
btn.disabled = false;
|
||
btn.innerHTML = original;
|
||
} else {
|
||
var msg = ( res && res.data && res.data.message ) ? res.data.message : failMsg;
|
||
window.alert( msg );
|
||
btn.disabled = false;
|
||
btn.innerHTML = original;
|
||
}
|
||
} )
|
||
.catch( function () {
|
||
window.alert( failMsg + ' Check your connection and try again.' );
|
||
btn.disabled = false;
|
||
btn.innerHTML = original;
|
||
} );
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Brand Kits page — transient success toast with an optional "View site" link.
|
||
*
|
||
* @param {string} message The toast message.
|
||
* @param {string} viewUrl Optional URL to surface as a "View site →" link.
|
||
*/
|
||
function showBrandKitToast( message, viewUrl ) {
|
||
var toast = document.createElement( 'div' );
|
||
toast.className = 'elementor-mcp-bk-toast';
|
||
var span = document.createElement( 'span' );
|
||
span.textContent = message;
|
||
toast.appendChild( span );
|
||
if ( viewUrl ) {
|
||
var link = document.createElement( 'a' );
|
||
link.href = viewUrl;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener noreferrer';
|
||
link.textContent = ( typeof emcpToolsAdmin !== 'undefined' && emcpToolsAdmin.viewSite ) ? emcpToolsAdmin.viewSite : 'View site →';
|
||
toast.appendChild( link );
|
||
}
|
||
document.body.appendChild( toast );
|
||
// Force reflow then animate in.
|
||
window.requestAnimationFrame( function () {
|
||
toast.classList.add( 'is-visible' );
|
||
} );
|
||
setTimeout( function () {
|
||
toast.classList.remove( 'is-visible' );
|
||
setTimeout( function () { toast.remove(); }, 400 );
|
||
}, 7000 );
|
||
}
|
||
|
||
/**
|
||
* Brand Kits page — category filters, apply-with-confirmation modal, and
|
||
* restore-from-backup.
|
||
*/
|
||
function initBrandKits() {
|
||
var root = document.querySelector( '.elementor-mcp-brand-kits' );
|
||
if ( ! root || typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl ) {
|
||
return;
|
||
}
|
||
|
||
var grid = root.querySelector( '.elementor-mcp-brand-kit-grid' );
|
||
|
||
// Note: the category filter pills are handled by initGridPagination(),
|
||
// which owns both filtering and pagination so they stay in sync.
|
||
|
||
// Apply confirmation modal.
|
||
var modal = root.querySelector( '.elementor-mcp-brand-kit-modal' );
|
||
var pending = null;
|
||
|
||
function closeModal() {
|
||
if ( modal ) {
|
||
modal.hidden = true;
|
||
}
|
||
pending = null;
|
||
}
|
||
|
||
if ( grid && modal ) {
|
||
grid.addEventListener( 'click', function ( e ) {
|
||
var btn = e.target.closest( '.elementor-mcp-brand-kit-apply' );
|
||
if ( ! btn ) {
|
||
return;
|
||
}
|
||
pending = {
|
||
slug: btn.getAttribute( 'data-kit-slug' ) || '',
|
||
cat: btn.getAttribute( 'data-category-slug' ) || '',
|
||
title: btn.getAttribute( 'data-kit-title' ) || ''
|
||
};
|
||
var titleEl = modal.querySelector( '.elementor-mcp-brand-kit-modal__title' );
|
||
if ( titleEl ) {
|
||
var tpl = ( emcpToolsAdmin.applyKitTitle || 'Apply "%s" brand kit?' );
|
||
titleEl.textContent = tpl.replace( '%s', pending.title );
|
||
}
|
||
var bk = modal.querySelector( '.elementor-mcp-brand-kit-modal__backup-input' );
|
||
if ( bk ) {
|
||
bk.checked = true;
|
||
}
|
||
modal.hidden = false;
|
||
} );
|
||
|
||
modal.addEventListener( 'click', function ( e ) {
|
||
if ( e.target.closest( '[data-modal-dismiss]' ) ) {
|
||
closeModal();
|
||
return;
|
||
}
|
||
var confirmBtn = e.target.closest( '.elementor-mcp-brand-kit-modal__confirm' );
|
||
if ( ! confirmBtn || ! pending ) {
|
||
return;
|
||
}
|
||
|
||
var backup = modal.querySelector( '.elementor-mcp-brand-kit-modal__backup-input' );
|
||
var doBackup = backup ? backup.checked : true;
|
||
var title = pending.title;
|
||
var orig = confirmBtn.textContent;
|
||
confirmBtn.disabled = true;
|
||
confirmBtn.textContent = emcpToolsAdmin.applying || 'Applying…';
|
||
|
||
var body = new URLSearchParams();
|
||
body.append( 'action', 'emcp_tools_apply_pro_brand_kit' );
|
||
body.append( 'nonce', grid.getAttribute( 'data-apply-nonce' ) || '' );
|
||
body.append( 'kit_slug', pending.slug );
|
||
body.append( 'category_slug', pending.cat );
|
||
body.append( 'backup', doBackup ? '1' : '0' );
|
||
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: body.toString()
|
||
} )
|
||
.then( function ( r ) { return r.json(); } )
|
||
.then( function ( res ) {
|
||
confirmBtn.disabled = false;
|
||
confirmBtn.textContent = orig;
|
||
if ( res && res.success ) {
|
||
closeModal();
|
||
var applied = ( emcpToolsAdmin.kitApplied || '%s applied.' ).replace( '%s', title );
|
||
showBrandKitToast( applied, res.data && res.data.view_url );
|
||
} else {
|
||
var msg = ( res && res.data && res.data.message ) ? res.data.message : 'Apply failed.';
|
||
window.alert( msg );
|
||
}
|
||
} )
|
||
.catch( function () {
|
||
confirmBtn.disabled = false;
|
||
confirmBtn.textContent = orig;
|
||
window.alert( 'Apply failed. Check your connection and try again.' );
|
||
} );
|
||
} );
|
||
}
|
||
|
||
// Restore from backup.
|
||
var restore = root.querySelector( '.elementor-mcp-brand-kit-restore' );
|
||
if ( restore ) {
|
||
var restoreBtn = restore.querySelector( '.elementor-mcp-brand-kit-restore-btn' );
|
||
if ( restoreBtn ) {
|
||
restoreBtn.addEventListener( 'click', function () {
|
||
var select = restore.querySelector( '.elementor-mcp-brand-kit-backup-select' );
|
||
var clobber = restore.querySelector( '.elementor-mcp-brand-kit-clobber-input' );
|
||
if ( ! select || ! select.value ) {
|
||
return;
|
||
}
|
||
if ( ! window.confirm( emcpToolsAdmin.restoreConfirm || 'Restore global colors and typography from this backup?' ) ) {
|
||
return;
|
||
}
|
||
var orig = restoreBtn.textContent;
|
||
restoreBtn.disabled = true;
|
||
restoreBtn.textContent = emcpToolsAdmin.restoring || 'Restoring…';
|
||
|
||
var body = new URLSearchParams();
|
||
body.append( 'action', 'emcp_tools_restore_pro_brand_kit' );
|
||
body.append( 'nonce', restore.getAttribute( 'data-restore-nonce' ) || '' );
|
||
body.append( 'backup_id', select.value );
|
||
body.append( 'full_clobber', ( clobber && clobber.checked ) ? '1' : '0' );
|
||
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: body.toString()
|
||
} )
|
||
.then( function ( r ) { return r.json(); } )
|
||
.then( function ( res ) {
|
||
restoreBtn.disabled = false;
|
||
restoreBtn.textContent = orig;
|
||
if ( res && res.success ) {
|
||
var msg = ( res.data && res.data.message ) ? res.data.message : 'Restored.';
|
||
showBrandKitToast( msg, res.data && res.data.view_url );
|
||
} else {
|
||
var emsg = ( res && res.data && res.data.message ) ? res.data.message : 'Restore failed.';
|
||
window.alert( emsg );
|
||
}
|
||
} )
|
||
.catch( function () {
|
||
restoreBtn.disabled = false;
|
||
restoreBtn.textContent = orig;
|
||
window.alert( 'Restore failed. Check your connection and try again.' );
|
||
} );
|
||
} );
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Wire pagination (and the category filter it owns) into each library grid.
|
||
* Each call no-ops when its grid isn't on the current page, so it's safe to
|
||
* run all three regardless of which tab rendered.
|
||
*/
|
||
function initPagers() {
|
||
initGridPagination( {
|
||
gridSelector: '.elementor-mcp-pro-prompts-grid',
|
||
cardSelector: '.elementor-mcp-pro-prompt-card',
|
||
filterSelector: '.elementor-mcp-pro-prompts .elementor-mcp-pro-filters',
|
||
pageSize: 12,
|
||
label: 'prompts'
|
||
} );
|
||
initGridPagination( {
|
||
gridSelector: '.elementor-mcp-template-grid',
|
||
cardSelector: '.elementor-mcp-template-card',
|
||
filterSelector: '.elementor-mcp-templates .elementor-mcp-pro-filters',
|
||
pageSize: 12,
|
||
label: 'templates'
|
||
} );
|
||
initGridPagination( {
|
||
gridSelector: '.elementor-mcp-brand-kit-grid',
|
||
cardSelector: '.elementor-mcp-brand-kit-card',
|
||
filterSelector: '.elementor-mcp-brand-kits .elementor-mcp-pro-filters',
|
||
pageSize: 12,
|
||
label: 'brand kits'
|
||
} );
|
||
initGridPagination( {
|
||
gridSelector: '.elementor-mcp-changelog-list',
|
||
cardSelector: '.elementor-mcp-changelog-version',
|
||
pageSize: 10,
|
||
label: 'releases'
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Slide-in code viewer overlay. Any element with [data-emcp-code-view] opens
|
||
* it; the code is read from the nearest .emcp-code-src in the same table cell
|
||
* (or a selector in the attribute's value). Provides Copy + Download.
|
||
*/
|
||
function initCodeOverlay() {
|
||
if ( document.getElementById( 'emcp-code-overlay' ) ) {
|
||
return;
|
||
}
|
||
var overlay = document.createElement( 'div' );
|
||
overlay.id = 'emcp-code-overlay';
|
||
overlay.className = 'emcp-code-overlay';
|
||
overlay.innerHTML =
|
||
'<div class="emcp-code-overlay__backdrop" data-emcp-close></div>' +
|
||
'<div class="emcp-code-overlay__panel" role="dialog" aria-modal="true" aria-label="Code viewer">' +
|
||
'<div class="emcp-code-overlay__header">' +
|
||
'<span class="emcp-code-overlay__title"></span>' +
|
||
'<button type="button" class="emcp-code-overlay__close" data-emcp-close aria-label="Close">×</button>' +
|
||
'</div>' +
|
||
'<div class="emcp-code-overlay__toolbar">' +
|
||
'<button type="button" class="emcp-code-overlay__btn" data-emcp-copy></button>' +
|
||
'<button type="button" class="emcp-code-overlay__btn" data-emcp-download></button>' +
|
||
'</div>' +
|
||
'<pre class="emcp-code-overlay__body"><code></code></pre>' +
|
||
'</div>';
|
||
document.body.appendChild( overlay );
|
||
|
||
var titleEl = overlay.querySelector( '.emcp-code-overlay__title' );
|
||
var codeEl = overlay.querySelector( '.emcp-code-overlay__body code' );
|
||
var copyBtn = overlay.querySelector( '[data-emcp-copy]' );
|
||
var dlBtn = overlay.querySelector( '[data-emcp-download]' );
|
||
copyBtn.textContent = window.emcpToolsAdmin && window.emcpToolsAdmin.copy ? window.emcpToolsAdmin.copy : 'Copy';
|
||
dlBtn.textContent = window.emcpToolsAdmin && window.emcpToolsAdmin.download ? window.emcpToolsAdmin.download : 'Download';
|
||
var filename = 'code.txt';
|
||
|
||
function open( title, code, fname ) {
|
||
titleEl.textContent = title || 'Code';
|
||
codeEl.textContent = code || '';
|
||
filename = fname || 'code.txt';
|
||
copyBtn.textContent = window.emcpToolsAdmin && window.emcpToolsAdmin.copy ? window.emcpToolsAdmin.copy : 'Copy';
|
||
overlay.classList.add( 'is-open' );
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
function close() {
|
||
overlay.classList.remove( 'is-open' );
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
// Open from any trigger.
|
||
document.addEventListener( 'click', function ( e ) {
|
||
var trigger = e.target.closest( '[data-emcp-code-view]' );
|
||
if ( ! trigger ) { return; }
|
||
e.preventDefault();
|
||
var sel = trigger.getAttribute( 'data-emcp-code-view' );
|
||
var src = null;
|
||
if ( sel ) { src = document.querySelector( sel ); }
|
||
if ( ! src ) {
|
||
var scope = trigger.closest( 'td' ) || trigger.parentNode;
|
||
src = scope ? scope.querySelector( '.emcp-code-src' ) : null;
|
||
}
|
||
open(
|
||
trigger.getAttribute( 'data-emcp-code-title' ) || 'Code',
|
||
src ? src.textContent : '',
|
||
trigger.getAttribute( 'data-emcp-code-filename' ) || 'code.txt'
|
||
);
|
||
} );
|
||
|
||
overlay.addEventListener( 'click', function ( e ) {
|
||
if ( e.target.closest( '[data-emcp-close]' ) ) { close(); }
|
||
} );
|
||
document.addEventListener( 'keydown', function ( e ) {
|
||
if ( 'Escape' === e.key && overlay.classList.contains( 'is-open' ) ) { close(); }
|
||
} );
|
||
|
||
copyBtn.addEventListener( 'click', function () {
|
||
var text = codeEl.textContent || '';
|
||
var done = function () {
|
||
copyBtn.textContent = window.emcpToolsAdmin && window.emcpToolsAdmin.copied ? window.emcpToolsAdmin.copied : 'Copied!';
|
||
setTimeout( function () { copyBtn.textContent = window.emcpToolsAdmin && window.emcpToolsAdmin.copy ? window.emcpToolsAdmin.copy : 'Copy'; }, 1500 );
|
||
};
|
||
if ( navigator.clipboard && navigator.clipboard.writeText ) {
|
||
navigator.clipboard.writeText( text ).then( done ).catch( function () { fallbackCopy( text, codeEl ); done(); } );
|
||
} else {
|
||
fallbackCopy( text, codeEl );
|
||
done();
|
||
}
|
||
} );
|
||
|
||
dlBtn.addEventListener( 'click', function () {
|
||
var blob = new Blob( [ codeEl.textContent || '' ], { type: 'text/plain' } );
|
||
var url = URL.createObjectURL( blob );
|
||
var a = document.createElement( 'a' );
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild( a );
|
||
a.click();
|
||
document.body.removeChild( a );
|
||
setTimeout( function () { URL.revokeObjectURL( url ); }, 1000 );
|
||
} );
|
||
}
|
||
|
||
/**
|
||
* Clipboard fallback (older browsers / insecure context): select the code
|
||
* node and execCommand('copy').
|
||
*
|
||
* @param {string} text Text to copy.
|
||
* @param {HTMLElement} node Element whose text can be range-selected.
|
||
*/
|
||
function fallbackCopy( text, node ) {
|
||
try {
|
||
var range = document.createRange();
|
||
range.selectNodeContents( node );
|
||
var sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange( range );
|
||
document.execCommand( 'copy' );
|
||
sel.removeAllRanges();
|
||
} catch ( e ) {}
|
||
}
|
||
|
||
/**
|
||
* Copy text to the clipboard (Clipboard API with an execCommand fallback for
|
||
* older browsers / insecure contexts). Returns a Promise that always resolves.
|
||
*
|
||
* @param {string} text Text to copy.
|
||
* @return {Promise}
|
||
*/
|
||
function emcpCopyText( text ) {
|
||
return new Promise( function ( resolve ) {
|
||
if ( navigator.clipboard && navigator.clipboard.writeText ) {
|
||
navigator.clipboard.writeText( text ).then( resolve ).catch( function () {
|
||
emcpExecCopy( text );
|
||
resolve();
|
||
} );
|
||
} else {
|
||
emcpExecCopy( text );
|
||
resolve();
|
||
}
|
||
} );
|
||
}
|
||
|
||
function emcpExecCopy( text ) {
|
||
try {
|
||
var ta = document.createElement( 'textarea' );
|
||
ta.value = text;
|
||
ta.style.position = 'fixed';
|
||
ta.style.opacity = '0';
|
||
document.body.appendChild( ta );
|
||
ta.select();
|
||
document.execCommand( 'copy' );
|
||
document.body.removeChild( ta );
|
||
} catch ( e ) {}
|
||
}
|
||
|
||
/**
|
||
* Click-to-copy for any [data-emcp-copy-text] element (e.g. a shortcode
|
||
* chip). Copies the attribute value (or the element text) and flashes a
|
||
* "Copied!" tooltip via the .is-copied class.
|
||
*/
|
||
function initClickToCopy() {
|
||
document.addEventListener( 'click', function ( e ) {
|
||
var el = e.target.closest( '[data-emcp-copy-text]' );
|
||
if ( ! el ) { return; }
|
||
var text = el.getAttribute( 'data-emcp-copy-text' ) || el.textContent || '';
|
||
emcpCopyText( text ).then( function () {
|
||
el.classList.add( 'is-copied' );
|
||
clearTimeout( el._emcpCopiedTimer );
|
||
el._emcpCopiedTimer = setTimeout( function () { el.classList.remove( 'is-copied' ); }, 1200 );
|
||
} );
|
||
} );
|
||
document.addEventListener( 'keydown', function ( e ) {
|
||
if ( ( 'Enter' === e.key || ' ' === e.key ) && e.target && e.target.matches && e.target.matches( '[data-emcp-copy-text]' ) ) {
|
||
e.preventDefault();
|
||
e.target.click();
|
||
}
|
||
} );
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Connection tab — client picker helpers
|
||
// -------------------------------------------------------------------------
|
||
|
||
function emcpEscapeHtml( s ) {
|
||
return String( s ).replace( /[&<>"']/g, function ( c ) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ c ];
|
||
} );
|
||
}
|
||
|
||
function emcpClientById( id ) {
|
||
var list = ( emcpToolsAdmin.connectionClients || [] );
|
||
for ( var i = 0; i < list.length; i++ ) { if ( list[ i ].id === id ) { return list[ i ]; } }
|
||
return null;
|
||
}
|
||
|
||
// Per-site MCP server name derived from the domain, so multiple connected sites
|
||
// don't collide in a client's config (e.g. "emcp-sociable-sylvain-z29-zipwp-dev").
|
||
function emcpServerName() {
|
||
var host = '';
|
||
try { host = new URL( window.emcpConn.siteUrl || window.emcpConn.endpoint ).hostname; } catch ( e ) { host = ''; }
|
||
host = ( host || '' ).replace( /^www\./, '' ).replace( /[^a-zA-Z0-9]+/g, '-' ).replace( /^-+|-+$/g, '' ).toLowerCase();
|
||
return host ? ( 'emcp-' + host ) : 'emcp-tools';
|
||
}
|
||
|
||
// Build the JSON config object for a given client + json-variant key.
|
||
function emcpJsonConfig( variant ) {
|
||
var c = window.emcpConn;
|
||
var key = emcpServerName();
|
||
var npx = { command: 'npx', args: [ '-y', '@msrbuilds/emcp-proxy@latest' ],
|
||
env: { WP_URL: c.siteUrl, WP_USERNAME: c.username, WP_APP_PASSWORD: c.appPassword, MCP_PROTOCOL_VERSION: '2024-11-05' } };
|
||
var http = { type: 'http', url: c.endpoint, headers: { Authorization: 'Basic ' + c.b64 } };
|
||
var servers = {};
|
||
if ( variant === 'npx' ) { servers[ key ] = Object.assign( { type: 'stdio' }, npx ); return { mcpServers: servers }; }
|
||
if ( variant === 'http' ) { servers[ key ] = http; return { mcpServers: servers }; }
|
||
if ( variant === 'antigravity-http' ) {
|
||
servers[ key ] = { serverUrl: c.endpoint, headers: { Authorization: 'Basic ' + c.b64 } };
|
||
return { mcpServers: servers };
|
||
}
|
||
if ( variant === 'remote' ) {
|
||
servers[ key ] = { command: 'npx', args: [ '-y', 'mcp-remote', c.endpoint, '--header', 'Authorization: Basic ' + c.b64 ] };
|
||
return { mcpServers: servers };
|
||
}
|
||
return { mcpServers: servers };
|
||
}
|
||
|
||
// OpenClaw ~/.openclaw/openclaw.json — the server lives under mcp.servers (NOT
|
||
// the top-level mcpServers other clients use). openclaw.json almost always
|
||
// already has other top-level keys, so we emit the "mcp" PROPERTY to merge in
|
||
// rather than a full { … } object that would clobber the file.
|
||
function emcpOpenclawConfig( variant ) {
|
||
var c = window.emcpConn, n = emcpServerName(), server;
|
||
if ( variant === 'npx' ) {
|
||
server = { command: 'npx', args: [ '-y', '@msrbuilds/emcp-proxy@latest' ],
|
||
env: { WP_URL: c.siteUrl, WP_USERNAME: c.username, WP_APP_PASSWORD: c.appPassword, MCP_PROTOCOL_VERSION: '2024-11-05' } };
|
||
} else {
|
||
server = { url: c.endpoint, transport: 'streamable-http', headers: { Authorization: 'Basic ' + c.b64 } };
|
||
}
|
||
var inner = { servers: {} };
|
||
inner.servers[ n ] = server;
|
||
return '"mcp": ' + JSON.stringify( inner, null, 4 );
|
||
}
|
||
|
||
// Hermes ~/.hermes/config.yaml — mcp_servers (YAML). Hand-rendered so the output
|
||
// matches Hermes' documented shape exactly.
|
||
function emcpHermesConfig( variant ) {
|
||
var c = window.emcpConn, n = emcpServerName();
|
||
if ( variant === 'npx' ) {
|
||
return 'mcp_servers:\n' +
|
||
' ' + n + ':\n' +
|
||
' command: "npx"\n' +
|
||
' args: ["-y", "@msrbuilds/emcp-proxy@latest"]\n' +
|
||
' env:\n' +
|
||
' WP_URL: "' + c.siteUrl + '"\n' +
|
||
' WP_USERNAME: "' + c.username + '"\n' +
|
||
' WP_APP_PASSWORD: "' + c.appPassword + '"\n' +
|
||
' MCP_PROTOCOL_VERSION: "2024-11-05"';
|
||
}
|
||
return 'mcp_servers:\n' +
|
||
' ' + n + ':\n' +
|
||
' url: "' + c.endpoint + '"\n' +
|
||
' headers:\n' +
|
||
' Authorization: "Basic ' + c.b64 + '"';
|
||
}
|
||
|
||
// Codex config.toml — streamable HTTP (expects `http_headers`, an inline table).
|
||
function emcpTomlConfig() {
|
||
var c = window.emcpConn, n = emcpServerName();
|
||
return '[mcp_servers.' + n + ']\n' +
|
||
'url = "' + c.endpoint + '"\n' +
|
||
'http_headers = { "Authorization" = "Basic ' + c.b64 + '" }';
|
||
}
|
||
|
||
// Codex config.toml — Node proxy over stdio (free npx alternative; robust with
|
||
// clients that struggle with the streamable-HTTP session handshake).
|
||
function emcpTomlStdioConfig() {
|
||
var c = window.emcpConn, n = emcpServerName();
|
||
return '[mcp_servers.' + n + ']\n' +
|
||
'command = "npx"\n' +
|
||
'args = ["-y", "@msrbuilds/emcp-proxy@latest"]\n\n' +
|
||
'[mcp_servers.' + n + '.env]\n' +
|
||
'WP_URL = "' + c.siteUrl + '"\n' +
|
||
'WP_USERNAME = "' + c.username + '"\n' +
|
||
'WP_APP_PASSWORD = "' + c.appPassword + '"\n' +
|
||
'MCP_PROTOCOL_VERSION = "2024-11-05"';
|
||
}
|
||
|
||
// Render one copy/download block.
|
||
function emcpBlock( title, bodyHtml ) {
|
||
return '<div class="elementor-mcp-config-card"><div class="elementor-mcp-config-card-header">' +
|
||
'<span class="elementor-mcp-config-card-title">' + emcpEscapeHtml( title ) + '</span></div>' +
|
||
'<div class="elementor-mcp-config-card-body">' + bodyHtml + '</div></div>';
|
||
}
|
||
|
||
function emcpCopyBlock( title, text ) {
|
||
var id = 'emcp-opt-' + Math.abs( ( title + text ).length );
|
||
return '<div class="elementor-mcp-config-card"><div class="elementor-mcp-config-card-header">' +
|
||
'<span class="elementor-mcp-config-card-title">' + emcpEscapeHtml( title ) + '</span>' +
|
||
'<button type="button" class="button elementor-mcp-copy-btn" data-target="' + id + '">Copy</button></div>' +
|
||
'<pre><code>' + emcpEscapeHtml( text ) + '</code></pre>' +
|
||
'<textarea id="' + id + '" class="elementor-mcp-copy-source">' + emcpEscapeHtml( text ) + '</textarea></div>';
|
||
}
|
||
|
||
// The selected authentication method (falls back to whatever is available).
|
||
function emcpAuthMethod() {
|
||
var r = document.querySelector( 'input[name="emcp_auth_method"]:checked' );
|
||
if ( r ) { return r.value; }
|
||
return ( typeof emcpToolsAdmin !== 'undefined' && emcpToolsAdmin.oauthEnabled ) ? 'oauth' : 'app-password';
|
||
}
|
||
|
||
// --- OAuth setup rendering (no credentials — browser sign-in supplies auth) ---
|
||
function emcpFill( tpl, name, endpoint ) {
|
||
return String( tpl ).replace( /%NAME%/g, name ).replace( /%ENDPOINT%/g, endpoint );
|
||
}
|
||
function emcpStep( title, descHtml ) {
|
||
return '<p class="emcp-oauth-step"><strong>' + emcpEscapeHtml( title ) + '</strong></p>' +
|
||
( descHtml ? '<p class="description emcp-oauth-desc">' + descHtml + '</p>' : '' );
|
||
}
|
||
function emcpSigninText() {
|
||
return ( typeof emcpToolsAdmin !== 'undefined' && emcpToolsAdmin.oauthSignin ) ||
|
||
'The next time your AI client connects, your browser opens so you can authorize it. Approve to finish connecting.';
|
||
}
|
||
function emcpOAuthDeeplink( kind, name, endpoint, label ) {
|
||
var url = '';
|
||
if ( kind === 'cursor' ) {
|
||
url = 'cursor://anysphere.cursor-deeplink/mcp/install?name=' + encodeURIComponent( name ) +
|
||
'&config=' + btoa( JSON.stringify( { url: endpoint } ) );
|
||
} else if ( kind === 'claude-ai' ) {
|
||
url = 'https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=' +
|
||
encodeURIComponent( name ) + '&connectorUrl=' + encodeURIComponent( endpoint );
|
||
}
|
||
if ( ! url ) { return ''; }
|
||
return '<p><a class="button button-primary emcp-oauth-deeplink" href="' + emcpEscapeHtml( url ) + '">' +
|
||
emcpEscapeHtml( label ) + '</a></p>';
|
||
}
|
||
|
||
function emcpRenderOAuth( client ) {
|
||
var endpoint = ( window.emcpConn && window.emcpConn.endpoint ) || emcpToolsAdmin.mcpEndpoint;
|
||
var name = emcpServerName();
|
||
var o = client.oauth;
|
||
var out = '';
|
||
|
||
if ( ! o || typeof o !== 'object' ) {
|
||
out += emcpCopyBlock( 'a. Add this site as an HTTP MCP server (paste the URL)', endpoint );
|
||
return out + emcpStep( 'b. Sign in', emcpEscapeHtml( emcpSigninText() ) );
|
||
}
|
||
|
||
if ( o.type === 'cmd' ) {
|
||
out += emcpStep( 'a. Run this in your terminal' );
|
||
out += emcpCopyBlock( '', emcpFill( o.cmd, name, endpoint ) );
|
||
return out + emcpStep( 'b. Sign in', emcpEscapeHtml( emcpSigninText() ) );
|
||
}
|
||
|
||
if ( o.type === 'connector' ) {
|
||
if ( o.deeplink ) { out += emcpOAuthDeeplink( o.deeplink, name, endpoint, 'Add the connector to ' + ( o.app || 'your client' ) ); }
|
||
if ( o.note ) { out += '<p class="description">' + emcpEscapeHtml( o.note ) + '</p>'; }
|
||
out += emcpStep( 'a. Open Connectors', emcpEscapeHtml( 'In ' + ( o.app || 'your client' ) + ', open Settings and go to Connectors.' ) );
|
||
out += emcpStep( 'b. Add a custom connector — give it this name' );
|
||
out += emcpCopyBlock( '', name );
|
||
out += emcpStep( 'c. Enter the server URL', emcpEscapeHtml( 'Paste the URL below and save. Leave the OAuth Client ID and Secret (under Advanced settings) empty, then sign in when the browser opens.' ) );
|
||
out += emcpCopyBlock( '', endpoint );
|
||
return out;
|
||
}
|
||
|
||
if ( o.type === 'steps' ) {
|
||
( o.steps || [] ).forEach( function ( s ) {
|
||
if ( s.copy ) {
|
||
out += emcpCopyBlock( s.title || '', emcpFill( s.copy, name, endpoint ) );
|
||
} else {
|
||
out += emcpStep( s.title || '', s.desc ? emcpEscapeHtml( emcpFill( s.desc, name, endpoint ) ) : '' );
|
||
}
|
||
} );
|
||
return out;
|
||
}
|
||
|
||
if ( o.type === 'config' ) {
|
||
if ( o.deeplink ) { out += emcpOAuthDeeplink( o.deeplink, name, endpoint, 'One-click install' ); }
|
||
var paths = '';
|
||
( o.paths || [] ).forEach( function ( p ) {
|
||
paths += '<code>' + emcpEscapeHtml( p.path ) + '</code> <span class="emcp-oauth-path-label">' + emcpEscapeHtml( p.label || '' ) + '</span><br />';
|
||
} );
|
||
out += emcpStep( 'a. Open your config', paths );
|
||
out += emcpStep( 'b. Add this server', emcpEscapeHtml( o.merge_msg || 'If your config file already has content, merge this into it instead of replacing it.' ) );
|
||
out += emcpCopyBlock( '', emcpFill( o.template, name, endpoint ) );
|
||
if ( o.note ) { out += '<p class="description">' + emcpEscapeHtml( emcpFill( o.note, name, endpoint ) ) + '</p>'; }
|
||
return out + emcpStep( 'c. Restart and sign in', emcpEscapeHtml( emcpSigninText() ) );
|
||
}
|
||
|
||
out += emcpCopyBlock( 'a. Connector URL', endpoint );
|
||
return out + emcpStep( 'b. Sign in', emcpEscapeHtml( emcpSigninText() ) );
|
||
}
|
||
|
||
function emcpSelectClient( id ) {
|
||
var client = emcpClientById( id );
|
||
if ( ! client ) { return; }
|
||
if ( ! window.emcpConn ) { window.emcpConn = { endpoint: emcpToolsAdmin.mcpEndpoint }; }
|
||
window.localStorage.setItem( 'emcpConnClient', id );
|
||
|
||
// toggle card selected state
|
||
var cards = document.querySelectorAll( '.elementor-mcp-client-card' );
|
||
for ( var i = 0; i < cards.length; i++ ) {
|
||
var on = cards[ i ].getAttribute( 'data-client' ) === id;
|
||
cards[ i ].classList.toggle( 'is-selected', on );
|
||
cards[ i ].setAttribute( 'aria-selected', on ? 'true' : 'false' );
|
||
}
|
||
|
||
var heading = document.getElementById( 'elementor-mcp-connect-heading' );
|
||
var nameEl = document.getElementById( 'elementor-mcp-connect-client-name' );
|
||
if ( heading ) { heading.style.display = ''; }
|
||
if ( nameEl ) { nameEl.textContent = client.label; }
|
||
|
||
var html = '';
|
||
var m = client.methods;
|
||
var method = emcpAuthMethod();
|
||
|
||
if ( method === 'oauth' ) {
|
||
// OAuth mode: no credentials — connect command + browser sign-in.
|
||
html = emcpRenderOAuth( client );
|
||
} else if ( ! window.emcpConn.b64 ) {
|
||
// App-password mode needs generated credentials first.
|
||
html = '<p class="description">' +
|
||
emcpEscapeHtml( ( emcpToolsAdmin.genFirst || 'Generate your credentials above — the config for %s then appears here.' ).replace( '%s', client.label ) ) +
|
||
'</p>';
|
||
} else {
|
||
|
||
// 0) Client-specific setup guide (e.g. Codex's custom-MCP form mapping).
|
||
// %ENDPOINT%/%B64% are filled with the live, escaped values.
|
||
if ( client.guide ) {
|
||
var guide = client.guide
|
||
.replace( /%NAME%/g, emcpEscapeHtml( emcpServerName() ) )
|
||
.replace( /%ENDPOINT%/g, emcpEscapeHtml( window.emcpConn.endpoint ) )
|
||
.replace( /%B64%/g, emcpEscapeHtml( window.emcpConn.b64 ) );
|
||
html += emcpBlock( client.guide_title || 'Setup guide', guide );
|
||
}
|
||
|
||
// 1) Bundle (.mcpb)
|
||
if ( m.bundle ) {
|
||
html += emcpBlock( 'One-click bundle (.mcpb)',
|
||
'<p class="description">Download and double-click to install in Claude Desktop — no config files to edit.</p>' +
|
||
'<p class="elementor-mcp-mcpb-warning"><span class="dashicons dashicons-warning" aria-hidden="true"></span> ' +
|
||
'<strong>Treat this file as a secret.</strong> It embeds your WordPress application password in plaintext, so anyone with the file can access this site. ' +
|
||
'Don\'t email it, share it, commit it to git, or leave it in a cloud-synced folder — and delete it once it\'s imported into Claude Desktop.</p>' +
|
||
'<p><button type="button" class="button button-primary" id="elementor-mcp-mcpb-download">Download .mcpb bundle</button></p>' );
|
||
}
|
||
// 2) CLI command
|
||
if ( m.cli ) {
|
||
var cli = m.cli.replace( /%NAME%/g, emcpServerName() ).replace( /%ENDPOINT%/g, window.emcpConn.endpoint ).replace( /%B64%/g, window.emcpConn.b64 );
|
||
html += emcpCopyBlock( 'Terminal command', cli );
|
||
}
|
||
// 3) AI setup prompt
|
||
if ( m.ai_prompt ) {
|
||
var prompt = 'Add an MCP server named "' + emcpServerName() + '" at ' + window.emcpConn.endpoint +
|
||
' using the HTTP transport with header Authorization: Basic ' + window.emcpConn.b64;
|
||
html += emcpCopyBlock( 'Ask your AI to set it up (paste into chat)', prompt );
|
||
}
|
||
// 4) Manual JSON / TOML
|
||
( m.json || [] ).forEach( function ( variant ) {
|
||
if ( variant === 'toml' ) { html += emcpCopyBlock( 'Manual config — direct HTTP (config.toml)', emcpTomlConfig() ); }
|
||
else if ( variant === 'toml-stdio' ) { html += emcpCopyBlock( 'Manual config — Node proxy / npx (config.toml)', emcpTomlStdioConfig() ); }
|
||
else if ( variant === 'openclaw-http' ) { html += emcpCopyBlock( 'Manual config — direct HTTP (openclaw.json)', emcpOpenclawConfig( 'http' ) ); }
|
||
else if ( variant === 'openclaw-npx' ) { html += emcpCopyBlock( 'Manual config — Node proxy / npx (openclaw.json)', emcpOpenclawConfig( 'npx' ) ); }
|
||
else if ( variant === 'hermes-http' ) { html += emcpCopyBlock( 'Manual config — direct HTTP (config.yaml)', emcpHermesConfig( 'http' ) ); }
|
||
else if ( variant === 'hermes-npx' ) { html += emcpCopyBlock( 'Manual config — Node proxy / npx (config.yaml)', emcpHermesConfig( 'npx' ) ); }
|
||
else if ( variant === 'antigravity-http' ) { html += emcpCopyBlock( 'Manual config — direct HTTP (mcp_config.json)', JSON.stringify( emcpJsonConfig( 'antigravity-http' ), null, 4 ) ); }
|
||
else {
|
||
var label = variant === 'npx' ? 'Manual config — Node proxy (npx)'
|
||
: variant === 'http' ? 'Manual config — direct HTTP'
|
||
: 'Manual config — npx mcp-remote';
|
||
html += emcpCopyBlock( label, JSON.stringify( emcpJsonConfig( variant ), null, 4 ) );
|
||
}
|
||
} );
|
||
} // /else (app-password mode with generated credentials)
|
||
|
||
var host = document.getElementById( 'elementor-mcp-client-options' );
|
||
if ( host ) { host.innerHTML = html; }
|
||
|
||
// Wire the .mcpb download button (if present) to submit the hidden form.
|
||
var dl = document.getElementById( 'elementor-mcp-mcpb-download' );
|
||
if ( dl ) {
|
||
dl.addEventListener( 'click', function () {
|
||
document.getElementById( 'elementor-mcp-mcpb-user-id' ).value = window.emcpConn.userId;
|
||
document.getElementById( 'elementor-mcp-mcpb-app-password' ).value = window.emcpConn.appPassword;
|
||
document.getElementById( 'elementor-mcp-mcpb-form' ).submit();
|
||
} );
|
||
}
|
||
}
|
||
|
||
// Delegate card clicks.
|
||
document.addEventListener( 'click', function ( e ) {
|
||
var card = e.target.closest ? e.target.closest( '.elementor-mcp-client-card' ) : null;
|
||
if ( card ) { emcpSelectClient( card.getAttribute( 'data-client' ) ); }
|
||
} );
|
||
|
||
// Context page: char/token counter, starter template, live preview.
|
||
function initContextPage() {
|
||
var ta = document.getElementById( 'elementor-mcp-context-text' );
|
||
if ( ! ta ) { return; }
|
||
var counter = document.getElementById( 'elementor-mcp-context-counter' );
|
||
var preview = document.getElementById( 'elementor-mcp-context-preview' );
|
||
var toggle = document.querySelector( 'input[name="emcp_tools_site_context_enabled"]' );
|
||
var tplBtn = document.getElementById( 'elementor-mcp-context-template' );
|
||
var max = parseInt( ta.getAttribute( 'maxlength' ) || '20000', 10 );
|
||
var base = ( emcpToolsAdmin && emcpToolsAdmin.siteContextBase ) || '';
|
||
var delim = ( emcpToolsAdmin && emcpToolsAdmin.siteContextDelimiter ) || '\n\n## Site context\n\n';
|
||
|
||
function refresh() {
|
||
var len = ta.value.length;
|
||
var tokens = Math.ceil( len / 4 );
|
||
if ( counter ) {
|
||
counter.textContent = len + ' characters · ~' + tokens + ' tokens';
|
||
counter.classList.toggle( 'is-warn', len > max * 0.9 );
|
||
}
|
||
if ( preview ) {
|
||
var ctx = ta.value.replace( /^\s+|\s+$/g, '' );
|
||
var on = ! toggle || toggle.checked;
|
||
preview.textContent = ( on && ctx ) ? ( base + delim + ctx ) : base;
|
||
}
|
||
}
|
||
|
||
ta.addEventListener( 'input', refresh );
|
||
if ( toggle ) { toggle.addEventListener( 'change', refresh ); }
|
||
if ( tplBtn ) {
|
||
tplBtn.addEventListener( 'click', function () {
|
||
if ( ta.value.trim() && ! window.confirm( 'Replace the current context with the starter template?' ) ) { return; }
|
||
ta.value = emcpContextTemplate();
|
||
refresh();
|
||
ta.focus();
|
||
} );
|
||
}
|
||
refresh();
|
||
}
|
||
|
||
function emcpContextTemplate() {
|
||
return [
|
||
'# About this site',
|
||
'',
|
||
'## Business identity',
|
||
'- Name:',
|
||
'- What we do:',
|
||
'- Primary audience:',
|
||
'',
|
||
'## Brand voice & tone',
|
||
'- ',
|
||
'',
|
||
'## Content & SEO rules',
|
||
'- ',
|
||
'',
|
||
'## Technical / Elementor constraints',
|
||
'- ',
|
||
'',
|
||
'## Guardrails (what NOT to do)',
|
||
'- '
|
||
].join( '\n' );
|
||
}
|
||
|
||
/**
|
||
* App-bar notifications bell: click-toggle dropdown (the neighboring Help
|
||
* menu is a pure-CSS hover/focus-within dropdown with no JS counterpart —
|
||
* the notif bell needs real JS so it can mark items read on open). Closes
|
||
* on outside click / Escape, and marks the currently-visible unread items
|
||
* read via admin-ajax the first time it's opened in a page view.
|
||
*/
|
||
function initNotifications() {
|
||
var wrap = document.querySelector( '.emcp-notif' );
|
||
if ( ! wrap ) { return; }
|
||
var toggle = wrap.querySelector( '.emcp-notif-toggle' );
|
||
var badge = wrap.querySelector( '.emcp-notif-badge' );
|
||
var overlay = wrap.querySelector( '.emcp-notif-overlay' );
|
||
var closeBtn = wrap.querySelector( '.emcp-notif-close' );
|
||
if ( ! toggle ) { return; }
|
||
|
||
var markedThisView = false;
|
||
|
||
function close() {
|
||
wrap.classList.remove( 'is-open' );
|
||
toggle.setAttribute( 'aria-expanded', 'false' );
|
||
}
|
||
|
||
function markVisibleRead() {
|
||
if ( markedThisView ) { return; }
|
||
|
||
var items = wrap.querySelectorAll( '.emcp-notif-item.is-unread[data-id]' );
|
||
if ( ! items.length ) { return; }
|
||
|
||
markedThisView = true;
|
||
|
||
var ids = [];
|
||
items.forEach( function ( item ) {
|
||
ids.push( item.getAttribute( 'data-id' ) );
|
||
item.classList.remove( 'is-unread' );
|
||
} );
|
||
|
||
if ( typeof emcpToolsAdmin === 'undefined' || ! emcpToolsAdmin.ajaxUrl ) { return; }
|
||
|
||
var payload = new FormData();
|
||
payload.append( 'action', 'emcp_tools_notifications_read' );
|
||
payload.append( 'nonce', toggle.getAttribute( 'data-nonce' ) || '' );
|
||
ids.forEach( function ( id ) {
|
||
payload.append( 'ids[]', id );
|
||
} );
|
||
|
||
/* global fetch */
|
||
fetch( emcpToolsAdmin.ajaxUrl, {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
body: payload
|
||
} ).then( function ( response ) {
|
||
return response.json();
|
||
} ).then( function ( result ) {
|
||
if ( result && result.success && badge ) {
|
||
var unread = result.data && typeof result.data.unread !== 'undefined' ? result.data.unread : 0;
|
||
badge.textContent = String( unread );
|
||
badge.classList.toggle( 'is-empty', 0 === unread );
|
||
}
|
||
} ).catch( function () {} );
|
||
}
|
||
|
||
function open() {
|
||
// Only one of help / notif open at a time.
|
||
var help = document.querySelector( '.emcp-help-menu' );
|
||
if ( help && document.activeElement && help.contains( document.activeElement ) ) {
|
||
document.activeElement.blur();
|
||
}
|
||
wrap.classList.add( 'is-open' );
|
||
toggle.setAttribute( 'aria-expanded', 'true' );
|
||
markVisibleRead();
|
||
}
|
||
|
||
toggle.addEventListener( 'click', function ( e ) {
|
||
e.stopPropagation();
|
||
if ( wrap.classList.contains( 'is-open' ) ) {
|
||
close();
|
||
} else {
|
||
open();
|
||
}
|
||
} );
|
||
|
||
// Drawer: clicking the overlay or the close button dismisses it.
|
||
if ( overlay ) { overlay.addEventListener( 'click', close ); }
|
||
if ( closeBtn ) { closeBtn.addEventListener( 'click', close ); }
|
||
|
||
document.addEventListener( 'click', function ( e ) {
|
||
if ( wrap.classList.contains( 'is-open' ) && ! wrap.contains( e.target ) ) {
|
||
close();
|
||
}
|
||
} );
|
||
|
||
document.addEventListener( 'keydown', function ( e ) {
|
||
if ( 'Escape' === e.key && wrap.classList.contains( 'is-open' ) ) {
|
||
close();
|
||
toggle.focus();
|
||
}
|
||
} );
|
||
}
|
||
|
||
// Initialize on DOM ready.
|
||
/**
|
||
* Header tab nav overflow controls: show prev/next arrows when the tabs
|
||
* exceed the viewport (small desktops), scrolling the nav on click. Touch
|
||
* devices still swipe natively. Arrows auto-hide at each end and when the
|
||
* nav fits. The active tab is scrolled into view on load.
|
||
*/
|
||
function initNavArrows() {
|
||
var wrap = document.querySelector( '.emcp-appnav-wrap' );
|
||
if ( ! wrap ) { return; }
|
||
var nav = wrap.querySelector( '.emcp-appnav' );
|
||
var prev = wrap.querySelector( '.emcp-appnav-arrow--prev' );
|
||
var next = wrap.querySelector( '.emcp-appnav-arrow--next' );
|
||
if ( ! nav || ! prev || ! next ) { return; }
|
||
|
||
function update() {
|
||
var max = nav.scrollWidth - nav.clientWidth;
|
||
var overflowing = max > 2;
|
||
prev.hidden = ! overflowing || nav.scrollLeft <= 1;
|
||
next.hidden = ! overflowing || nav.scrollLeft >= max - 1;
|
||
wrap.classList.toggle( 'is-overflowing', overflowing );
|
||
}
|
||
function step( dir ) {
|
||
nav.scrollBy( { left: dir * Math.max( 160, Math.round( nav.clientWidth * 0.7 ) ), behavior: 'smooth' } );
|
||
}
|
||
|
||
prev.addEventListener( 'click', function () { step( -1 ); } );
|
||
next.addEventListener( 'click', function () { step( 1 ); } );
|
||
nav.addEventListener( 'scroll', update, { passive: true } );
|
||
window.addEventListener( 'resize', update );
|
||
|
||
// Bring the active tab into view (in case it's off-screen on a narrow window).
|
||
var active = nav.querySelector( '.emcp-appnav-item.is-active' );
|
||
if ( active && active.scrollIntoView ) {
|
||
active.scrollIntoView( { inline: 'center', block: 'nearest' } );
|
||
}
|
||
update();
|
||
}
|
||
|
||
/**
|
||
* Wire the settings-saved toast that PHP already rendered, bottom right.
|
||
*
|
||
* WordPress stacks notices above the page, which shifts the whole screen
|
||
* down at the exact moment you have finished with the top of it and are
|
||
* looking at what you just changed. Bottom right keeps the confirmation
|
||
* near where the eye already is and stops the layout jumping.
|
||
*
|
||
* The markup arrives in its final position, so all this adds is the
|
||
* dismissing: a close button, an auto-hide, and a pause while it is being
|
||
* read. If this never runs the toast simply stays until the next page load,
|
||
* which is the right way round for a confirmation.
|
||
*
|
||
* Only this one notice is a toast. Every other notice on these screens is
|
||
* left where it is: some carry buttons or explain page state, and a corner
|
||
* that fades out is the wrong place for something you have to act on.
|
||
*/
|
||
function initSavedToast() {
|
||
var stack = document.querySelector( '.emcp-toasts' );
|
||
var toast = stack && stack.querySelector( '.emcp-toast' );
|
||
if ( ! toast ) {
|
||
return;
|
||
}
|
||
|
||
var timer = null;
|
||
|
||
function dismiss() {
|
||
window.clearTimeout( timer );
|
||
toast.classList.add( 'is-leaving' );
|
||
// Remove on a timer rather than on animationend: a background tab
|
||
// never fires the event, and the node would sit there forever.
|
||
window.setTimeout( function () {
|
||
if ( stack.parentNode ) {
|
||
stack.parentNode.removeChild( stack );
|
||
}
|
||
}, 260 );
|
||
}
|
||
|
||
var close = toast.querySelector( '.emcp-toast__close' );
|
||
if ( close ) {
|
||
close.addEventListener( 'click', dismiss );
|
||
}
|
||
timer = window.setTimeout( dismiss, 5000 );
|
||
|
||
// Reading it should not race the timer.
|
||
toast.addEventListener( 'mouseenter', function () {
|
||
window.clearTimeout( timer );
|
||
} );
|
||
toast.addEventListener( 'focusin', function () {
|
||
window.clearTimeout( timer );
|
||
} );
|
||
}
|
||
|
||
function initAll() {
|
||
initToolsForm();
|
||
initBase64Generator();
|
||
initCopyButtons();
|
||
initPagers();
|
||
initProSync();
|
||
initProTemplateActions();
|
||
initBrandKits();
|
||
initCodeOverlay();
|
||
initClickToCopy();
|
||
initContextPage();
|
||
initNavArrows();
|
||
initNotifications();
|
||
initSavedToast();
|
||
}
|
||
|
||
if ( document.readyState === 'loading' ) {
|
||
document.addEventListener( 'DOMContentLoaded', initAll );
|
||
} else {
|
||
initAll();
|
||
}
|
||
})();
|