Files
FlowPilot/sidepanel/editable-list-picker.js
T
2026-05-05 03:35:19 +08:00

229 lines
6.9 KiB
JavaScript

(function attachSidepanelEditableListPicker(globalScope) {
const editableListPickers = [];
function splitEditableListValues(value = '') {
return String(value || '')
.split(/[\r\n,,、]+/)
.map((name) => name.trim())
.filter(Boolean);
}
function normalizeEditableListValues(...sources) {
const values = [];
const seen = new Set();
const append = (value) => {
if (Array.isArray(value)) {
value.forEach(append);
return;
}
for (const item of splitEditableListValues(value)) {
const key = item.toLowerCase();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
values.push(item);
}
};
sources.forEach(append);
return values;
}
function createEditableListPicker(config = {}) {
const {
root,
input,
trigger,
current,
menu,
emptyLabel = '请先添加',
fallbackItems = [],
minItems = 0,
deleteLabel = '删除',
itemLabel = '项目',
normalizeItems = normalizeEditableListValues,
normalizeValue = (value) => String(value || '').trim(),
getItemValue = (item) => String(item || '').trim(),
getItemLabel = (item) => getItemValue(item),
getItemDeleteLabel = (item) => getItemLabel(item),
onDelete = null,
onDeleteError = null,
} = config;
const picker = {
root,
input,
trigger,
current,
menu,
items: [],
open: false,
};
const getFallbackItems = () => normalizeItems(fallbackItems);
const getNormalizedItemValue = (item) => normalizeValue(getItemValue(item));
const findItemByValue = (value) => {
const normalized = normalizeValue(value);
return picker.items.find((item) => getNormalizedItemValue(item) === normalized) || null;
};
const reportDeleteError = (error) => {
const fallbackMessage = `${deleteLabel}${itemLabel}失败。`;
if (typeof onDeleteError === 'function') {
onDeleteError(error, fallbackMessage);
return;
}
if (typeof globalScope.showToast === 'function') {
globalScope.showToast(error?.message || fallbackMessage, 'error');
}
};
picker.setOpen = (open) => {
picker.open = Boolean(open) && !trigger?.disabled;
if (menu) {
menu.hidden = !picker.open;
}
if (trigger) {
trigger.setAttribute('aria-expanded', picker.open ? 'true' : 'false');
trigger.classList?.toggle('is-open', picker.open);
}
};
picker.close = () => {
picker.setOpen(false);
};
picker.setVisible = (visible) => {
if (root) {
root.style.display = visible ? '' : 'none';
}
if (!visible) {
picker.close();
}
};
picker.setSelection = (value, options = {}) => {
const fallback = getNormalizedItemValue(picker.items[0]) || getNormalizedItemValue(getFallbackItems()[0]) || '';
const selected = normalizeValue(value) || fallback;
const selectedItem = findItemByValue(selected);
if (input) {
input.value = selected;
if (options.emit && typeof input.dispatchEvent === 'function') {
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if (current) {
current.textContent = selectedItem ? getItemLabel(selectedItem) : (selected || emptyLabel);
}
};
picker.render = (items = [], selectedValue = '') => {
const normalizedItems = normalizeItems(items);
picker.items = normalizedItems.length ? normalizedItems : getFallbackItems();
const inputValue = normalizeValue(input?.value);
const selected = normalizeValue(selectedValue)
|| (findItemByValue(inputValue) ? inputValue : '')
|| getNormalizedItemValue(picker.items[0])
|| '';
if (trigger) {
trigger.disabled = picker.items.length === 0;
}
if (picker.items.length === 0) {
if (menu) {
menu.innerHTML = '';
}
picker.setSelection('', { emit: false });
picker.close();
return;
}
if (
!menu
|| typeof menu.appendChild !== 'function'
|| typeof globalScope.document === 'undefined'
|| typeof globalScope.document.createElement !== 'function'
) {
picker.setSelection(selected, { emit: false });
return;
}
menu.innerHTML = '';
picker.items.forEach((item) => {
const itemValue = getNormalizedItemValue(item);
const row = globalScope.document.createElement('div');
row.className = 'editable-list-option-row';
const option = globalScope.document.createElement('button');
option.type = 'button';
option.className = 'editable-list-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', itemValue === selected ? 'true' : 'false');
option.textContent = getItemLabel(item);
option.addEventListener('click', () => {
picker.setSelection(itemValue, { emit: true });
picker.close();
});
const deleteButton = globalScope.document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'editable-list-delete';
deleteButton.textContent = deleteLabel;
deleteButton.title = `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`;
deleteButton.setAttribute('aria-label', `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`);
deleteButton.disabled = picker.items.length <= minItems;
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (typeof onDelete === 'function') {
Promise.resolve(onDelete(itemValue, item)).catch(reportDeleteError);
}
});
row.appendChild(option);
row.appendChild(deleteButton);
menu.appendChild(row);
});
picker.setSelection(selected, { emit: false });
};
trigger?.addEventListener('click', (event) => {
event.stopPropagation();
editableListPickers.forEach((item) => {
if (item !== picker) {
item.close();
}
});
picker.setOpen(!picker.open);
});
trigger?.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
picker.close();
}
});
menu?.addEventListener('click', (event) => {
event.stopPropagation();
});
editableListPickers.push(picker);
return picker;
}
function closeEditableListPickers() {
editableListPickers.forEach((picker) => picker.close());
}
function isClickInsideEditableListPicker(target) {
return editableListPickers.some((picker) => Boolean(picker.root?.contains(target)));
}
globalScope.SidepanelEditableListPicker = {
closeEditableListPickers,
createEditableListPicker,
isClickInsideEditableListPicker,
normalizeEditableListValues,
splitEditableListValues,
};
})(window);