Files
heretek-control-core/assets/js/admin.js
T

1905 lines
70 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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">&times;</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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ 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();
}
})();