00baa0231b
- 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
348 lines
11 KiB
JavaScript
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;
|
|
} |