Verbindung: Probleme vor der Anleitung. Tools: Suche, Filter, Alle ein-/ausklappen, Zurücksetzen je Gruppe. Divi-Reiter in Klartext mit Statussymbolen. Rollenhinweise, Hilfe-Tab, Reiter als Raster und Karten auf dem Handy, Kontrast, Tippflächen, einheitliche Du-Anrede. Bestätigungen über data-lit-mcp-confirm statt Inline-onclick. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
376 lines
12 KiB
JavaScript
376 lines
12 KiB
JavaScript
/* Einstellungen → LIT MCP: copy the server URL, live group feedback, search and reset for the
|
||
collapsible tool groups, filter paging, and questions before a destructive or locking-out save. */
|
||
( function () {
|
||
'use strict';
|
||
|
||
// "URL kopieren" – like WordPress' "Copy URL to clipboard" in the media library.
|
||
function setUpCopyButton() {
|
||
var hideTimeout;
|
||
|
||
if ( ! window.ClipboardJS || ! document.querySelector( '.lit-mcp-copy' ) ) {
|
||
return;
|
||
}
|
||
new window.ClipboardJS( '.lit-mcp-copy' ).on( 'success', function ( event ) {
|
||
var success = event.trigger.parentNode.querySelector( '.success' );
|
||
|
||
event.clearSelection();
|
||
event.trigger.focus();
|
||
if ( success ) {
|
||
clearTimeout( hideTimeout );
|
||
success.classList.remove( 'hidden' );
|
||
hideTimeout = setTimeout( function () {
|
||
success.classList.add( 'hidden' );
|
||
}, 3000 );
|
||
}
|
||
if ( window.wp && window.wp.a11y && window.litMcpAdmin ) {
|
||
window.wp.a11y.speak( window.litMcpAdmin.copied );
|
||
}
|
||
} );
|
||
}
|
||
|
||
// Tool groups: WordPress' own postbox toggles, closed state saved per user.
|
||
function setUpGroupToggles() {
|
||
if ( window.postboxes && window.pagenow && document.querySelector( '.lit-mcp-groups' ) ) {
|
||
window.postboxes.add_postbox_toggles( window.pagenow );
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Tools: a group's mode and its "nur lesen" switch show on the group's rows at once,
|
||
* the same way the page renders them after saving (the server stays the authority).
|
||
*/
|
||
function setUpLiveGroupFeedback() {
|
||
var groups = document.querySelector( '.lit-mcp-groups' );
|
||
|
||
if ( ! groups ) {
|
||
return;
|
||
}
|
||
groups.querySelectorAll( '.lit-mcp-group' ).forEach( function ( group ) {
|
||
var mode = group.querySelector( '.lit-mcp-group-mode' );
|
||
var readonly = group.querySelector( '.lit-mcp-group-readonly' );
|
||
var tools = group.querySelectorAll( '.lit-mcp-tool-mode' );
|
||
|
||
if ( mode ) {
|
||
mode.addEventListener( 'change', function () {
|
||
tools.forEach( function ( select ) {
|
||
labelInheritedOption( select, mode, groups.dataset.likeGroup );
|
||
} );
|
||
} );
|
||
}
|
||
if ( readonly ) {
|
||
readonly.addEventListener( 'change', function () {
|
||
tools.forEach( function ( select ) {
|
||
if ( select.dataset.writes ) {
|
||
lockTool( select, readonly.checked );
|
||
}
|
||
} );
|
||
} );
|
||
}
|
||
} );
|
||
}
|
||
|
||
// First option: "Standard (…)" without a group mode, "wie Gruppe (…)" with one.
|
||
function labelInheritedOption( select, mode, likeGroup ) {
|
||
var label;
|
||
|
||
if ( select.dataset.required ) {
|
||
return;
|
||
}
|
||
if ( ! mode.value ) {
|
||
select.options[ 0 ].text = select.dataset.defaultLabel;
|
||
return;
|
||
}
|
||
label = mode.options[ mode.selectedIndex ].text;
|
||
if ( select.dataset.directOnly && mode.value === 'indirect' ) {
|
||
label = optionText( select, 'direct' ) || label;
|
||
}
|
||
select.options[ 0 ].text = likeGroup.replace( '%s', label );
|
||
}
|
||
|
||
function optionText( select, value ) {
|
||
var option = select.querySelector( 'option[value="' + value + '"]' );
|
||
|
||
return option ? option.text : '';
|
||
}
|
||
|
||
/*
|
||
* "Nur lesen": the select of a writing tool is disabled and its note shown. A disabled
|
||
* select is not submitted, so its own setting is kept in a hidden field meanwhile
|
||
* (the server does the same when it renders a locked row).
|
||
*/
|
||
function lockTool( select, locked ) {
|
||
var cell = select.parentNode;
|
||
var note = cell.querySelector( '.lit-mcp-readonly-note' );
|
||
var keep = cell.querySelector( 'input[type="hidden"][name="' + select.name + '"]' );
|
||
|
||
select.disabled = locked;
|
||
if ( locked && select.value && ! keep ) {
|
||
keep = document.createElement( 'input' );
|
||
keep.type = 'hidden';
|
||
keep.name = select.name;
|
||
keep.value = select.value;
|
||
cell.appendChild( keep );
|
||
}
|
||
if ( ! locked && keep ) {
|
||
keep.remove();
|
||
}
|
||
if ( note ) {
|
||
note.hidden = ! locked;
|
||
}
|
||
}
|
||
|
||
function setGroupOpen( group, open ) {
|
||
var toggle = group.querySelector( '.handlediv' );
|
||
|
||
group.classList.toggle( 'closed', ! open );
|
||
if ( toggle ) {
|
||
toggle.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
|
||
}
|
||
}
|
||
|
||
function saveGroupState() {
|
||
if ( window.postboxes && window.pagenow ) {
|
||
window.postboxes.save_state( window.pagenow );
|
||
}
|
||
}
|
||
|
||
function markAllUserSet( groups ) {
|
||
groups.forEach( function ( group ) {
|
||
group.dataset.litUserSet = '1';
|
||
} );
|
||
}
|
||
|
||
// Open or close every group; with `save`, remember it like a click on a single toggle would.
|
||
function setAllGroups( groups, open, save ) {
|
||
groups.forEach( function ( group ) {
|
||
setGroupOpen( group, open );
|
||
} );
|
||
if ( save ) {
|
||
saveGroupState();
|
||
}
|
||
}
|
||
|
||
// Tools: search by name, "only changed", open/close all. Rows and empty groups hide instead of vanish.
|
||
function setUpToolFilter() {
|
||
var groups = document.querySelector( '.lit-mcp-groups' );
|
||
var search = document.querySelector( '.lit-mcp-tool-search' );
|
||
var only = document.querySelector( '.lit-mcp-only-changed' );
|
||
var status = document.querySelector( '.lit-mcp-filter-status' );
|
||
var collapse = document.querySelector( '.lit-mcp-collapse-all' );
|
||
var expand = document.querySelector( '.lit-mcp-expand-all' );
|
||
var closedBefore = null;
|
||
var all;
|
||
|
||
if ( ! groups || ! search || ! only || ! collapse || ! expand ) {
|
||
return;
|
||
}
|
||
all = groups.querySelectorAll( '.lit-mcp-group' );
|
||
|
||
function countText( shown ) {
|
||
var text = window.litMcpAdmin;
|
||
|
||
if ( ! text ) {
|
||
return '';
|
||
}
|
||
return shown === 1 ? text.toolShown : text.toolsShown.replace( '%d', shown );
|
||
}
|
||
|
||
// The filter is cleared: forget the temporary state and, if the user changed groups meanwhile, save it now.
|
||
function finishFilter() {
|
||
var changed = false;
|
||
|
||
closedBefore = null;
|
||
all.forEach( function ( group ) {
|
||
changed = changed || !! group.dataset.litUserSet;
|
||
delete group.dataset.litUserSet;
|
||
} );
|
||
if ( changed ) {
|
||
saveGroupState();
|
||
}
|
||
}
|
||
|
||
function isFiltering() {
|
||
return search.value.trim() !== '' || only.checked;
|
||
}
|
||
|
||
function apply() {
|
||
var term = search.value.trim().toLowerCase();
|
||
var filtering = isFiltering();
|
||
var shown = 0;
|
||
|
||
/*
|
||
* A filter opens the groups with hits; clearing it puts every group back as the user had it.
|
||
* A group the user opens or closes themselves while filtering keeps that choice.
|
||
*/
|
||
if ( filtering && closedBefore === null ) {
|
||
closedBefore = Array.prototype.map.call( all, function ( group ) {
|
||
return group.classList.contains( 'closed' );
|
||
} );
|
||
}
|
||
all.forEach( function ( group, index ) {
|
||
var visible = 0;
|
||
|
||
group.querySelectorAll( 'tbody tr' ).forEach( function ( row ) {
|
||
var select = row.querySelector( '.lit-mcp-tool-mode' );
|
||
var cell = row.querySelector( '.column-tool' );
|
||
var name = cell ? cell.textContent.toLowerCase() : '';
|
||
var match = ( term === '' || name.indexOf( term ) !== -1 ) &&
|
||
( ! only.checked || ( select && select.value !== '' ) );
|
||
|
||
row.hidden = ! match;
|
||
visible += match ? 1 : 0;
|
||
} );
|
||
group.hidden = visible === 0;
|
||
if ( filtering && visible > 0 && ! group.dataset.litUserSet ) {
|
||
setGroupOpen( group, true );
|
||
} else if ( ! filtering && closedBefore && ! group.dataset.litUserSet ) {
|
||
setGroupOpen( group, ! closedBefore[ index ] );
|
||
}
|
||
shown += visible;
|
||
} );
|
||
if ( ! filtering && closedBefore ) {
|
||
finishFilter();
|
||
}
|
||
if ( status ) {
|
||
status.textContent = filtering ? countText( shown ) : '';
|
||
}
|
||
}
|
||
|
||
// Enter in the search field would submit the whole settings form, half-done changes included.
|
||
search.addEventListener( 'keydown', function ( event ) {
|
||
if ( event.key === 'Enter' ) {
|
||
event.preventDefault();
|
||
}
|
||
} );
|
||
search.addEventListener( 'input', apply );
|
||
only.addEventListener( 'change', apply );
|
||
// While filtering, nothing is saved: the hidden groups would be stored as hidden boxes.
|
||
// "User set" only means something while a filter is on; outside of it a stale flag would stop the next filter.
|
||
collapse.addEventListener( 'click', function () {
|
||
if ( isFiltering() ) {
|
||
markAllUserSet( all );
|
||
}
|
||
setAllGroups( all, false, ! isFiltering() );
|
||
} );
|
||
expand.addEventListener( 'click', function () {
|
||
if ( isFiltering() ) {
|
||
markAllUserSet( all );
|
||
}
|
||
setAllGroups( all, true, ! isFiltering() );
|
||
} );
|
||
groups.addEventListener( 'click', function ( event ) {
|
||
var group = event.target.closest( '.handlediv, .hndle' ) && event.target.closest( '.lit-mcp-group' );
|
||
|
||
if ( group && isFiltering() ) {
|
||
group.dataset.litUserSet = '1';
|
||
}
|
||
} );
|
||
// The browser may restore the search text after a reload; make the list match it.
|
||
apply();
|
||
}
|
||
|
||
// Tools: "Auf Standard zurücksetzen" clears a group's mode, its "nur lesen" switch and every tool's own setting.
|
||
function setUpGroupReset() {
|
||
document.querySelectorAll( '.lit-mcp-group' ).forEach( function ( group ) {
|
||
var button = group.querySelector( '.lit-mcp-group-reset' );
|
||
|
||
if ( ! button ) {
|
||
return;
|
||
}
|
||
button.addEventListener( 'click', function () {
|
||
var readonly = group.querySelector( '.lit-mcp-group-readonly' );
|
||
var mode = group.querySelector( '.lit-mcp-group-mode' );
|
||
|
||
// Order matters: unlock the rows first, then fall back to the default.
|
||
change( readonly, function () {
|
||
readonly.checked = false;
|
||
} );
|
||
change( mode, function () {
|
||
mode.value = '';
|
||
} );
|
||
group.querySelectorAll( '.lit-mcp-tool-mode' ).forEach( function ( select ) {
|
||
change( select, function () {
|
||
select.value = '';
|
||
} );
|
||
} );
|
||
if ( window.wp && window.wp.a11y && window.litMcpAdmin ) {
|
||
window.wp.a11y.speak( window.litMcpAdmin.groupReset );
|
||
}
|
||
} );
|
||
} );
|
||
}
|
||
|
||
function change( field, apply ) {
|
||
if ( ! field ) {
|
||
return;
|
||
}
|
||
apply();
|
||
field.dispatchEvent( new Event( 'change', { bubbles: true } ) );
|
||
}
|
||
|
||
// A button with data-lit-mcp-confirm asks first (revoke a connection, clear the log).
|
||
function setUpConfirmButtons() {
|
||
document.addEventListener( 'click', function ( event ) {
|
||
var button = event.target.closest( '[data-lit-mcp-confirm]' );
|
||
|
||
if ( button && ! window.confirm( button.dataset.litMcpConfirm ) ) {
|
||
event.preventDefault();
|
||
}
|
||
} );
|
||
}
|
||
|
||
// Protokoll: a changed filter starts again on page 1.
|
||
function setUpFilterPaging() {
|
||
document.querySelectorAll( '.lit-mcp-log-filters select, .lit-mcp-log-filters input' ).forEach(
|
||
function ( control ) {
|
||
control.addEventListener( 'change', function () {
|
||
var paged = control.form && control.form.querySelector( 'input[name="paged"]' );
|
||
if ( paged ) {
|
||
paged.value = '1';
|
||
}
|
||
} );
|
||
}
|
||
);
|
||
}
|
||
|
||
// Zugriff: ask before saving a role set that leaves nobody, or the admin themselves, with access.
|
||
function setUpAccessConfirmation() {
|
||
var form = document.querySelector( '.lit-mcp-access-form' );
|
||
|
||
if ( ! form ) {
|
||
return;
|
||
}
|
||
form.addEventListener( 'submit', function ( event ) {
|
||
var checked = Array.prototype.map.call(
|
||
form.querySelectorAll( 'input[type="checkbox"]:checked' ),
|
||
function ( box ) {
|
||
return box.value;
|
||
}
|
||
);
|
||
var own = form.dataset.ownRoles ? form.dataset.ownRoles.split( ' ' ) : null;
|
||
var question = '';
|
||
|
||
if ( checked.length === 0 ) {
|
||
question = form.dataset.confirmNone;
|
||
} else if ( own && ! own.some( function ( role ) { return checked.indexOf( role ) !== -1; } ) ) {
|
||
question = form.dataset.confirmOwn;
|
||
}
|
||
if ( question && ! window.confirm( question ) ) {
|
||
event.preventDefault();
|
||
}
|
||
} );
|
||
}
|
||
|
||
document.addEventListener( 'DOMContentLoaded', function () {
|
||
// First: the confirmations guard destructive buttons and must not depend on the rest starting up.
|
||
setUpConfirmButtons();
|
||
setUpCopyButton();
|
||
setUpGroupToggles();
|
||
setUpLiveGroupFeedback();
|
||
setUpToolFilter();
|
||
setUpGroupReset();
|
||
setUpFilterPaging();
|
||
setUpAccessConfirmation();
|
||
} );
|
||
}() );
|