Files
wordpress-theme-aksrt/assets/widgets/admin-transfer.js
T
kate522andClaude Fable 5 a579a5b00a feat(widgets): rebuild admin widgets page to match reference theme
- Replace transfer modal/admin-post flow with reference-style AJAX
  export/import/template dialogs (admin-transfer.js, TransferController)
- Customizer "移出模块" action, reference lazy form loading with t-notice
- Common fields use reference labels, dependencies, compact/inline rows,
  top/bottom-only extra spacing; form header with description/reminder
- Remove placement hint, template preview and legacy transfer code
- Fix widget audit (current_datetime stub, honor catalog common flags)
  and regenerate stale golden fixtures for posts_toc, quote, posts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-30 14:00:57 +08:00

326 lines
15 KiB
JavaScript

/* Sidebar toolbar: per-area export / import / template import (zibll WIE flow). */
(function ($) {
'use strict';
const config = window.aksrtWidgetAdmin || {};
const t = (key) => (config.i18n && config.i18n[key]) || key;
function post(action, data) {
return $.ajax({
url: config.ajaxUrl,
type: 'POST',
dataType: 'json',
data: $.extend({ action, nonce: config.transferNonce }, data || {}),
});
}
function failMessage(res, xhr, fallbackKey) {
return res?.data?.message || xhr?.responseJSON?.data?.message || t(fallbackKey);
}
function clearResult($result) {
return $result.removeClass('is-ok is-warn is-err').empty();
}
function showError($result, message) {
clearResult($result).addClass('is-err').text(message);
}
function reloadButton() {
return $('<p class="aksrt-wie-modal__reload-wrap"></p>').append(
$('<button type="button" class="button button-primary aksrt-wie-modal__reload"></button>').text(t('reloadPage'))
);
}
function renderImportResult($result, res, autoReload) {
clearResult($result);
if (!res?.success || !res.data) {
showError($result, failMessage(res, null, 'importFailed'));
return;
}
const warnings = Array.isArray(res.data.warnings) ? res.data.warnings : [];
$result.addClass(warnings.length ? 'is-warn' : 'is-ok');
[res.data.message || t('importSuccess')].concat(warnings).forEach((line, index) => {
if (index) $result.append('<br>');
$result.append(document.createTextNode(String(line)));
});
$result.append(reloadButton());
if (autoReload) window.setTimeout(() => window.location.reload(), 200);
}
function withButtonLoading($button, loadingText, idleText, request) {
$button.prop('disabled', true).text(loadingText);
return request().always(() => $button.prop('disabled', false).text(idleText));
}
function importModeHtml(name) {
return `<div class="aksrt-wie-modal__import-mode">
<div class="aksrt-wie-modal__import-mode-title description"></div>
<label class="aksrt-wie-modal__import-mode-row"><input type="radio" name="${name}" value="replace"> <span data-mode-label="replace"></span></label>
<label class="aksrt-wie-modal__import-mode-row"><input type="radio" name="${name}" value="append" checked> <span data-mode-label="append"></span></label>
</div>`;
}
function createModal(opts) {
let $modal = null;
function ensure() {
if ($modal) return $modal;
$modal = $(`<div id="${opts.id}" class="aksrt-wie-modal${opts.extraClass ? ' ' + opts.extraClass : ''}" aria-hidden="true">
<div class="aksrt-wie-modal__backdrop" data-aksrt-wie-close></div>
<div class="aksrt-wie-modal__dialog" role="dialog" aria-modal="true">
<button type="button" class="aksrt-wie-modal__close" data-aksrt-wie-close aria-label="${t('close')}">&times;</button>
${opts.bodyHtml}
${opts.actionsHtml}
<p class="aksrt-wie-modal__result" aria-live="polite"></p>
</div>
</div>`).appendTo('body');
$modal.find('.aksrt-wie-modal__import-mode-title').text(t('importModeLabel'));
$modal.find('[data-mode-label="replace"]').text(t('importModeReplace'));
$modal.find('[data-mode-label="append"]').text(t('importModeAppend'));
$modal.find('.aksrt-wie-modal__cancel').text(t('close'));
$modal.on('click', '[data-aksrt-wie-close], .aksrt-wie-modal__cancel', close);
if (opts.onInit) opts.onInit($modal);
return $modal;
}
function open() {
ensure().find('input[name$="-import-mode"][value="append"]').prop('checked', true);
clearResult($modal.find('.aksrt-wie-modal__result'));
$modal.addClass('is-open').attr('aria-hidden', 'false');
}
function close() {
if ($modal) $modal.removeClass('is-open').attr('aria-hidden', 'true');
}
return { ensure, open, close, $result: () => ensure().find('.aksrt-wie-modal__result') };
}
const state = { mode: 'export', sidebarId: '', templateSidebarId: '' };
const ioModal = createModal({
id: 'aksrt-wie-modal',
bodyHtml: `<h3 class="aksrt-wie-modal__title"></h3>
<p class="aksrt-wie-modal__hint description"></p>
<textarea class="aksrt-wie-modal__textarea" spellcheck="false"></textarea>
${importModeHtml('aksrt-wie-import-mode')}`,
actionsHtml: `<p class="aksrt-wie-modal__actions">
<button type="button" class="button aksrt-wie-modal__copy"></button>
<button type="button" class="button button-primary aksrt-wie-modal__submit"></button>
<button type="button" class="button aksrt-wie-modal__cancel"></button>
</p>`,
onInit($modal) {
$modal.find('.aksrt-wie-modal__copy').on('click', function () {
const textarea = $modal.find('.aksrt-wie-modal__textarea')[0];
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
try {
document.execCommand('copy');
$(this).text(t('copyDone'));
} catch (_error) {
window.alert(t('copyFail'));
}
});
$modal.find('.aksrt-wie-modal__submit').on('click', submitImport);
},
});
const templateModal = createModal({
id: 'aksrt-wie-template-modal',
extraClass: 'aksrt-wie-template-layer',
bodyHtml: `<h3 class="aksrt-wie-modal__title"></h3>
<div class="aksrt-wie-modal__hint description"></div>
<div class="aksrt-wie-template-list-wrap"><p class="aksrt-wie-template-list-status description"></p><div class="aksrt-wie-template-list"></div></div>
${importModeHtml('aksrt-wie-template-import-mode')}`,
actionsHtml: `<p class="aksrt-wie-modal__actions">
<button type="button" class="button button-primary aksrt-wie-template-submit"></button>
<button type="button" class="button aksrt-wie-modal__cancel"></button>
</p>`,
onInit($modal) {
$modal.find('.aksrt-wie-modal__title').text(t('templateModalTitle'));
// templateHint is server-built, escaped translation markup.
$modal.find('.aksrt-wie-modal__hint').html(t('templateHint'));
$modal.find('.aksrt-wie-template-submit').text(t('templateSubmit')).on('click', submitTemplate);
},
});
function openIoModal(mode, sidebarId) {
state.mode = mode;
state.sidebarId = sidebarId;
const $modal = ioModal.ensure();
const isExport = mode === 'export';
$modal.find('.aksrt-wie-modal__title').text(t(isExport ? 'exportTitle' : 'importTitle'));
$modal.find('.aksrt-wie-modal__hint').text(t(isExport ? 'exportHint' : 'importHint'));
$modal.find('.aksrt-wie-modal__textarea').prop('readonly', isExport).val('');
$modal.find('.aksrt-wie-modal__copy').toggle(isExport).text(t('copy'));
$modal.find('.aksrt-wie-modal__submit').toggle(!isExport).text(t('submitImport')).prop('disabled', false);
$modal.find('.aksrt-wie-modal__import-mode').toggle(!isExport);
ioModal.open();
if (isExport) fetchExport();
}
function fetchExport() {
const $textarea = ioModal.ensure().find('.aksrt-wie-modal__textarea').val(t('loadingExport'));
post('aksrt_widgets_export_sidebar', { sidebar_id: state.sidebarId })
.done((res) => {
if (res?.success && res.data?.json_text) {
$textarea.val(res.data.json_text);
return;
}
$textarea.val('');
showError(ioModal.$result(), failMessage(res, null, 'exportFailed'));
})
.fail((xhr) => {
$textarea.val('');
showError(ioModal.$result(), failMessage(null, xhr, 'networkError'));
});
}
function submitImport() {
const $modal = ioModal.ensure();
const mode = $modal.find('input[name="aksrt-wie-import-mode"]:checked').val() || 'append';
if (!window.confirm(t(mode === 'append' ? 'confirmImportAppend' : 'confirmImportReplace'))) return;
const raw = String($modal.find('.aksrt-wie-modal__textarea').val() || '').trim();
if (!raw) {
showError(ioModal.$result(), t('pasteJsonFirst'));
return;
}
withButtonLoading($modal.find('.aksrt-wie-modal__submit'), t('loadingImport'), t('submitImport'), () =>
post('aksrt_widgets_import_sidebar', { sidebar_id: state.sidebarId, json: raw, import_mode: mode })
.done((res) => {
if (res?.success) $modal.find('.aksrt-wie-modal__textarea').val('');
renderImportResult(ioModal.$result(), res, false);
})
.fail((xhr) => showError(ioModal.$result(), failMessage(null, xhr, 'networkError')))
);
}
function templateItem(template) {
const $meta = $('<span class="aksrt-wie-template-item-meta"></span>')
.append($('<span class="aksrt-wie-template-item-title"></span>').text(template.title || template.id));
if (template.desc) $meta.append($('<span class="aksrt-wie-template-item-desc"></span>').text(template.desc));
if (template.remind) {
$meta.append($('<span class="aksrt-wie-template-item-remind"></span>')
.append('<span class="dashicons dashicons-info-outline" aria-hidden="true"></span>')
.append(document.createTextNode(template.remind)));
}
if (Array.isArray(template.modules) && template.modules.length) {
$meta.append($('<span class="aksrt-wie-template-item-modules"></span>').text(template.modules.map((module) => module.title).join(' / ')));
}
const $label = $('<label class="aksrt-wie-template-item-label"></label>')
.append($('<input type="radio" name="aksrt-wie-template-id">').val(template.id))
.append($meta);
return $('<div class="aksrt-wie-template-item"></div>').append($('<div class="aksrt-wie-template-item-row"></div>').append($label));
}
function openTemplateModal(sidebarId) {
state.templateSidebarId = sidebarId;
const $modal = templateModal.ensure();
const $list = $modal.find('.aksrt-wie-template-list').empty();
const $status = $modal.find('.aksrt-wie-template-list-status').text(t('templateLoadingList'));
templateModal.open();
post('aksrt_widgets_list_templates')
.done((res) => {
const templates = res?.success ? res.data?.templates : null;
$status.text(Array.isArray(templates) && templates.length ? '' : t('templateEmpty'));
(templates || []).forEach((template) => $list.append(templateItem(template)));
})
.fail((xhr) => $status.text(failMessage(null, xhr, 'networkError')));
}
function submitTemplate() {
const $modal = templateModal.ensure();
const $picked = $modal.find('input[name="aksrt-wie-template-id"]:checked');
if (!$picked.length) {
window.alert(t('templatePick'));
return;
}
const templateId = String($picked.val());
const mode = $modal.find('input[name="aksrt-wie-template-import-mode"]:checked').val() || 'append';
const title = $picked.closest('.aksrt-wie-template-item').find('.aksrt-wie-template-item-title').text().trim() || templateId;
const message = t('confirmTemplate').replace('{title}', title).replace('{mode}', t(mode === 'append' ? 'importModeAppend' : 'importModeReplace'));
if (!window.confirm(message)) return;
withButtonLoading($modal.find('.aksrt-wie-template-submit'), t('loadingImport'), t('templateSubmit'), () =>
post('aksrt_widgets_import_template', { sidebar_id: state.templateSidebarId, template_id: templateId, import_mode: mode })
.done((res) => renderImportResult(templateModal.$result(), res, true))
.fail((xhr) => showError(templateModal.$result(), failMessage(null, xhr, 'networkError')))
);
}
function injectToolbar($anchor, sidebarId) {
if (!$anchor?.length || !sidebarId || $anchor.next('.aksrt-wie-toolbar').length) return;
const $toolbar = $('<div class="aksrt-wie-toolbar"></div>');
[['export', 'exportModules'], ['import', 'importModules'], ['template', 'importTemplate']].forEach(([action, label]) => {
$('<button type="button" class="button button-small"></button>')
.text(t(label))
.attr('data-aksrt-wie-action', action)
.attr('data-sidebar-id', sidebarId)
.appendTo($toolbar);
});
$anchor.after($toolbar);
}
function scanWidgetsScreen() {
$('.widgets-holder-wrap:not(#available-widgets):not(.inactive-sidebar):not(.is-aksrt-wie)').each(function () {
const $wrap = $(this);
const $sortables = $wrap.find('.widgets-sortables').first();
const sidebarId = String($sortables.attr('id') || '');
if (!sidebarId) return;
const $description = $sortables.find('.sidebar-description .description').first();
$wrap.addClass('is-aksrt-wie');
injectToolbar($description.length ? $description : $sortables.find('.description').first(), sidebarId);
});
}
function scanCustomizer() {
const prefix = 'sub-accordion-section-sidebar-widgets-';
$('.control-section-sidebar:not(.is-aksrt-wie)').each(function () {
const $section = $(this);
const sectionId = String($section.attr('id') || '');
if (!sectionId.startsWith(prefix) || sectionId.length === prefix.length) return;
$section.addClass('is-aksrt-wie');
const $description = $section.find('.section-meta .description').first();
injectToolbar($description.length ? $description : $section.find('.accordion-section-title, .customize-section-title').first(), sectionId.slice(prefix.length));
});
$('.customize-control-widget_form:not(.is-aksrt-wie)').each(function () {
const $nav = $(this).find('.widget-reorder-nav').first();
if (!$nav.length) return;
$('<span class="aksrt-wie-btn-delete dashicons-trash" tabindex="0" role="button"></span>').text(t('removeModule')).appendTo($nav);
$(this).addClass('is-aksrt-wie');
});
}
$(document)
.on('click', '[data-aksrt-wie-action]', function () {
const action = String($(this).data('aksrt-wie-action'));
const sidebarId = String($(this).data('sidebar-id') || '');
if (!sidebarId) return;
if (action === 'template') openTemplateModal(sidebarId);
else openIoModal(action, sidebarId);
})
.on('click keydown', '.aksrt-wie-btn-delete', function (event) {
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
$(this).closest('.customize-control-widget_form').find('.widget-control-remove').first().trigger('click');
})
.on('click', '.aksrt-wie-modal__reload', () => window.location.reload())
.on('keydown', (event) => {
if (event.key !== 'Escape') return;
ioModal.close();
templateModal.close();
})
.on('widget-added', () => {
if (document.body.classList.contains('wp-customizer')) scanCustomizer();
});
$(function () {
if (!config.ajaxUrl) return;
if (document.body.classList.contains('widgets-php')) scanWidgetsScreen();
if (document.body.classList.contains('wp-customizer')) {
window.wp?.customize?.bind?.('ready', () => window.setTimeout(scanCustomizer, 300));
$(document).on('click', '.control-panel-widgets .control-section-sidebar', () => window.setTimeout(scanCustomizer, 100));
}
});
})(jQuery);