window.escHtml = (s) => String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
window.copyToClipboard = async (text, $btnElement) => {
try {
await navigator.clipboard.writeText(text);
const originalHtml = $btnElement.html();
$btnElement.text("Copied!");
setTimeout(() => { $btnElement.html(originalHtml); }, 1500);
} catch (err) {
console.error('Failed to copy', err);
}
};
class ToolHistory {
constructor(toolId, maxEntries = 10) {
this.key = 'bt-history-' + toolId;
this.maxEntries = maxEntries;
this.cursor = -1; this.draft = '';
try {
this.entries = JSON.parse(localStorage.getItem(this.key) || '[]');
} catch (e) {
this.entries = [];
}
}
push(value) {
if (!value || !value.trim()) return;
this.entries = this.entries.filter(e => e !== value);
this.entries.unshift(value);
if (this.entries.length > this.maxEntries) this.entries.length = this.maxEntries;
localStorage.setItem(this.key, JSON.stringify(this.entries));
this.cursor = -1;
}
prev(currentDraft) {
if (this.entries.length === 0) return null;
if (this.cursor === -1) this.draft = currentDraft;
if (this.cursor < this.entries.length - 1) this.cursor++;
return this.entries[this.cursor];
}
next() {
if (this.cursor === -1) return null; if (this.cursor === 0) { this.cursor = -1; return this.draft; }
this.cursor--;
return this.entries[this.cursor];
}
getAll() { return this.entries; }
clear() {
this.entries = [];
this.cursor = -1;
localStorage.removeItem(this.key);
}
}
window.ToolHistory = ToolHistory;
window.attachToolHistory = (inputEl, toolId, opts = {}) => {
const $input = $(inputEl);
const history = new ToolHistory(toolId);
const getValue = opts.getValue || (() => $input.val());
const setValue = opts.setValue || ((v) => { $input.val(v).trigger('input'); });
if (!$input.parent().hasClass('tool-history-input-wrap')) {
$input.wrap('<div class="tool-history-input-wrap relative flex-1 flex flex-col min-h-0"></div>');
}
const $wrap = $input.parent();
const $btn = $(`<button type="button" class="tool-history-btn absolute top-2 right-2 z-10 text-gray-600 hover:text-gray-300 bg-gray-900/70 rounded p-1 transition-colors" title="Input history"><svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg></button>`);
const $dropdown = $('<div class="tool-history-dropdown hidden absolute top-9 right-2 z-20 w-64 max-h-56 overflow-y-auto bg-gray-900 border border-gray-700 rounded-lg shadow-2xl text-xs"></div>');
$wrap.append($btn, $dropdown);
const renderDropdown = () => {
const items = history.getAll();
if (!items.length) {
$dropdown.html('<div class="px-3 py-2 text-gray-600 italic">No history yet</div>');
return;
}
$dropdown.html(
items.map((v, i) => `<div class="tool-history-item px-3 py-2 hover:bg-gray-800 cursor-pointer text-gray-300 truncate border-b border-gray-800" data-idx="${i}" title="${window.escHtml(v)}">${window.escHtml(v.slice(0, 80))}</div>`).join('') +
'<div class="tool-history-clear px-3 py-2 hover:bg-gray-800 cursor-pointer text-rose-400 font-semibold">Clear history</div>'
);
};
$btn.on('click', (e) => {
e.stopPropagation();
renderDropdown();
$dropdown.toggleClass('hidden');
});
$dropdown.on('click', function(e) { e.stopPropagation(); });
$dropdown.on('click', '.tool-history-item', function() {
setValue(history.getAll()[$(this).data('idx')]);
$dropdown.addClass('hidden');
});
$dropdown.on('click', '.tool-history-clear', function() {
history.clear();
renderDropdown();
});
$(document).on('click', () => $dropdown.addClass('hidden'));
$input.on('keydown', function(e) {
const el = this;
const isTextarea = el.tagName === 'TEXTAREA';
const atStart = el.selectionStart === 0 && el.selectionEnd === 0;
const atEnd = el.selectionStart === el.value.length && el.selectionEnd === el.value.length;
if (e.key === 'ArrowUp' && (!isTextarea || atStart)) {
const val = history.prev(getValue());
if (val !== null) { e.preventDefault(); setValue(val); }
} else if (e.key === 'ArrowDown' && (!isTextarea || atEnd)) {
const val = history.next();
if (val !== null) { e.preventDefault(); setValue(val); }
}
});
$input.on('blur', () => history.push(getValue()));
return history;
};