Files
wordpress-theme-aksrt/assets/widgets/admin.js
T
kate522 8aa116ec43 fix(theme): align reference layouts and restore passing contracts
Align homepage and category discovery, modern widgets, admin controls, quotes, and comments with the reference theme. Remove superseded paths and add rendering and behavior coverage. Update stale asset and finance contracts; all 760 Node tests and 139 widget assertions pass.
2026-10-29 19:16:03 +08:00

351 lines
17 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.
(function ($) {
'use strict';
const config = window.aksrtWidgetAdmin || {};
const core = window.AKSRTWidgetAdminCore;
function initForm(root) {
const $root = $(root);
$root.find('.aksrt-widget-color').wpColorPicker();
$root.find('.aksrt-widget-repeater__rows').sortable({
handle: '.dashicons-move',
axis: 'y',
update: renumberRepeaters,
});
$root.find('.aksrt-widget-terms__selected').sortable({
items: '.aksrt-widget-terms__chip',
update: () => $root.find('.aksrt-widget-terms').each((_i, el) => syncTerms(el)),
});
$root.find('input[type="range"]').each(function () {
const output = this.parentElement.querySelector('output');
if (output) this.addEventListener('input', () => { output.textContent = this.value; });
});
evaluateDependencies($root);
updateWidgetTitle($root);
updatePlacementWarning($root);
updateRepeaterControls($root);
}
/* terms 字段:以“已选标签”为唯一真源,同步隐藏输入(选择顺序)与复选框。 */
function syncTerms(wrap) {
const $wrap = $(wrap);
const ids = $wrap.find('.aksrt-widget-terms__chip').map((_i, el) => String($(el).data('term-id'))).get();
$wrap.find('.aksrt-widget-terms__value').val(ids.join(',')).trigger('change');
$wrap.find('.aksrt-widget-terms__option input').each(function () {
this.checked = ids.indexOf(String(this.value)) !== -1;
});
}
function addTermChip(wrap, id, name) {
const $selected = $(wrap).find('.aksrt-widget-terms__selected');
if ($selected.find(`.aksrt-widget-terms__chip[data-term-id="${id}"]`).length) return;
const chip = $(`<span class="aksrt-widget-terms__chip" data-term-id="${id}"><span class="dashicons dashicons-move" aria-hidden="true"></span></span>`);
chip.append(document.createTextNode(name));
chip.append('<button type="button" class="button-link-delete aksrt-widget-terms__remove" aria-label="移除">×</button>');
$selected.append(chip);
}
function updateWidgetTitle(root) {
const $root = $(root);
const title = String($root.find('.aksrt-widget-field[data-field-id="title"] input').first().val() || '').trim();
if (!title) return;
$root.closest('.widget').find('.widget-title h3, .widget-title-action + h3').first().contents().filter(function () {
return this.nodeType === Node.TEXT_NODE;
}).first().replaceWith(`${title} `);
}
function updatePlacementWarning(root) {
const $widget = $(root).closest('.widget');
const $hint = $widget.find('.aksrt-widget-placement-hint').first();
if (!$hint.length) return;
const size = String($hint.data('widget-size') || 'any');
const area = String($widget.closest('.widgets-sortables').attr('id') || '');
const isSidebar = /sidebar/.test(area);
$hint.toggleClass('is-warning', (size === 'wide' && isSidebar) || (size === 'sidebar' && !isSidebar && area !== ''));
}
function injectTransferToolbar($anchor, sidebarId) {
if (!$anchor?.length || !sidebarId || $anchor.next('.aksrt-widget-transfer-toolbar').length) return;
const $toolbar = $('<div class="aksrt-widget-transfer-toolbar" role="group"></div>');
[
['export', config.i18n?.export || '导出'],
['import', config.i18n?.import || '导入'],
['template', config.i18n?.template || '应用模板'],
].forEach(([action, label]) => {
$('<button type="button" class="button button-small"></button>')
.text(label)
.attr('data-aksrt-transfer-action', action)
.attr('data-sidebar-id', sidebarId)
.appendTo($toolbar);
});
$anchor.after($toolbar);
}
function scanWidgetAreas() {
$('.widgets-holder-wrap:not(#available-widgets):not(.inactive-sidebar)').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();
const $anchor = $description.length ? $description : $sortables.find('.description').first();
if ($anchor.length) injectTransferToolbar($anchor, sidebarId);
});
$('.control-section-sidebar').each(function () {
const $section = $(this);
const sectionId = String($section.attr('id') || '');
const prefix = 'sub-accordion-section-sidebar-widgets-';
if (!sectionId.startsWith(prefix)) return;
const sidebarId = sectionId.slice(prefix.length);
const $description = $section.find('.section-meta .description').first();
const $anchor = $description.length ? $description : $section.find('.accordion-section-title, .customize-section-title').first();
if ($anchor.length) injectTransferToolbar($anchor, sidebarId);
});
$('.customize-control-widget_form').each(function () {
const $control = $(this);
const $nav = $control.find('.widget-reorder-nav').first();
if (!$nav.length || $nav.find('.aksrt-widget-customizer-remove').length) return;
$('<button type="button" class="button-link-delete aksrt-widget-customizer-remove" aria-label="移除此小工具">移除</button>').appendTo($nav);
});
}
function openTransferModal(action, sidebarId) {
const $modal = $('#aksrt-widget-transfer-modal');
if (!$modal.length) return;
const sidebars = config.sidebars || {};
const areaName = sidebars[sidebarId] || sidebarId;
$modal.find('.aksrt-widget-transfer-modal__area').text(areaName);
$modal.find('input[name="sidebar"], input[name="target_sidebar"]').val(sidebarId);
$modal.find('[data-transfer-tab]').removeClass('is-active');
$modal.find(`[data-transfer-tab="${action}"]`).addClass('is-active');
$modal.find('[data-transfer-panel]').prop('hidden', true);
$modal.find(`[data-transfer-panel="${action}"]`).prop('hidden', false);
$modal.prop('hidden', false).addClass('is-open').attr('aria-hidden', 'false');
$modal.find('[data-transfer-panel="' + action + '"] button, [data-transfer-panel="' + action + '"] input, [data-transfer-panel="' + action + '"] select').filter(':visible').first().trigger('focus');
}
function closeTransferModal() {
$('#aksrt-widget-transfer-modal').removeClass('is-open').attr('aria-hidden', 'true').prop('hidden', true);
}
function loadForm(container) {
const $container = $(container);
if ($container.data('loading') || $container.data('loaded')) return;
const $widget = $container.closest('.widget, .customize-control-widget_form');
const inputName = String($widget.find('input[name^="widget-"]').first().attr('name') || '');
const number = core.resolveWidgetNumber(inputName, $widget.attr('id'), $container.data('widget-number'));
// New-widget templates use __i__ until WordPress assigns a real instance number.
if (!number) return;
$container.data('loading', true).find('.aksrt-widget-form-lazy__status').text(config.i18n?.loading || 'Loading…');
$.post(config.ajaxUrl, {
action: config.action,
nonce: config.nonce,
id_base: $container.data('id-base'),
widget_number: number,
}).done((response) => {
if (!response?.success || !response.data?.html) {
$container.data('loading', false).find('.aksrt-widget-form-lazy__status').text(response?.data?.message || config.i18n?.error || 'Failed to load.');
return;
}
const $form = $(response.data.html);
$container.replaceWith($form);
initForm($form);
}).fail(() => {
$container.data('loading', false).find('.aksrt-widget-form-lazy__status').text(config.i18n?.error || 'Failed to load.');
});
}
function renumberRepeaters(_event, ui) {
const $form = $(ui?.item || this).closest('.aksrt-widget-form');
$form.find('.aksrt-widget-repeater__rows').each(function () {
$(this).children('.aksrt-widget-repeater__row').each(function (index) {
this.dataset.rowIndex = String(index);
});
});
$form.find('[name]').each(function () {
const rowIndexes = $(this).parents('.aksrt-widget-repeater__row').get().reverse().map((row) => Number(row.dataset.rowIndex || 0));
if (!rowIndexes.length) return;
this.name = core.replaceRepeaterIndexes(this.name, rowIndexes);
});
updateRepeaterTitles($form);
}
function updateRepeaterTitles($form) {
$form.find('.aksrt-widget-repeater__rows').each(function () {
$(this).children('.aksrt-widget-repeater__row').each(function (index) {
const $row = $(this);
const $title = $row.find('.aksrt-widget-repeater__title').first();
const prefix = String($title.data('title-prefix') || '项目');
const $firstControl = $row.children('.aksrt-widget-repeater__fields').find('input:not([type="hidden"]), select, textarea').first();
const value = String($firstControl.val() || '').trim();
$title.text(prefix + (index + 1) + (value ? ':' + value : ''));
});
});
}
function updateRepeaterControls($root) {
$root.filter('.aksrt-widget-repeater').add($root.find('.aksrt-widget-repeater')).each(function () {
const $repeater = $(this);
const count = $repeater.children('.aksrt-widget-repeater__rows').children('.aksrt-widget-repeater__row').length;
const min = Number($repeater.data('min-items') || 0);
const max = Number($repeater.data('max-items') || 50);
$repeater.children('.aksrt-widget-repeater__rows').children('.aksrt-widget-repeater__row').children('.aksrt-widget-repeater__bar').children('.aksrt-widget-repeater__remove').prop('disabled', count <= min);
$repeater.children('.aksrt-widget-repeater__add').prop('disabled', count >= max);
});
}
function fieldValue($form, id) {
const $field = $form.find(`.aksrt-widget-field[data-field-id="${CSS.escape(id)}"]`).first();
const $checked = $field.find('input:checked').first();
if ($checked.length) return String($checked.val());
return String($field.find('input,select,textarea').first().val() ?? '');
}
function evaluateDependencies($form) {
$form.find('[data-dependency]').each(function () {
const rule = core.parseDependency(this.dataset.dependency || '[]');
const $row = $(this).closest('.aksrt-widget-repeater__row');
const scope = $row.length ? $row : $form;
const ids = Array.isArray(rule[0]) ? rule[0].map((part) => part[0]) : String(rule[0] || '').split('|');
const current = {};
ids.forEach((id) => { current[id] = fieldValue(scope, id); });
this.hidden = !core.dependencyVisible(rule, current);
});
}
$(document)
.on('click', '[data-aksrt-transfer-action]', function () {
openTransferModal(String($(this).data('aksrt-transfer-action')), String($(this).data('sidebar-id')));
})
.on('click', '[data-aksrt-transfer-close]', closeTransferModal)
.on('click', '[data-transfer-tab]', function () {
const $modal = $(this).closest('.aksrt-widget-transfer-modal');
const panel = String($(this).data('transfer-tab'));
$modal.find('[data-transfer-tab]').removeClass('is-active');
$(this).addClass('is-active');
$modal.find('[data-transfer-panel]').prop('hidden', true);
$modal.find(`[data-transfer-panel="${panel}"]`).prop('hidden', false);
})
.on('click', '.aksrt-widget-customizer-remove', function (event) {
event.preventDefault();
$(this).closest('.customize-control-widget_form').find('.widget-control-remove').first().trigger('click');
})
.on('click', '.aksrt-widget-form-lazy__load', function () { loadForm($(this).closest('.aksrt-widget-form-lazy')); })
.on('click', '.widget.open .aksrt-widget-form-lazy, .customize-control-widget_form .aksrt-widget-form-lazy', function () { loadForm(this); })
.on('change input', '.aksrt-widget-form input, .aksrt-widget-form select, .aksrt-widget-form textarea', function () {
const $form = $(this).closest('.aksrt-widget-form');
evaluateDependencies($form);
updateWidgetTitle($form);
updateRepeaterTitles($form);
})
.on('click', '.aksrt-widget-repeater__add', function () {
const $repeater = $(this).closest('.aksrt-widget-repeater');
const max = Number($repeater.data('max-items') || 50);
const $rows = $repeater.children('.aksrt-widget-repeater__rows');
if ($rows.children().length >= max) return;
const index = $rows.children().length;
const token = String($repeater.data('index-token') || '__index__');
const html = $repeater.children('template').html().replaceAll(token, String(index));
const $row = $(html).appendTo($rows);
initForm($row);
renumberRepeaters.call($rows[0]);
updateRepeaterControls($repeater);
})
.on('click', '.aksrt-widget-repeater__remove', function (event) {
event.preventDefault();
event.stopPropagation();
const $repeater = $(this).closest('.aksrt-widget-repeater');
const $rows = $repeater.children('.aksrt-widget-repeater__rows');
if ($rows.children('.aksrt-widget-repeater__row').length <= Number($repeater.data('min-items') || 0)) return;
$(this).closest('.aksrt-widget-repeater__row').remove();
renumberRepeaters.call($rows[0]);
updateRepeaterControls($repeater);
})
.on('click', '.aksrt-widget-terms__remove', function () {
$(this).closest('.aksrt-widget-terms__chip').remove();
syncTerms($(this).closest('.aksrt-widget-terms'));
})
.on('change', '.aksrt-widget-terms__option input', function () {
const wrap = this.closest('.aksrt-widget-terms');
const option = this.closest('.aksrt-widget-terms__option');
const id = String(this.value);
const name = (option.querySelector('[data-term-name]') || option).getAttribute('data-term-name');
if (this.checked) addTermChip(wrap, id, name || id);
else $(wrap).find('.aksrt-widget-terms__chip').filter((_index, chip) => String($(chip).data('term-id')) === id).remove();
syncTerms(wrap);
})
.on('input', '.aksrt-widget-terms__search', function () {
const needle = this.value.trim().toLowerCase();
$(this).closest('.aksrt-widget-terms').find('.aksrt-widget-terms__option').each(function () {
const name = String(this.getAttribute('data-term-name') || '').toLowerCase();
this.hidden = needle !== '' && name.indexOf(needle) === -1;
});
})
.on('click', '.aksrt-widget-upload__choose', function () {
const $wrap = $(this).closest('.aksrt-widget-upload');
const frame = wp.media({ title: $wrap.data('media-library') === 'video' ? '选择视频' : '选择图片', library: $wrap.data('media-library') ? { type: $wrap.data('media-library') } : undefined, multiple: false });
frame.on('select', () => {
const attachment = frame.state().get('selection').first().toJSON();
$wrap.find('input[type="url"]').val(attachment.url).trigger('change');
if ($wrap.data('media-library') === 'video') return;
let $image = $wrap.find('img');
if (!$image.length) $image = $('<img alt="">').prependTo($wrap);
$image.attr('src', attachment.url);
});
frame.open();
})
.on('click', '.aksrt-widget-upload__remove', function () {
const $wrap = $(this).closest('.aksrt-widget-upload');
$wrap.find('input[type="url"]').val('').trigger('change');
$wrap.find('img').remove();
})
.on('click', '.aksrt-widget-template-preview', function () {
const $form = $(this).closest('form');
const $output = $('.aksrt-widget-template-preview-output').first();
$output.prop('hidden', false).html('<p>正在生成预览…</p>');
$.post(config.ajaxUrl, {
action: 'aksrt_widgets_template_preview',
nonce: config.transferNonce,
template: $form.find('[name="template"]').val(),
}).done((response) => {
if (!response?.success || !response.data) {
$output.empty().append($('<p class="notice notice-error"></p>').text(response?.data?.message || config.i18n?.error || '预览加载失败,请重试。'));
return;
}
$output.html(response.data.html || '<p>模板为空。</p>');
}).fail((xhr) => {
const message = xhr?.responseJSON?.data?.message || config.i18n?.error || '预览加载失败,请重试。';
$output.empty().append($('<p class="notice notice-error"></p>').text(message));
});
});
$(document).on('widget-added widget-updated', function (_event, widget) {
$(widget).find('.aksrt-widget-form-lazy').each(function () { loadForm(this); });
updatePlacementWarning(widget);
scanWidgetAreas();
});
if (window.wp?.customize?.bind) {
window.wp.customize.bind('ready', function () {
$('#customize-theme-controls').on('expanded', function (_event, section) {
if (!section || !String(section.id || section).includes('sidebar-widgets')) return;
window.setTimeout(function () {
scanWidgetAreas();
$('.customize-control-widget_form .aksrt-widget-form-lazy').each(function () { loadForm(this); });
}, 100);
});
});
}
$(function () {
scanWidgetAreas();
$(document).on('keydown', function (event) {
if (event.key === 'Escape') closeTransferModal();
});
$('.aksrt-widget-form').each(function () { initForm(this); });
$('.widget.open .aksrt-widget-form-lazy').each(function () { loadForm(this); });
});
})(jQuery);