UNPKG

p3x-onenote

Version:

📚 P3X OneNote Linux

507 lines (441 loc) • 20.6 kB
import registry from './registry.mjs' // ── Toast ────────────────────────────────────────────────────────── const toastContainer = document.getElementById('p3x-toast-container'); let currentToast = null; function dismissCurrentToast() { if (currentToast) { const el = currentToast; currentToast = null; el.classList.add('p3x-toast-out'); el.addEventListener('animationend', () => el.remove(), { once: true }); } } function showToast(options) { if (typeof options === 'string') { options = { message: options }; } // Dismiss any existing toast dismissCurrentToast(); const el = document.createElement('div'); el.className = 'p3x-toast'; el.textContent = options.message; currentToast = el; el.addEventListener('click', () => { if (currentToast === el) dismissCurrentToast(); }); toastContainer.appendChild(el); if (!options.sticky) { const duration = options.duration || 5000; setTimeout(() => { if (currentToast === el) dismissCurrentToast(); }, duration); } } export const p3xToast = { action: showToast, setProxy: { clear: () => showToast(registry.lang.dialog.setProxy.clear), set: (value) => showToast(registry.lang.dialog.setProxy.set(value)), }, }; // ── Dialog helpers ──────────────────────────────────────────────── const dialogEl = document.getElementById('p3x-dialog'); function closeDialog() { dialogEl.close(); dialogEl.innerHTML = ''; } // ── Prompt (single text input) ─────────────────────────────────── function showPrompt({ title, text, placeholder, initialValue, cancelText, okText }) { return new Promise((resolve, reject) => { dialogEl.innerHTML = ` <form method="dialog"> <h3 class="p3x-dialog-title">${title}</h3> <div class="p3x-dialog-body"> ${text ? `<p>${text}</p>` : ''} <input class="p3x-dialog-input" type="text" placeholder="${placeholder || ''}" value="${initialValue || ''}"> </div> <div class="p3x-dialog-actions"> <button type="button" class="p3x-btn" data-action="cancel">${cancelText}</button> <button type="submit" class="p3x-btn">${okText}</button> </div> </form>`; const input = dialogEl.querySelector('input'); const cancelBtn = dialogEl.querySelector('[data-action="cancel"]'); cancelBtn.onclick = () => { closeDialog(); reject(undefined); }; dialogEl.querySelector('form').onsubmit = (e) => { e.preventDefault(); const val = input.value; closeDialog(); resolve(val); }; dialogEl.addEventListener('cancel', () => { closeDialog(); reject(undefined); }, { once: true }); dialogEl.showModal(); input.focus(); input.select(); }); } // ── Choice dialog (buttons only) ──────────────────────────────── function showChoice({ title, body, buttons, cancelText }) { return new Promise((resolve, reject) => { const btnHtml = buttons.map(b => `<button type="button" class="p3x-btn" data-value="${b.value}">${b.label}</button>` ).join(''); dialogEl.innerHTML = ` <h3 class="p3x-dialog-title">${title}</h3> <div class="p3x-dialog-body">${body || ''}</div> <div class="p3x-dialog-actions"> ${cancelText ? `<button type="button" class="p3x-btn" data-action="cancel">${cancelText}</button>` : ''} ${btnHtml} </div>`; dialogEl.querySelectorAll('[data-value]').forEach(btn => { btn.onclick = () => { closeDialog(); resolve(btn.dataset.value); }; }); const cancelBtn = dialogEl.querySelector('[data-action="cancel"]'); if (cancelBtn) cancelBtn.onclick = () => { closeDialog(); reject(undefined); }; dialogEl.addEventListener('cancel', () => { closeDialog(); reject(undefined); }, { once: true }); dialogEl.showModal(); }); } // ── Bookmark form dialog ──────────────────────────────────────── function showBookmarkForm(opts) { const lang = registry.lang; const isEdit = opts.edit === true; const title = isEdit ? lang.bookmarks.edit : lang.bookmarks.add; const model = opts.model || { title: '', url: '', category: '' }; return new Promise((resolve, reject) => { dialogEl.innerHTML = ` <form> <h3 class="p3x-dialog-title">${title}</h3> <div class="p3x-dialog-body"> <div class="p3x-dialog-field" id="p3x-field-title"> <label>${lang.bookmarks.form.title}</label> <input class="p3x-dialog-input" name="title" required value="${model.title || ''}"> <div class="p3x-field-error">${lang.validation.required}</div> </div> <div class="p3x-dialog-field" id="p3x-field-url"> <label>${lang.bookmarks.form.url}</label> <input class="p3x-dialog-input" name="url" type="url" required maxlength="2048" value="${model.url || ''}"> <div class="p3x-field-error">${lang.validation.url}</div> </div> <div class="p3x-dialog-field" id="p3x-field-category"> <label>${lang.bookmarks.form?.category || 'Folder'}</label> <input class="p3x-dialog-input" name="category" list="p3x-category-list" placeholder="${lang.bookmarks.form?.categoryPlaceholder || 'e.g. Work/Projects (use / for subfolders)'}" value="${model.category || ''}"> <datalist id="p3x-category-list"></datalist> </div> </div> <div class="p3x-dialog-actions"> <button type="button" class="p3x-btn" data-action="cancel">${lang.button.cancel}</button> ${isEdit ? `<button type="button" class="p3x-btn p3x-btn-warn" data-action="delete">${lang.button.delete}</button>` : ''} <button type="submit" class="p3x-btn">${lang.button.save}</button> </div> </form>`; const form = dialogEl.querySelector('form'); const titleInput = form.querySelector('[name="title"]'); const urlInput = form.querySelector('[name="url"]'); const categoryInput = form.querySelector('[name="category"]'); const titleField = dialogEl.querySelector('#p3x-field-title'); const urlField = dialogEl.querySelector('#p3x-field-url'); // Populate datalist with existing folder paths const datalist = dialogEl.querySelector('#p3x-category-list'); const bookmarks = registry.conf.get('bookmarks') || []; const folderSet = new Set(); for (const bm of bookmarks) { if (bm.category) { const parts = bm.category.split('/').map(p => p.trim()).filter(p => p); for (let i = 1; i <= parts.length; i++) { folderSet.add(parts.slice(0, i).join('/')); } } } const sortedFolders = [...folderSet].sort((a, b) => a.localeCompare(b)); for (const folder of sortedFolders) { const opt = document.createElement('option'); opt.value = folder; datalist.appendChild(opt); } const validate = () => { titleField.classList.toggle('p3x-invalid', !titleInput.value.trim()); urlField.classList.toggle('p3x-invalid', !urlInput.validity.valid); return titleInput.value.trim() && urlInput.validity.valid; }; const normalizeCategory = (val) => { return val.split('/').map(s => s.trim()).filter(s => s).join('/'); }; form.onsubmit = (e) => { e.preventDefault(); if (!validate()) return; closeDialog(); resolve({ opts: opts, model: { title: titleInput.value.trim(), url: urlInput.value.trim(), category: normalizeCategory(categoryInput.value) }, }); }; const deleteBtn = dialogEl.querySelector('[data-action="delete"]'); if (deleteBtn) { deleteBtn.onclick = () => { closeDialog(); resolve({ delete: true, opts: opts, model: { title: titleInput.value.trim(), url: urlInput.value.trim(), category: normalizeCategory(categoryInput.value) }, }); }; } dialogEl.querySelector('[data-action="cancel"]').onclick = () => { closeDialog(); reject(undefined); }; dialogEl.addEventListener('cancel', () => { closeDialog(); reject(undefined); }, { once: true }); dialogEl.showModal(); titleInput.focus(); }); } // ── Bookmark Manager ─────────────────────────────────────────── function showBookmarkManager() { const lang = registry.lang; const conf = registry.conf; let bookmarks = conf.get('bookmarks') || []; let filter = ''; const normalizeCategory = (val) => val.split('/').map(s => s.trim()).filter(s => s).join('/'); function getExistingFolders() { const folders = new Set(); for (const bm of bookmarks) { if (bm.category) { const parts = bm.category.split('/').map(p => p.trim()).filter(p => p); for (let i = 1; i <= parts.length; i++) { folders.add(parts.slice(0, i).join('/')); } } } return [...folders].sort((a, b) => a.localeCompare(b)); } function save() { conf.set('bookmarks', bookmarks); const { ipcRenderer } = window.electronShim; ipcRenderer.send('p3x-onenote-bookmarks-manager-saved'); } function render() { const filtered = filter ? bookmarks.filter(bm => bm.title.toLowerCase().includes(filter) || bm.url.toLowerCase().includes(filter) || (bm.category || '').toLowerCase().includes(filter)) : bookmarks; const folders = getExistingFolders(); const datalistHtml = folders.map(f => `<option value="${f}">`).join(''); const rowsHtml = filtered.length === 0 ? `<tr><td colspan="4" class="p3x-bm-empty">${lang.bookmarks?.managerEmpty || 'No bookmarks found.'}</td></tr>` : filtered.map((bm, displayIdx) => { const realIdx = bookmarks.indexOf(bm); const folderHtml = bm.category ? `<span class="p3x-bm-folder-tag">${bm.category}</span>` : `<span style="opacity:0.4">—</span>`; return `<tr data-idx="${realIdx}"> <td class="p3x-bm-cell-edit" data-field="title" title="${bm.title}">${bm.title}</td> <td class="p3x-bm-cell-edit" data-field="url" title="${bm.url}">${bm.url}</td> <td class="p3x-bm-cell-edit" data-field="category">${folderHtml}</td> <td class="p3x-bm-actions"> <button class="p3x-bm-delete" data-idx="${realIdx}" title="${lang.button.delete}"> <i class="fas fa-trash" style="pointer-events:none;"></i> </button> </td> </tr>`; }).join(''); const listEl = dialogEl.querySelector('.p3x-bm-list'); listEl.innerHTML = ` <datalist id="p3x-bm-folder-list">${datalistHtml}</datalist> <table class="p3x-bm-table"> <thead><tr> <th>${lang.bookmarks.form.title}</th> <th>${lang.bookmarks.form.url}</th> <th>${lang.bookmarks.form?.category || 'Folder'}</th> <th></th> </tr></thead> <tbody>${rowsHtml}</tbody> </table>`; // Wire up inline editing listEl.querySelectorAll('.p3x-bm-cell-edit').forEach(cell => { cell.addEventListener('click', () => { const row = cell.closest('tr'); const idx = parseInt(row.dataset.idx); const field = cell.dataset.field; const bm = bookmarks[idx]; if (!bm) return; const currentVal = bm[field] || ''; const input = document.createElement('input'); input.className = 'p3x-bm-inline-input'; input.value = currentVal; if (field === 'category') { input.setAttribute('list', 'p3x-bm-folder-list'); input.placeholder = lang.bookmarks.form?.categoryPlaceholder || 'e.g. Work/Projects'; } const commit = () => { let val = input.value.trim(); if (field === 'category') val = normalizeCategory(val); if (field === 'title' && !val) val = currentVal; // don't allow empty title bm[field] = val; save(); render(); }; input.addEventListener('blur', commit); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); commit(); } if (e.key === 'Escape') { render(); } }); cell.textContent = ''; cell.appendChild(input); input.focus(); input.select(); }); }); // Wire up delete buttons with confirmation listEl.querySelectorAll('.p3x-bm-delete').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.idx); const bm = bookmarks[idx]; if (!bm) return; // Simple inline confirmation const row = btn.closest('tr'); const actionsCell = btn.closest('.p3x-bm-actions'); actionsCell.innerHTML = ` <span style="font-size:11px;margin-right:4px;">${lang.bookmarks?.confirmDelete || 'Delete?'}</span> <button class="p3x-bm-confirm-yes" style="color:#a4262c;font-weight:bold;" title="${lang.button.yes || 'Yes'}"> <i class="fas fa-check" style="pointer-events:none;"></i> </button> <button class="p3x-bm-confirm-no" title="${lang.button.cancel || 'Cancel'}"> <i class="fas fa-times" style="pointer-events:none;"></i> </button> `; actionsCell.querySelector('.p3x-bm-confirm-yes').addEventListener('click', () => { bookmarks.splice(idx, 1); save(); render(); }); actionsCell.querySelector('.p3x-bm-confirm-no').addEventListener('click', () => { render(); }); }); }); } return new Promise((resolve) => { dialogEl.classList.add('p3x-bookmark-manager'); dialogEl.innerHTML = ` <h3 class="p3x-dialog-title">${lang.bookmarks?.manager || 'Manage Bookmarks'}</h3> <div class="p3x-bm-toolbar"> <input class="p3x-dialog-input p3x-bm-search" type="text" placeholder="${lang.bookmarks?.managerSearch || 'Search bookmarks...'}"> </div> <div class="p3x-bm-list"></div> <div class="p3x-dialog-actions"> <button type="button" class="p3x-btn" data-action="close">${lang.button.ok}</button> </div>`; const searchInput = dialogEl.querySelector('.p3x-bm-search'); searchInput.addEventListener('input', () => { filter = searchInput.value.toLowerCase(); render(); }); dialogEl.querySelector('[data-action="close"]').onclick = () => { dialogEl.classList.remove('p3x-bookmark-manager'); closeDialog(); resolve(); }; dialogEl.addEventListener('cancel', () => { dialogEl.classList.remove('p3x-bookmark-manager'); closeDialog(); resolve(); }, { once: true }); render(); dialogEl.showModal(); searchInput.focus(); }); } // ── Public prompt API (matches old Angular interface) ─────────── export const p3xPrompt = { setProxy() { const lang = registry.lang; return showPrompt({ title: lang.label.setProxy, text: lang.dialog.setProxy.info, placeholder: lang.dialog.setProxy.placeholder, initialValue: registry.data.proxy, cancelText: lang.button.cancel, okText: lang.button.save, }); }, goToUrl() { const lang = registry.lang; return showPrompt({ title: lang.label.openUrl, text: lang.dialog.openUrl.info, placeholder: lang.dialog.openUrl.placeholder, initialValue: '', cancelText: lang.button.cancel, okText: lang.button.go, }); }, configureLanguge(opts) { const lang = registry.lang; return showChoice({ title: lang.menu.language.dialog.label, buttons: [ { label: lang.menu.language.dialog.personal, value: 'personal' }, { label: lang.menu.language.dialog.corporate, value: 'corporate' }, ], cancelText: lang.button.cancel, }); }, redirect(opts) { const lang = registry.lang; return showChoice({ title: lang.label.promptRedirectUrlTitle, body: `<div>${lang.dialog.redirect.url({ url: opts.url })}</div>`, buttons: [ { label: lang.dialog.redirect.urlInternal, value: 'internal' }, { label: lang.dialog.redirect.urlExternal, value: 'external' }, ], cancelText: lang.button.cancel, }); }, bookmarks(opts) { return showBookmarkForm(opts); }, bookmarkManager() { return showBookmarkManager(); }, addTab() { const lang = registry.lang; return showChoice({ title: lang.tabs?.addTab || 'Add tab', buttons: [ { label: lang.menu?.language?.dialog?.personal || lang.tabs?.personal || 'Personal', value: 'personal' }, { label: lang.menu?.language?.dialog?.corporate || lang.tabs?.corporate || 'Corporate', value: 'corporate' }, ], cancelText: lang.button.cancel, }); }, renameTab(currentName) { const lang = registry.lang; return showPrompt({ title: lang.tabs?.renameTab || 'Rename tab', text: lang.tabs?.renamePrompt || 'Enter a custom name for this tab (leave empty to use default)', placeholder: lang.tabs?.renamePlaceholder || 'Custom tab name', initialValue: currentName || '', cancelText: lang.button.cancel, okText: lang.button.save, }); }, confirmCloseTab(message) { const lang = registry.lang; return showChoice({ title: lang.tabs?.closeTab || 'Close tab', body: `<p>${message}</p>`, buttons: [ { label: lang.button.yes || 'Yes', value: 'yes' }, ], cancelText: lang.button.cancel, }); }, };