/* * Folders, the same everywhere (docs/library.md): like tags (a thing may be in several), * shown as a row of filter chips; assigned with a sheet of checkboxes; managed in another * (rename = merge onto an existing name, delete = the things stay, order by ↑ ↓). Each area * keeps its folders where it keeps them and calls these for the look and the dialogs: * * DioraFolders.chips(el, {folders: [{name, count}], active, none, lead, onPick, onManage}) * DioraFolders.assign({title, folders: [names], selected: [names], onDone(names)}) * DioraFolders.manage({title, folders: [names], onRename(old, new), onDelete(name), onOrder(names)}) * * Runs everywhere (web, desktop, both Android apps). Chrome-61-safe: no ?. / ??. */ (function () { 'use strict'; function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c])); } // ---- the chip row ---------------------------------------------------------------------- // lead: chips before the folders (the library's Angefangen/Ungelesen …): [{id, label, count, on}], // picked with onPick({lead: id}). active: null = all, '' = without a folder, else a name. function chips(el, o) { const lead = o.lead || []; const leadOn = lead.some(c => c.on); let h = ''; el.onclick = e => { const b = e.target.closest('button'); if (!b) return; if (b.dataset.dfoManage) { o.onManage(); return; } if (b.dataset.dfoAll) { o.onPick(null); return; } if (b.dataset.dfoLead != null) { o.onPick({lead: b.dataset.dfoLead}); return; } if (b.dataset.dfoFolder != null) { const name = b.dataset.dfoFolder; o.onPick(o.active === name ? null : name); // a second tap shows everything again } }; } function chip(cls, label, count, on, attrs) { return ''; } // ---- a sheet (above the page and the book views, below the page's dialogs) ------------------- let open = null; function sheet(title, body, onClose) { close(); const wrap = document.createElement('div'); wrap.className = 'dfo-scrim'; wrap.innerHTML = ''; wrap.querySelector('.dfo-body').appendChild(body); wrap.addEventListener('click', e => { if (e.target === wrap || e.target.closest('[data-dfo-close]')) close(); }); document.body.appendChild(wrap); open = {wrap, onClose}; return wrap; } function close() { if (!open) return; const o = open; open = null; o.wrap.remove(); if (o.onClose) o.onClose(); } const back = window.__dioraBack; window.__dioraBack = function () { if (open) { close(); return true; } return back ? back() : false; }; document.addEventListener('keydown', e => { if (e.key === 'Escape' && open) close(); }); // ---- "In Ordner …" --------------------------------------------------------------------------- function assign(o) { const chosen = new Set(o.selected || []); const names = (o.folders || []).slice(); const body = document.createElement('div'); const draw = () => { body.innerHTML = '
' + (names.length ? names.map(n => '').join('') : '

Noch keine Ordner.

') + '
' + '
' + '
' + '
' + '
'; }; draw(); body.addEventListener('change', e => { if (e.target.type !== 'checkbox') return; if (e.target.checked) chosen.add(e.target.value); else chosen.delete(e.target.value); }); body.addEventListener('submit', e => { e.preventDefault(); const n = ' '.concat(e.target.n.value).split(/\s+/).filter(Boolean).join(' ').slice(0, 100); if (!n) return; const same = names.filter(x => x.toLowerCase() === n.toLowerCase())[0]; if (!same) names.push(n); chosen.add(same || n); draw(); }); body.addEventListener('click', e => { if (!e.target.closest('[data-dfo-ok]')) return; const picked = names.filter(n => chosen.has(n)); close(); o.onDone(picked); }); sheet(o.title || 'In Ordner', body); } // ---- "Ordner verwalten" --------------------------------------------------------------------------- function manage(o) { let names = (o.folders || []).slice(); const body = document.createElement('div'); const draw = () => { body.innerHTML = names.length ? '
' + names.map((n, i) => '
' + esc(n) + '' + '' + '' + '' + '
').join('') + '
' + '

Umbenennen auf einen vorhandenen Namen legt beide zusammen. Löschen entfernt nur den Ordner, nicht seinen Inhalt.

' : '

Noch keine Ordner. Sie entstehen beim Einordnen („In Ordner …“).

