290 lines
9.5 KiB
JavaScript
290 lines
9.5 KiB
JavaScript
/**
|
|
* Batch Operations Panel Component
|
|
* Builds UI for creating missing text files, append/prepend, and find/replace.
|
|
* Delegates the actual operations to callbacks provided by the caller.
|
|
*/
|
|
|
|
import { createButton, BUTTON_VARIANTS } from '../components/buttons.js';
|
|
import { createInput, createRadioGroup } from '../components/formElements.js';
|
|
import { notifications } from './notifications.js';
|
|
import { createCollapsiblePanel } from '../components/collapsiblePanel.js';
|
|
|
|
/**
|
|
* Create the Batch Operations panel
|
|
* @param {Object} options
|
|
* @param {Function} options.onCreateMissing - async () => Promise<void>
|
|
* @param {Function} options.onAppendStart - async (text: string) => Promise<void>
|
|
* @param {Function} options.onAppendEnd - async (text: string) => Promise<void>
|
|
* @param {Function} options.onReplaceAll - async (findText: string, replaceText: string) => Promise<void>
|
|
* @returns {HTMLElement} Panel element
|
|
*/
|
|
export function createBatchOpsPanel({ onCreateMissing, onAppendStart, onAppendEnd, onReplaceAll, onTrimAll, onDedupLinesAll } = {}) {
|
|
const panel = document.createElement('div');
|
|
panel.style.cssText = `
|
|
display: block;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
background: #3a3a3a;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
border: 1px solid #555;
|
|
`;
|
|
|
|
// Use shared collapsible panel (Unicode carets explicitly enabled per user override)
|
|
const section = createCollapsiblePanel({ titleText: 'Batch Operations', defaultExpanded: true, useUnicodeCarets: true });
|
|
// NOTE: Using Unicode caret glyphs (▾/▸) here by explicit override of the ASCII-only rule.
|
|
// Some search tools may not work properly around these characters; edit with care.
|
|
// Scope selector
|
|
const scopeRow = document.createElement('div');
|
|
scopeRow.style.cssText = `
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
margin-bottom: 6px;
|
|
`;
|
|
const scopeLabel = document.createElement('label');
|
|
scopeLabel.textContent = 'Scope:';
|
|
scopeLabel.style.cssText = `
|
|
color: #fff;
|
|
font-size: 12px;
|
|
min-width: 60px;
|
|
`;
|
|
const { container: scopeRadiosContainer, radios: scopeRadios } = createRadioGroup('batch-scope', [
|
|
{ value: 'folder', label: 'Current folder', checked: true },
|
|
{ value: 'current', label: 'Current image' }
|
|
], { layout: 'horizontal' });
|
|
scopeRow.appendChild(scopeLabel);
|
|
scopeRow.appendChild(scopeRadiosContainer);
|
|
|
|
const content = section.contentEl;
|
|
|
|
// Buttons row container
|
|
const buttonsRow = document.createElement('div');
|
|
buttonsRow.style.cssText = `
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
`;
|
|
|
|
// Create Missing Text
|
|
const createMissingBtn = createButton('Create Missing Text', {
|
|
variant: BUTTON_VARIANTS.SUCCESS,
|
|
size: 'small',
|
|
style: { marginTop: '0' }
|
|
});
|
|
createMissingBtn.addEventListener('click', async () => {
|
|
if (onCreateMissing) {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
await onCreateMissing({ scope });
|
|
}
|
|
});
|
|
|
|
// Append/Prepend group
|
|
const appendGroup = document.createElement('div');
|
|
appendGroup.style.cssText = `
|
|
display: flex;
|
|
gap: 5px;
|
|
align-items: center;
|
|
`;
|
|
|
|
const appendTextInput = createInput({
|
|
type: 'text',
|
|
placeholder: 'Text to append...',
|
|
style: { width: '220px', padding: '6px 10px', background: '#555', border: '1px solid #666', fontSize: '12px' }
|
|
});
|
|
|
|
const prependBtn = createButton('Prepend All', {
|
|
variant: BUTTON_VARIANTS.PRIMARY,
|
|
size: 'small',
|
|
style: { marginTop: '0' }
|
|
});
|
|
|
|
const appendBtn = createButton('Append All', {
|
|
variant: BUTTON_VARIANTS.PRIMARY,
|
|
size: 'small',
|
|
style: { marginTop: '0' }
|
|
});
|
|
|
|
prependBtn.addEventListener('click', async () => {
|
|
const text = appendTextInput.value.trim();
|
|
if (!text) {
|
|
notifications.warning('Please enter text to prepend.');
|
|
return;
|
|
}
|
|
if (onAppendStart) {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
await onAppendStart(text, { scope });
|
|
}
|
|
});
|
|
|
|
appendBtn.addEventListener('click', async () => {
|
|
const text = appendTextInput.value.trim();
|
|
if (!text) {
|
|
notifications.warning('Please enter text to append.');
|
|
return;
|
|
}
|
|
if (onAppendEnd) {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
await onAppendEnd(text, { scope });
|
|
}
|
|
});
|
|
|
|
appendGroup.appendChild(appendTextInput);
|
|
appendGroup.appendChild(prependBtn);
|
|
appendGroup.appendChild(appendBtn);
|
|
|
|
// Find/Replace group
|
|
const replaceGroup = document.createElement('div');
|
|
replaceGroup.style.cssText = `
|
|
display: flex;
|
|
gap: 5px;
|
|
align-items: center;
|
|
margin-top: 8px;
|
|
`;
|
|
|
|
const findTextInput = createInput({
|
|
type: 'text',
|
|
placeholder: 'Find...',
|
|
style: { width: '180px', padding: '6px 10px', background: '#555', border: '1px solid #666', fontSize: '12px' }
|
|
});
|
|
|
|
const replaceTextInput = createInput({
|
|
type: 'text',
|
|
placeholder: 'Replace...',
|
|
style: { width: '180px', padding: '6px 10px', background: '#555', border: '1px solid #666', fontSize: '12px' }
|
|
});
|
|
|
|
const replaceAllBtn = createButton('Replace All', {
|
|
variant: BUTTON_VARIANTS.WARNING,
|
|
size: 'small',
|
|
style: { marginTop: '0' }
|
|
});
|
|
|
|
// Replace options row
|
|
const replaceOptions = document.createElement('div');
|
|
replaceOptions.style.cssText = `
|
|
display: flex;
|
|
gap: 10px;
|
|
align-items: center;
|
|
margin-top: 6px;
|
|
color: #ddd;
|
|
font-size: 12px;
|
|
`;
|
|
// Options are always visible when panel is expanded; no separate Advanced toggle
|
|
|
|
const caseSensitiveCheckbox = document.createElement('input');
|
|
caseSensitiveCheckbox.type = 'checkbox';
|
|
caseSensitiveCheckbox.id = 'batch-case-sensitive';
|
|
const caseSensitiveLabel = document.createElement('label');
|
|
caseSensitiveLabel.htmlFor = 'batch-case-sensitive';
|
|
caseSensitiveLabel.textContent = 'Case sensitive';
|
|
|
|
const wholeWordCheckbox = document.createElement('input');
|
|
wholeWordCheckbox.type = 'checkbox';
|
|
wholeWordCheckbox.id = 'batch-whole-word';
|
|
const wholeWordLabel = document.createElement('label');
|
|
wholeWordLabel.htmlFor = 'batch-whole-word';
|
|
wholeWordLabel.textContent = 'Whole word';
|
|
|
|
const regexCheckbox = document.createElement('input');
|
|
regexCheckbox.type = 'checkbox';
|
|
regexCheckbox.id = 'batch-regex';
|
|
const regexLabel = document.createElement('label');
|
|
regexLabel.htmlFor = 'batch-regex';
|
|
regexLabel.textContent = 'Regex';
|
|
|
|
replaceOptions.appendChild(caseSensitiveCheckbox);
|
|
replaceOptions.appendChild(caseSensitiveLabel);
|
|
replaceOptions.appendChild(wholeWordCheckbox);
|
|
replaceOptions.appendChild(wholeWordLabel);
|
|
replaceOptions.appendChild(regexCheckbox);
|
|
replaceOptions.appendChild(regexLabel);
|
|
|
|
replaceAllBtn.addEventListener('click', async () => {
|
|
const findText = findTextInput.value;
|
|
const replaceText = replaceTextInput.value;
|
|
if (!findText) {
|
|
notifications.warning('Please enter text to find.');
|
|
return;
|
|
}
|
|
if (onReplaceAll) {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
await onReplaceAll(findText, replaceText, {
|
|
caseSensitive: !!caseSensitiveCheckbox.checked,
|
|
wholeWord: !!wholeWordCheckbox.checked,
|
|
useRegex: !!regexCheckbox.checked,
|
|
scope
|
|
});
|
|
}
|
|
});
|
|
|
|
replaceGroup.appendChild(findTextInput);
|
|
replaceGroup.appendChild(replaceTextInput);
|
|
replaceGroup.appendChild(replaceAllBtn);
|
|
|
|
// Assemble
|
|
panel.appendChild(section.container);
|
|
content.appendChild(scopeRow);
|
|
content.appendChild(buttonsRow);
|
|
buttonsRow.appendChild(appendGroup);
|
|
content.appendChild(replaceGroup);
|
|
content.appendChild(replaceOptions);
|
|
|
|
// Utility buttons row
|
|
const utilRow = document.createElement('div');
|
|
utilRow.style.cssText = `
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 10px;
|
|
`;
|
|
|
|
const trimAllBtn = createButton('Trim Whitespace All', {
|
|
variant: BUTTON_VARIANTS.PRIMARY,
|
|
size: 'small',
|
|
style: { marginTop: '0' }
|
|
});
|
|
trimAllBtn.addEventListener('click', async () => {
|
|
if (onTrimAll) {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
await onTrimAll({ scope });
|
|
}
|
|
});
|
|
|
|
const dedupBtn = createButton('Deduplicate Lines All', {
|
|
variant: BUTTON_VARIANTS.PRIMARY,
|
|
size: 'small',
|
|
style: { marginTop: '0' }
|
|
});
|
|
dedupBtn.addEventListener('click', async () => {
|
|
if (onDedupLinesAll) {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
await onDedupLinesAll({ scope });
|
|
}
|
|
});
|
|
|
|
utilRow.appendChild(createMissingBtn);
|
|
utilRow.appendChild(trimAllBtn);
|
|
utilRow.appendChild(dedupBtn);
|
|
content.appendChild(utilRow);
|
|
|
|
// Update labels based on scope (remove "All" when scope is current)
|
|
const updateScopeLabels = () => {
|
|
const scope = (scopeRadios && scopeRadios.find(r => r.radio.checked)?.radio.value) || 'folder';
|
|
prependBtn.textContent = scope === 'current' ? 'Prepend' : 'Prepend All';
|
|
appendBtn.textContent = scope === 'current' ? 'Append' : 'Append All';
|
|
trimAllBtn.textContent = scope === 'current' ? 'Trim Whitespace' : 'Trim Whitespace All';
|
|
dedupBtn.textContent = scope === 'current' ? 'Deduplicate Lines' : 'Deduplicate Lines All';
|
|
// createMissingBtn stays as 'Create Missing Text'
|
|
};
|
|
// Attach change listeners to radios
|
|
if (scopeRadios && Array.isArray(scopeRadios)) {
|
|
scopeRadios.forEach(({ radio }) => {
|
|
radio.addEventListener('change', updateScopeLabels);
|
|
});
|
|
}
|
|
updateScopeLabels();
|
|
|
|
return panel;
|
|
}
|