v2.3 Papierkorb: renderTrash, Wiederherstellen, endgueltig loeschen, leeren
This commit is contained in:
@@ -49,6 +49,7 @@ function openSettings() {
|
|||||||
document.getElementById('settingsOverlay').classList.add('active');
|
document.getElementById('settingsOverlay').classList.add('active');
|
||||||
});
|
});
|
||||||
panel.setAttribute('aria-hidden', 'false');
|
panel.setAttribute('aria-hidden', 'false');
|
||||||
|
renderTrash();
|
||||||
_settingsTrap = _makeTrap(panel, closeSettings);
|
_settingsTrap = _makeTrap(panel, closeSettings);
|
||||||
document.addEventListener('keydown', _settingsTrap);
|
document.addEventListener('keydown', _settingsTrap);
|
||||||
const first = _focusable(panel)[0];
|
const first = _focusable(panel)[0];
|
||||||
@@ -141,6 +142,161 @@ function initAccordion() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- PAPIERKORB ----
|
||||||
|
/**
|
||||||
|
* Formatiert einen deletedAt-Timestamp lokalisiert (folgt der aktiven UI-Sprache).
|
||||||
|
* @param {number} ts - Millisekunden-Timestamp
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatTrashDate(ts) {
|
||||||
|
const locale = I18n.currentLang === 'de' ? 'de-DE' : 'en-US';
|
||||||
|
return new Date(ts).toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert den Papierkorb in die Settings-Section. Wird bei jedem openSettings()
|
||||||
|
* sowie nach jeder Trash-Mutation aufgerufen. Baut DOM ohne innerHTML (XSS-frei,
|
||||||
|
* Titel kommen aus User-/Importdaten).
|
||||||
|
*/
|
||||||
|
function renderTrash() {
|
||||||
|
const listEl = document.getElementById('trashList');
|
||||||
|
const actionsRow = document.getElementById('trashActionsRow');
|
||||||
|
if (!listEl) return;
|
||||||
|
listEl.replaceChildren();
|
||||||
|
|
||||||
|
if (trash.length === 0) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'trash-empty';
|
||||||
|
empty.textContent = t('trash.empty');
|
||||||
|
listEl.appendChild(empty);
|
||||||
|
if (actionsRow) actionsRow.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actionsRow) actionsRow.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Neueste zuerst.
|
||||||
|
const sorted = [...trash].sort((a, b) => b.deletedAt - a.deletedAt);
|
||||||
|
sorted.forEach(entry => listEl.appendChild(createTrashItemEl(entry)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut eine einzelne Papierkorb-Zeile.
|
||||||
|
* @param {Object} entry - trash-Eintrag { item, type, originBoardId, deletedAt }
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
function createTrashItemEl(entry) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'trash-item';
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.className = 'trash-item-info';
|
||||||
|
|
||||||
|
const titleLine = document.createElement('span');
|
||||||
|
titleLine.className = 'trash-item-title';
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'trash-item-badge';
|
||||||
|
badge.textContent = entry.type === 'board' ? t('trash.type.board') : t('trash.type.bookmark');
|
||||||
|
const titleText = document.createTextNode(entry.item && entry.item.title ? entry.item.title : '');
|
||||||
|
titleLine.append(badge, titleText);
|
||||||
|
|
||||||
|
const meta = document.createElement('span');
|
||||||
|
meta.className = 'trash-item-meta';
|
||||||
|
let metaText = t('trash.deleted_at', { date: formatTrashDate(entry.deletedAt) });
|
||||||
|
if (entry.type === 'bookmark') {
|
||||||
|
const origin = entry.originBoardId ? boards.find(b => b.id === entry.originBoardId) : null;
|
||||||
|
metaText += origin
|
||||||
|
? ' · ' + t('trash.from_board', { board: origin.title })
|
||||||
|
: ' · ' + t('trash.from_board_unknown');
|
||||||
|
}
|
||||||
|
meta.textContent = metaText;
|
||||||
|
|
||||||
|
info.append(titleLine, meta);
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'trash-item-actions';
|
||||||
|
|
||||||
|
const btnRestore = document.createElement('button');
|
||||||
|
btnRestore.className = 'btn-small';
|
||||||
|
btnRestore.textContent = t('trash.restore');
|
||||||
|
btnRestore.title = t('trash.restore_title');
|
||||||
|
btnRestore.addEventListener('click', () => restoreTrashEntry(entry));
|
||||||
|
|
||||||
|
const btnForever = document.createElement('button');
|
||||||
|
btnForever.className = 'btn-danger';
|
||||||
|
btnForever.textContent = t('trash.delete_forever');
|
||||||
|
btnForever.title = t('trash.delete_forever_title');
|
||||||
|
btnForever.addEventListener('click', () => deleteTrashEntryForever(entry));
|
||||||
|
|
||||||
|
actions.append(btnRestore, btnForever);
|
||||||
|
row.append(info, actions);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stellt einen Papierkorb-Eintrag wieder her.
|
||||||
|
* Bookmark -> in originBoardId (falls noch vorhanden), sonst in die Inbox (ensureInboxBoard).
|
||||||
|
* Board -> zurueck in boards[].
|
||||||
|
* @param {Object} entry
|
||||||
|
*/
|
||||||
|
async function restoreTrashEntry(entry) {
|
||||||
|
if (entry.type === 'board') {
|
||||||
|
// Ganzes Board zurueck (inkl. blurred). Falls die id schon existiert (Edge-Case),
|
||||||
|
// neue uid vergeben, damit nichts ueberschrieben wird.
|
||||||
|
const restored = structuredClone(entry.item);
|
||||||
|
if (boards.some(b => b.id === restored.id)) restored.id = uid();
|
||||||
|
boards.push(restored);
|
||||||
|
await saveBoards();
|
||||||
|
} else {
|
||||||
|
const restored = structuredClone(entry.item);
|
||||||
|
let target = entry.originBoardId ? boards.find(b => b.id === entry.originBoardId) : null;
|
||||||
|
let toInbox = false;
|
||||||
|
if (!target) {
|
||||||
|
// Ursprungs-Board weg -> in die Inbox (Page-Wrapper ensureInboxBoard aus Phase 1).
|
||||||
|
target = await ensureInboxBoard();
|
||||||
|
toInbox = true;
|
||||||
|
}
|
||||||
|
target.bookmarks.push(restored);
|
||||||
|
await saveBoards();
|
||||||
|
if (toInbox) {
|
||||||
|
await HellionDialog.alert(t('trash.restored_to_inbox'), { type: 'info', title: t('trash.restored_to_inbox.title') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trash = trash.filter(e => e !== entry);
|
||||||
|
await saveTrash();
|
||||||
|
renderTrash();
|
||||||
|
renderBoards();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loescht einen einzelnen Papierkorb-Eintrag endgueltig (mit Confirm).
|
||||||
|
* @param {Object} entry
|
||||||
|
*/
|
||||||
|
async function deleteTrashEntryForever(entry) {
|
||||||
|
const ok = await HellionDialog.confirm(
|
||||||
|
t('trash.delete_forever_confirm'),
|
||||||
|
{ type: 'danger', title: t('trash.delete_forever_confirm.title'), confirmText: t('trash.delete_forever') }
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
trash = trash.filter(e => e !== entry);
|
||||||
|
await saveTrash();
|
||||||
|
renderTrash();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leert den gesamten Papierkorb (mit Confirm).
|
||||||
|
*/
|
||||||
|
async function emptyTrash() {
|
||||||
|
if (trash.length === 0) return;
|
||||||
|
const ok = await HellionDialog.confirm(
|
||||||
|
t('trash.empty_confirm', { count: trash.length }),
|
||||||
|
{ type: 'danger', title: t('trash.empty_confirm.title'), confirmText: t('trash.empty_btn') }
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
trash = [];
|
||||||
|
await saveTrash();
|
||||||
|
renderTrash();
|
||||||
|
}
|
||||||
|
|
||||||
// ---- APPLY SETTINGS ----
|
// ---- APPLY SETTINGS ----
|
||||||
function applySettings() {
|
function applySettings() {
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
@@ -336,6 +492,10 @@ function bindSettingsEvents() {
|
|||||||
Onboarding.start();
|
Onboarding.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Papierkorb leeren
|
||||||
|
const btnEmptyTrash = document.getElementById('btnEmptyTrash');
|
||||||
|
if (btnEmptyTrash) btnEmptyTrash.addEventListener('click', emptyTrash);
|
||||||
|
|
||||||
// Reset All
|
// Reset All
|
||||||
document.getElementById('btnResetAll').addEventListener('click', async () => {
|
document.getElementById('btnResetAll').addEventListener('click', async () => {
|
||||||
const ok = await HellionDialog.confirm(
|
const ok = await HellionDialog.confirm(
|
||||||
@@ -344,11 +504,13 @@ function bindSettingsEvents() {
|
|||||||
);
|
);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
boards = [];
|
boards = [];
|
||||||
|
trash = [];
|
||||||
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
|
||||||
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
|
||||||
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
|
showSearch: true, searchEngine: 'google', toolbarPos: 'right',
|
||||||
imageRefEnabled: false, language: 'auto' };
|
imageRefEnabled: false, language: 'auto' };
|
||||||
await saveBoards();
|
await saveBoards();
|
||||||
|
await saveTrash();
|
||||||
await saveSettings();
|
await saveSettings();
|
||||||
setLanguage('auto');
|
setLanguage('auto');
|
||||||
applySettings();
|
applySettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user