'; }; draw(); body.addEventListener('click', async e => { const b = e.target.closest('button'); if (!b) return; const ask = (msg, value) => typeof customPrompt === 'function' ? customPrompt(msg, value) : Promise.resolve(window.prompt(msg, value)); const sure = msg => typeof customConfirm === 'function' ? customConfirm(msg, {danger: true}) : Promise.resolve(window.confirm(msg)); if (b.dataset.up != null || b.dataset.down != null) { const i = Number(b.dataset.up != null ? b.dataset.up : b.dataset.down); const j = b.dataset.up != null ? i - 1 : i + 1; if (j < 0 || j >= names.length) return; const t = names[i]; names[i] = names[j]; names[j] = t; draw(); o.onOrder(names.slice()); } else if (b.dataset.rename != null) { const old = names[Number(b.dataset.rename)]; const value = await ask('Neuer Name für „' + old + '“:', old); const n = value == null ? '' : String(value).split(/\s+/).filter(Boolean).join(' ').slice(0, 100); if (!n || n === old) return; await o.onRename(old, n); const merged = names.filter(x => x.toLowerCase() === n.toLowerCase())[0]; names = merged ? names.filter(x => x !== old) : names.map(x => (x === old ? n : x)); draw(); } else if (b.dataset.delete != null) { const n = names[Number(b.dataset.delete)]; if (!(await sure('Ordner „' + n + '“ löschen? Was darin liegt, bleibt.'))) return; await o.onDelete(n); names = names.filter(x => x !== n); draw(); } }); sheet(o.title || 'Ordner verwalten', body); } window.DioraFolders = {chips, assign, manage, close}; // ---- styles ---------------------------------------------------------------------------------------- const style = document.createElement('style'); style.textContent = [ '.dfo-row { display: flex; gap: 6px; align-items: center; overflow-x: auto; padding: 2px 0 6px; scrollbar-width: none; }', '.dfo-row::-webkit-scrollbar { display: none; }', '@media (min-width: 721px) { .dfo-row { flex-wrap: wrap; overflow: visible; } }', '.dfo-chip { flex: none; min-height: 34px; padding: 0 12px; border-radius: 17px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer; white-space: nowrap; }', '.dfo-chip.on { background: var(--accent, #e63946); border-color: var(--accent, #e63946); color: var(--on-accent, #fff); }', '.dfo-count { opacity: .7; font-size: 12px; }', '.dfo-none { font-style: italic; }', '.dfo-manage { min-width: 34px; padding: 0 8px; }', '.dfo-sep { flex: none; width: 1px; height: 20px; background: var(--border-strong, #444); margin: 0 2px; }', // Above the book view (900), below the page's dialogs (990): a name is asked for on top. '.dfo-scrim { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 950; background: var(--scrim, rgba(0,0,0,.5)); display: flex; align-items: flex-end; justify-content: center; }', '@media (min-width: 721px) { .dfo-scrim { align-items: center; } }', '.dfo-sheet { width: 100%; max-width: 480px; max-height: 80vh; overflow-y: auto; background: var(--panel, #161616); color: var(--fg, #eee); border-radius: 14px 14px 0 0; padding: 12px 16px 18px; box-sizing: border-box; }', '@media (min-width: 721px) { .dfo-sheet { border-radius: 14px; } }', '.dfo-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }', '.dfo-x, .dfo-icon { background: none; border: none; color: inherit; font: inherit; font-size: 17px; min-width: 38px; min-height: 38px; cursor: pointer; }', '.dfo-icon[disabled] { opacity: .3; cursor: default; }', '.dfo-check { display: flex; align-items: center; gap: 10px; padding: 9px 2px; border-bottom: 1px solid var(--border, #2a2a2a); cursor: pointer; }', '.dfo-check input { width: 18px; height: 18px; }', '.dfo-mrow { display: flex; align-items: center; border-bottom: 1px solid var(--border, #2a2a2a); }', '.dfo-mname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.dfo-new { display: flex; gap: 8px; margin: 12px 0 4px; }', '.dfo-input { flex: 1; min-width: 0; padding: 9px 10px; border-radius: 6px; border: 1px solid var(--border, #2a2a2a); background: var(--bg, #000); color: inherit; font: inherit; }', '.dfo-btn { min-height: 38px; padding: 0 14px; border-radius: 19px; border: 1px solid var(--border-strong, #444); background: none; color: inherit; font: inherit; font-size: 14px; cursor: pointer; }', '.dfo-primary { background: var(--accent, #e63946); border-color: var(--accent, #e63946); color: var(--on-accent, #fff); }', '.dfo-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }', '.dfo-note { color: var(--muted, #9a9a9a); font-size: 13px; line-height: 1.45; }', 'body.eink .dfo-chip.on, body.eink .dfo-primary { background: #000; color: #fff; }', ].join('\n'); document.head.appendChild(style); })();