Files
Hellion-NewTab/src/js/boards.js
T
JonKazama-Hellion 00baa0231b feat(ui): Custom Dialog-System, Onboarding und Backup-Reminder
- HellionDialog.alert/confirm ersetzt alle nativen confirm() und alert() Aufrufe
- 6-stufiger Onboarding-Flow beim ersten Start (Boards, Themes, Features, Backup)
- Backup-Reminder erinnert alle 7 Tage an JSON-Export
- innerHTML komplett durch createElement/createElementNS ersetzt (XSS-Schutz)
- Drag & Drop Inline-Styles durch CSS-Klassen ersetzt
2026-03-21 19:08:17 +01:00

348 lines
11 KiB
JavaScript

/* =============================================
HELLION NEWTAB — boards.js
Board & Bookmark Rendering, Modals
============================================= */
let pendingBookmarkBoardId = null;
let pendingRenameCallback = null;
const SVG_NS = 'http://www.w3.org/2000/svg';
/**
* Erzeugt ein SVG-Element mit Attributen und Kinder-Elementen.
* @param {string} tag - SVG-Tag (z.B. 'svg', 'circle', 'line')
* @param {Object} attrs - Attribute als Key-Value
* @param {Array} children - Kind-Elemente
* @returns {SVGElement}
*/
function svgEl(tag, attrs, children) {
const el = document.createElementNS(SVG_NS, tag);
if (attrs) {
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
}
if (children) {
for (const child of children) el.appendChild(child);
}
return el;
}
/** Erzeugt das 6-Punkt Drag-Handle SVG */
function createDragHandleSvg() {
return svgEl('svg', { width: '10', height: '14', viewBox: '0 0 10 14', fill: 'currentColor' }, [
svgEl('circle', { cx: '2', cy: '2', r: '1.5' }),
svgEl('circle', { cx: '8', cy: '2', r: '1.5' }),
svgEl('circle', { cx: '2', cy: '7', r: '1.5' }),
svgEl('circle', { cx: '8', cy: '7', r: '1.5' }),
svgEl('circle', { cx: '2', cy: '12', r: '1.5' }),
svgEl('circle', { cx: '8', cy: '12', r: '1.5' }),
]);
}
/** Erzeugt das Plus-Icon SVG */
function createPlusSvg() {
return svgEl('svg', { width: '11', height: '11', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '2' }, [
svgEl('line', { x1: '12', y1: '5', x2: '12', y2: '19' }),
svgEl('line', { x1: '5', y1: '12', x2: '19', y2: '12' }),
]);
}
// ---- RENDER ----
function renderBoards() {
const wrapper = document.getElementById('boardsWrapper');
wrapper.replaceChildren();
if (boards.length === 0) {
const empty = document.createElement('div');
empty.className = 'empty-state';
const boardStrong = document.createElement('strong');
boardStrong.className = 'accent-text';
boardStrong.textContent = '+ Board';
const importStrong = document.createElement('strong');
importStrong.className = 'accent-text';
importStrong.textContent = 'Import';
empty.append(
'No boards yet. Click ', boardStrong, ' to create one, or use ', importStrong, ' to load your browser bookmarks.'
);
wrapper.appendChild(empty);
return;
}
boards.forEach(board => wrapper.appendChild(createBoardEl(board)));
initBoardDragDrop();
}
function createBoardEl(board) {
const div = document.createElement('div');
div.className = 'board' + (board.blurred ? ' blurred' : '');
div.dataset.boardId = board.id;
// Header
const header = document.createElement('div');
header.className = 'board-header';
const dragHandle = document.createElement('span');
dragHandle.className = 'board-drag-handle';
dragHandle.title = 'Board verschieben';
dragHandle.appendChild(createDragHandleSvg());
const titleSpanHeader = document.createElement('span');
titleSpanHeader.className = 'board-title';
titleSpanHeader.title = board.title;
titleSpanHeader.textContent = board.title;
const actions = document.createElement('div');
actions.className = 'board-actions';
const btnBlur = document.createElement('button');
btnBlur.className = 'board-action-btn btn-blur-board';
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)';
btnBlur.textContent = '\uD83D\uDD12';
const btnRename = document.createElement('button');
btnRename.className = 'board-action-btn btn-rename-board';
btnRename.title = 'Umbenennen';
btnRename.textContent = '\u270E';
const btnDelete = document.createElement('button');
btnDelete.className = 'board-action-btn btn-delete-board';
btnDelete.title = 'Löschen';
btnDelete.textContent = '\u2715';
actions.append(btnBlur, btnRename, btnDelete);
header.append(dragHandle, titleSpanHeader, actions);
// Blur-Overlay
const blurOverlay = document.createElement('div');
blurOverlay.className = 'board-blur-overlay';
div.appendChild(blurOverlay);
btnBlur.addEventListener('click', async e => {
e.stopPropagation();
board.blurred = !board.blurred;
div.classList.toggle('blurred', board.blurred);
btnBlur.title = board.blurred ? 'Unblur' : 'Blur (privat)';
await saveBoards();
});
blurOverlay.addEventListener('click', async () => {
board.blurred = false;
div.classList.remove('blurred');
btnBlur.title = 'Blur (privat)';
await saveBoards();
});
btnRename.addEventListener('click', e => {
e.stopPropagation();
openRenameModal(board.title, async newName => {
if (!newName.trim()) return;
board.title = newName.trim();
await saveBoards();
renderBoards();
});
});
btnDelete.addEventListener('click', async e => {
e.stopPropagation();
const ok = await HellionDialog.confirm(
`Board "${board.title}" wirklich löschen?`,
{ type: 'danger', title: 'Board löschen', confirmText: 'Löschen' }
);
if (ok) {
boards = boards.filter(b => b.id !== board.id);
await saveBoards();
renderBoards();
}
});
// Bookmark List
const list = document.createElement('ul');
list.className = 'board-list';
list.dataset.boardId = board.id;
const visibleCount = settings.hideExtra ? settings.visibleCount : board.bookmarks.length;
const visible = board.bookmarks.slice(0, visibleCount);
const hidden = board.bookmarks.slice(visibleCount);
visible.forEach(bm => list.appendChild(createBmEl(bm)));
div.appendChild(header);
div.appendChild(list);
// Event Delegation für Bookmark-Klicks und -Löschungen
bindBoardListEvents(list, board);
// Show More
if (hidden.length > 0) {
let expanded = false;
let hiddenEls = [];
const showMoreBtn = document.createElement('button');
showMoreBtn.className = 'show-more-btn';
showMoreBtn.textContent = `Show ${hidden.length} more…`;
showMoreBtn.addEventListener('click', () => {
if (!expanded) {
hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); });
showMoreBtn.textContent = 'Show less';
expanded = true;
} else {
hiddenEls.forEach(el => el.remove());
hiddenEls = [];
showMoreBtn.textContent = `Show ${hidden.length} more…`;
expanded = false;
}
});
div.appendChild(showMoreBtn);
}
// Add Bookmark
const addBtn = document.createElement('button');
addBtn.className = 'add-bm-btn';
addBtn.appendChild(createPlusSvg());
addBtn.append(' Add link');
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
div.appendChild(addBtn);
initBookmarkDragDrop(list, board);
return div;
}
function createBmEl(bm) {
const li = document.createElement('li');
li.className = 'bm-item';
li.dataset.bmId = bm.id;
li.dataset.bmUrl = bm.url;
li.draggable = true;
const favicon = document.createElement('img');
favicon.className = 'bm-favicon';
favicon.width = 14;
favicon.height = 14;
favicon.src = getFaviconUrl(bm.url);
favicon.addEventListener('error', function() {
this.classList.add('hidden');
this.nextElementSibling.classList.remove('hidden');
});
const fallback = document.createElement('div');
fallback.className = 'bm-favicon-fallback hidden';
fallback.textContent = bm.title.charAt(0).toUpperCase();
const textDiv = document.createElement('div');
textDiv.className = 'bm-text';
const titleSpan = document.createElement('span');
titleSpan.className = 'bm-title';
titleSpan.title = bm.title;
titleSpan.textContent = bm.title;
const descSpan = document.createElement('span');
descSpan.className = 'bm-desc';
descSpan.textContent = bm.desc || '';
textDiv.appendChild(titleSpan);
textDiv.appendChild(descSpan);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'bm-delete';
deleteBtn.title = 'Entfernen';
deleteBtn.textContent = '✕';
li.appendChild(favicon);
li.appendChild(fallback);
li.appendChild(textDiv);
li.appendChild(deleteBtn);
return li;
}
// Event Delegation: Ein Listener pro Board-Liste statt pro Bookmark
function bindBoardListEvents(list, board) {
list.addEventListener('click', async e => {
const bmItem = e.target.closest('.bm-item');
if (!bmItem) return;
// Delete-Button geklickt
if (e.target.closest('.bm-delete')) {
e.stopPropagation();
const bmId = bmItem.dataset.bmId;
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
await saveBoards();
renderBoards();
return;
}
// Bookmark-Link geklickt
const url = bmItem.dataset.bmUrl;
if (url) {
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
}
});
}
// ---- MODALS ----
function openModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function openAddBoardModal() {
document.getElementById('newBoardName').value = '';
openModal('addBoardOverlay');
setTimeout(() => document.getElementById('newBoardName').focus(), 50);
}
function openAddBookmarkModal(boardId) {
pendingBookmarkBoardId = boardId;
['newBmTitle','newBmUrl','newBmDesc'].forEach(id => document.getElementById(id).value = '');
openModal('addBookmarkOverlay');
setTimeout(() => document.getElementById('newBmTitle').focus(), 50);
}
function openRenameModal(currentName, callback) {
pendingRenameCallback = callback;
document.getElementById('renameInput').value = currentName;
openModal('renameOverlay');
setTimeout(() => { const i = document.getElementById('renameInput'); i.focus(); i.select(); }, 50);
}
// ---- BOOKMARK HTML IMPORT ----
function parseBookmarkHtml(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const result = [];
function parseFolder(dlEl, folderName) {
const bms = [];
dlEl.querySelectorAll(':scope > dt').forEach(dt => {
const a = dt.querySelector(':scope > a');
const h3 = dt.querySelector(':scope > h3');
if (a && a.href) {
bms.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
} else if (h3) {
const subDl = dt.querySelector(':scope > dl');
if (subDl) {
const sub = parseFolder(subDl, h3.textContent.trim());
if (sub.bookmarks.length > 0) result.push(sub);
}
}
});
return { id: uid(), title: folderName || 'Imported', bookmarks: bms };
}
const topDts = doc.querySelectorAll('body > dl > dt, body > p > dl > dt');
if (topDts.length === 0) {
const allLinks = [];
doc.querySelectorAll('a').forEach(a => {
if (a.href && !a.href.startsWith('place:'))
allLinks.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
});
if (allLinks.length > 0) result.push({ id: uid(), title: 'Imported Bookmarks', bookmarks: allLinks });
} else {
doc.querySelectorAll('body > dl > dt, body > p > dl > dt').forEach(dt => {
const h3 = dt.querySelector(':scope > h3');
const dl = dt.querySelector(':scope > dl');
if (h3 && dl) {
const folder = parseFolder(dl, h3.textContent.trim());
if (folder.bookmarks.length > 0) result.push(folder);
}
});
}
return result;
}