max / goingson
44 files changed,
+1077 insertions,
-954 deletions
| @@ -326,7 +326,7 @@ async function openAboutModal() { | |||
| 326 | 326 | <p class="about-copyright">© 2026 Make Creative, LLC</p> | |
| 327 | 327 | </div> | |
| 328 | 328 | <div class="form-actions"> | |
| 329 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button> | |
| 329 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Close</button> | |
| 330 | 330 | </div> | |
| 331 | 331 | `; | |
| 332 | 332 | GoingsOn.ui.openModal('About', content); | |
| @@ -353,9 +353,9 @@ function showWelcome() { | |||
| 353 | 353 | <div class="welcome-section"> | |
| 354 | 354 | <h3 class="welcome-subhead">Get Started</h3> | |
| 355 | 355 | <div class="welcome-step-stack"> | |
| 356 | - | <button class="btn btn-secondary text-left" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal(); GoingsOn.keyboard.openQuickAdd();">${step1}</button> | |
| 357 | - | <button class="btn btn-secondary text-left" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal(); GoingsOn.navigation.switchView('day-plan');">${step2}</button> | |
| 358 | - | <button class="btn btn-secondary text-left" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal(); GoingsOn.emails.openAccountsModal();"><strong>3.</strong> Add an email account</button> | |
| 356 | + | <button class="btn btn-secondary text-left" data-act="ui.markSeenThen" data-a1="go-welcomed" data-a2="keyboard.openQuickAdd">${step1}</button> | |
| 357 | + | <button class="btn btn-secondary text-left" data-act="ui.markSeenThen" data-a1="go-welcomed" data-a2="navigation.switchView" data-a3="day-plan">${step2}</button> | |
| 358 | + | <button class="btn btn-secondary text-left" data-act="ui.markSeenThen" data-a1="go-welcomed" data-a2="emails.openAccountsModal"><strong>3.</strong> Add an email account</button> | |
| 359 | 359 | </div> | |
| 360 | 360 | </div> | |
| 361 | 361 | <div class="welcome-section"> | |
| @@ -369,7 +369,7 @@ function showWelcome() { | |||
| 369 | 369 | ${shortcutsHint} | |
| 370 | 370 | </div> | |
| 371 | 371 | <div class="form-actions"> | |
| 372 | - | <button class="btn btn-primary" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal()">Get Started</button> | |
| 372 | + | <button class="btn btn-primary" data-act="ui.markSeenThen" data-a1="go-welcomed">Get Started</button> | |
| 373 | 373 | </div> | |
| 374 | 374 | `; | |
| 375 | 375 | GoingsOn.ui.openModal('Welcome to GoingsOn', content); |
| @@ -67,13 +67,13 @@ | |||
| 67 | 67 | </div> | |
| 68 | 68 | <div class="attachment-actions"> | |
| 69 | 69 | ${a.hasLocalBlob ? ` | |
| 70 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.open('${escArg(a.id)}')" title="Open">Open</button> | |
| 71 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.attachments.saveAs('${escArg(a.id)}', '${escArg(a.filename)}')" title="Save As">Save</button> | |
| 70 | + | <button class="btn btn-sm btn-secondary" data-act="attachments.open" data-a1="${escAttr(a.id)}" title="Open">Open</button> | |
| 71 | + | <button class="btn btn-sm btn-secondary" data-act="attachments.saveAs" data-a1="${escAttr(a.id)}" data-a2="${escAttr(a.filename)}" title="Save As">Save</button> | |
| 72 | 72 | ` : ` | |
| 73 | 73 | <span class="attachment-sync-warning">Sync needed</span> | |
| 74 | 74 | `} | |
| 75 | 75 | <button class="btn btn-sm btn-secondary text-accent-red" | |
| 76 | - | onclick="GoingsOn.attachments.remove('${escArg(a.id)}', '${tid}', '${pid}')" title="Delete">×</button> | |
| 76 | + | data-act="attachments.remove" data-a1="${escAttr(a.id)}" data-a2="${escAttr(tid)}" data-a3="${escAttr(pid)}" title="Delete">×</button> | |
| 77 | 77 | </div> | |
| 78 | 78 | </div> | |
| 79 | 79 | `).join(''); | |
| @@ -83,7 +83,7 @@ | |||
| 83 | 83 | <div> | |
| 84 | 84 | ${listHtml} | |
| 85 | 85 | <div class="attachment-attach-row"> | |
| 86 | - | <button class="btn btn-primary" onclick="GoingsOn.attachments.pickAndAttach('${tid}', '${pid}')">Attach File</button> | |
| 86 | + | <button class="btn btn-primary" data-act="attachments.pickAndAttach" data-a1="${escAttr(tid)}" data-a2="${escAttr(pid)}">Attach File</button> | |
| 87 | 87 | </div> | |
| 88 | 88 | </div> | |
| 89 | 89 | `; |
| @@ -56,7 +56,7 @@ | |||
| 56 | 56 | ||
| 57 | 57 | for (const opt of options) { | |
| 58 | 58 | optionsHtml += ` | |
| 59 | - | <button class="snooze-option" onclick="GoingsOn.bulk._snoozeCallback('${escArg(opt.time)}')"> | |
| 59 | + | <button class="snooze-option" data-act="bulk._snoozeCallback" data-a1="${escAttr(opt.time)}"> | |
| 60 | 60 | <span class="snooze-option-label">${esc(opt.label)}</span> | |
| 61 | 61 | <span class="snooze-option-time">${esc(opt.formatted)}</span> | |
| 62 | 62 | </button> | |
| @@ -198,9 +198,9 @@ | |||
| 198 | 198 | ||
| 199 | 199 | const projects = GoingsOn.projects?.getCache?.() || []; | |
| 200 | 200 | let optionsHtml = `<p class="bulk-modal-prompt bulk-modal-prompt--wide">Set project for ${selectedTaskIds.size} tasks:</p>`; | |
| 201 | - | optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" onclick="GoingsOn.bulk._applyProject(null)">No Project</button>`; | |
| 201 | + | optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" data-act="bulk._applyProject" data-args='[null]'>No Project</button>`; | |
| 202 | 202 | for (const p of projects) { | |
| 203 | - | optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" onclick="GoingsOn.bulk._applyProject('${escArg(p.id)}')">${GoingsOn.utils.escapeHtml(p.name)}</button>`; | |
| 203 | + | optionsHtml += `<button class="btn btn-sm text-left w-full bulk-modal-option-btn" data-act="bulk._applyProject" data-a1="${escAttr(p.id)}">${GoingsOn.utils.escapeHtml(p.name)}</button>`; | |
| 204 | 204 | } | |
| 205 | 205 | GoingsOn.ui.openModal('Set Project', `<div class="bulk-modal-scroll">${optionsHtml}</div>`); | |
| 206 | 206 | } | |
| @@ -212,9 +212,9 @@ | |||
| 212 | 212 | const content = ` | |
| 213 | 213 | <p class="bulk-modal-prompt bulk-modal-prompt--wide">Set priority for ${selectedTaskIds.size} tasks:</p> | |
| 214 | 214 | <div class="bulk-priority-row"> | |
| 215 | - | <button class="btn btn-sm" onclick="GoingsOn.bulk._applyPriority('High')">High</button> | |
| 216 | - | <button class="btn btn-sm" onclick="GoingsOn.bulk._applyPriority('Medium')">Medium</button> | |
| 217 | - | <button class="btn btn-sm" onclick="GoingsOn.bulk._applyPriority('Low')">Low</button> | |
| 215 | + | <button class="btn btn-sm" data-act="bulk._applyPriority" data-a1="High">High</button> | |
| 216 | + | <button class="btn btn-sm" data-act="bulk._applyPriority" data-a1="Medium">Medium</button> | |
| 217 | + | <button class="btn btn-sm" data-act="bulk._applyPriority" data-a1="Low">Low</button> | |
| 218 | 218 | </div> | |
| 219 | 219 | `; | |
| 220 | 220 | GoingsOn.ui.openModal('Set Priority', content); |
| @@ -5,6 +5,7 @@ | |||
| 5 | 5 | ||
| 6 | 6 | (function() { | |
| 7 | 7 | 'use strict'; | |
| 8 | + | const escAttr = GoingsOn.utils.escapeAttrValue; | |
| 8 | 9 | const esc = GoingsOn.utils.escapeHtml; | |
| 9 | 10 | ||
| 10 | 11 | // ============ Modal ============ | |
| @@ -180,7 +181,7 @@ function showUndoToast(message, { onConfirm, onUndo, timeout = 15000 }) { | |||
| 180 | 181 | ||
| 181 | 182 | toast.innerHTML = ` | |
| 182 | 183 | <span class="undo-message">${esc(message)}</span> | |
| 183 | - | <button class="btn btn-sm btn-primary" onclick="GoingsOn.ui.executeUndo('${undoId}')">Undo</button> | |
| 184 | + | <button class="btn btn-sm btn-primary" data-act="ui.executeUndo" data-a1="${escAttr(undoId)}">Undo</button> | |
| 184 | 185 | <span class="undo-countdown"></span> | |
| 185 | 186 | `; | |
| 186 | 187 |
| @@ -11,6 +11,75 @@ | |||
| 11 | 11 | ||
| 12 | 12 | const openModal = (...args) => GoingsOn.modal.openModal(...args); | |
| 13 | 13 | const closeModal = (...args) => GoingsOn.modal.closeModal(...args); | |
| 14 | + | ||
| 15 | + | /** Resolve a `GoingsOn` dot-path (e.g. "emails.open") to its function. */ | |
| 16 | + | function resolvePath(path) { | |
| 17 | + | return String(path).split('.').reduce((o, k) => (o == null ? o : o[k]), window.GoingsOn); | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | /** A do-nothing action. Put `data-act="ui.noop"` on a wrapper so delegation's | |
| 21 | + | * `closest()` stops there instead of bubbling to an ancestor's action | |
| 22 | + | * (replaces the old inline `event.stopPropagation()` guards). */ | |
| 23 | + | function noop() {} | |
| 24 | + | ||
| 25 | + | /** | |
| 26 | + | * Close the active modal, then resolve a `GoingsOn` dot-path and call it with | |
| 27 | + | * the given args. Replaces inline `onclick="ui.closeModal(); foo.bar('x')"` | |
| 28 | + | * two-statement handlers with a single delegated action. | |
| 29 | + | */ | |
| 30 | + | function closeModalThen(path, ...args) { | |
| 31 | + | closeModal(); | |
| 32 | + | const fn = resolvePath(path); | |
| 33 | + | if (typeof fn === 'function') fn(...args); | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | /** Toggle the `show` class on the element's next sibling (dropdown menus). */ | |
| 37 | + | function toggleMenu(el) { | |
| 38 | + | if (el && el.nextElementSibling) el.nextElementSibling.classList.toggle('show'); | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | /** Run a `GoingsOn` action, then close the dropdown the trigger sits in | |
| 42 | + | * (replaces `foo(); this.parentElement.classList.remove('show')`). */ | |
| 43 | + | function menuAction(el, path, ...args) { | |
| 44 | + | const fn = resolvePath(path); | |
| 45 | + | if (typeof fn === 'function') fn(...args); | |
| 46 | + | if (el && el.parentElement) el.parentElement.classList.remove('show'); | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | /** Toggle an "expanded" section: flips `expanded` on the button and `hidden` | |
| 50 | + | * on its next sibling; optionally swaps label text from data attributes. */ | |
| 51 | + | function toggleExpand(el) { | |
| 52 | + | if (!el) return; | |
| 53 | + | el.classList.toggle('expanded'); | |
| 54 | + | if (el.nextElementSibling) el.nextElementSibling.classList.toggle('hidden'); | |
| 55 | + | const on = el.dataset.textExpanded, off = el.dataset.textCollapsed; | |
| 56 | + | if (on && off) el.textContent = el.classList.contains('expanded') ? on : off; | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | /** Set a localStorage key (replaces inline `localStorage.setItem(...)`). */ | |
| 60 | + | function setLocalStorage(key, value) { | |
| 61 | + | try { localStorage.setItem(key, value); } catch (e) { /* storage disabled */ } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | /** Run a `GoingsOn` action only when Enter was pressed (for keydown handlers). */ | |
| 65 | + | function onEnter(event, path, ...args) { | |
| 66 | + | if (event && event.key === 'Enter') { | |
| 67 | + | const fn = resolvePath(path); | |
| 68 | + | if (typeof fn === 'function') fn(...args); | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | /** Set a localStorage flag to "1", close the modal, then optionally run an | |
| 73 | + | * action. Replaces the welcome-modal `localStorage.setItem(...); closeModal(); | |
| 74 | + | * foo()` three-statement handlers. */ | |
| 75 | + | function markSeenThen(key, path, ...args) { | |
| 76 | + | setLocalStorage(key, '1'); | |
| 77 | + | closeModal(); | |
| 78 | + | if (path) { | |
| 79 | + | const fn = resolvePath(path); | |
| 80 | + | if (typeof fn === 'function') fn(...args); | |
| 81 | + | } | |
| 82 | + | } | |
| 14 | 83 | const showToast = (...args) => GoingsOn.modal.showToast(...args); | |
| 15 | 84 | const showUndoToast = (...args) => GoingsOn.modal.showUndoToast(...args); | |
| 16 | 85 | const bulkActionWithUndo = (...args) => GoingsOn.modal.bulkActionWithUndo(...args); | |
| @@ -59,14 +128,14 @@ function emptyStateIcon(key) { | |||
| 59 | 128 | * so empty/onboarding states read as one designed pattern. | |
| 60 | 129 | * @param {string} message - The empty state message text | |
| 61 | 130 | * @param {string} [buttonLabel] - Optional button label | |
| 62 | - | * @param {string} [onClickFn] - Optional onclick handler (as a string, e.g., "GoingsOn.tasks.openNew()") | |
| 131 | + | * @param {string} [onClickAct] - Optional delegated action dot-path (e.g. "tasks.openNew") | |
| 63 | 132 | * @param {string} [iconKey] - Optional EMPTY_STATE_ICONS key for a leading icon | |
| 64 | 133 | * @returns {string} - HTML string for the empty state | |
| 65 | 134 | */ | |
| 66 | - | function renderEmptyState(message, buttonLabel, onClickFn, iconKey) { | |
| 135 | + | function renderEmptyState(message, buttonLabel, onClickAct, iconKey) { | |
| 67 | 136 | let html = `<div class="empty-state">${emptyStateIcon(iconKey)}<p class="empty-state-text">${GoingsOn.utils.escapeHtml(message)}</p>`; | |
| 68 | - | if (buttonLabel && onClickFn) { | |
| 69 | - | html += `<button class="btn btn-primary empty-state-action" onclick="${GoingsOn.utils.escapeAttrValue(onClickFn)}">${GoingsOn.utils.escapeHtml(buttonLabel)}</button>`; | |
| 137 | + | if (buttonLabel && onClickAct) { | |
| 138 | + | html += `<button class="btn btn-primary empty-state-action" data-act="${GoingsOn.utils.escapeAttrValue(onClickAct)}">${GoingsOn.utils.escapeHtml(buttonLabel)}</button>`; | |
| 70 | 139 | } | |
| 71 | 140 | html += `</div>`; | |
| 72 | 141 | return html; | |
| @@ -507,6 +576,16 @@ GoingsOn.ui = { | |||
| 507 | 576 | // Modal (delegated to components-modal.js) | |
| 508 | 577 | openModal, | |
| 509 | 578 | closeModal, | |
| 579 | + | closeModalThen, | |
| 580 | + | ||
| 581 | + | // Delegated-event helpers (replace inline on* handlers; see dispatch.js) | |
| 582 | + | noop, | |
| 583 | + | toggleMenu, | |
| 584 | + | menuAction, | |
| 585 | + | toggleExpand, | |
| 586 | + | setLocalStorage, | |
| 587 | + | onEnter, | |
| 588 | + | markSeenThen, | |
| 510 | 589 | ||
| 511 | 590 | // Toast notifications | |
| 512 | 591 | showToast, |
| @@ -59,13 +59,13 @@ | |||
| 59 | 59 | ? `<span class="tag status-${(item.status || '').toLowerCase()}">${esc(item.status)}</span>` | |
| 60 | 60 | : ''; | |
| 61 | 61 | ||
| 62 | - | let onclick = ''; | |
| 63 | - | if (item.kind === 'task') onclick = `GoingsOn.taskOverview.open('${escArg(item.id)}')`; | |
| 64 | - | else if (item.kind === 'event') onclick = `GoingsOn.events.open('${escArg(item.id)}')`; | |
| 65 | - | else if (item.kind === 'email') onclick = `GoingsOn.emails.open('${escArg(item.id)}')`; | |
| 62 | + | let act = ''; | |
| 63 | + | if (item.kind === 'task') act = 'taskOverview.open'; | |
| 64 | + | else if (item.kind === 'event') act = 'events.open'; | |
| 65 | + | else if (item.kind === 'email') act = 'emails.open'; | |
| 66 | 66 | ||
| 67 | 67 | return ` | |
| 68 | - | <div class="contact-timeline-item row-flex row-flex-3" onclick="${onclick}" role="button" tabindex="0"> | |
| 68 | + | <div class="contact-timeline-item row-flex row-flex-3" ${act ? `data-act="${act}" data-a1="${escAttr(item.id)}"` : ''} role="button" tabindex="0"> | |
| 69 | 69 | <span class="contact-timeline-icon">${icon}</span> | |
| 70 | 70 | <span class="contact-timeline-title">${esc(item.title)}</span> | |
| 71 | 71 | ${badge} | |
| @@ -81,12 +81,12 @@ | |||
| 81 | 81 | // Actions bar | |
| 82 | 82 | if (contact.isImplicit) { | |
| 83 | 83 | actionsEl.innerHTML = ` | |
| 84 | - | <button class="btn btn-primary" onclick="GoingsOn.contactDashboard.promote('${escArg(contact.id)}')">Save as Contact</button> | |
| 84 | + | <button class="btn btn-primary" data-act="contactDashboard.promote" data-a1="${escAttr(contact.id)}">Save as Contact</button> | |
| 85 | 85 | `; | |
| 86 | 86 | } else { | |
| 87 | 87 | actionsEl.innerHTML = ` | |
| 88 | - | <button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escArg(contact.id)}')">Edit</button> | |
| 89 | - | <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.contacts.delete('${escArg(contact.id)}')">Delete</button> | |
| 88 | + | <button class="btn btn-secondary" data-act="contacts.openEdit" data-a1="${escAttr(contact.id)}">Edit</button> | |
| 89 | + | <button class="btn btn-secondary text-accent-red" data-act="contacts.delete" data-a1="${escAttr(contact.id)}">Delete</button> | |
| 90 | 90 | `; | |
| 91 | 91 | } | |
| 92 | 92 | ||
| @@ -137,7 +137,7 @@ | |||
| 137 | 137 | const items = timelineItems.map(renderTimelineItem).join(''); | |
| 138 | 138 | // Rust caps the initial feed; offer to load the rest without a second round of client math. | |
| 139 | 139 | const showAll = totalActivity > timelineItems.length | |
| 140 | - | ? `<button class="btn btn-link" onclick="GoingsOn.contactDashboard.showAllActivity()">Show all ${totalActivity} interactions</button>` | |
| 140 | + | ? `<button class="btn btn-link" data-act="contactDashboard.showAllActivity">Show all ${totalActivity} interactions</button>` | |
| 141 | 141 | : ''; | |
| 142 | 142 | timelineHtml = `<div class="contact-timeline" id="contact-timeline">${items}</div>${showAll}`; | |
| 143 | 143 | } |
| @@ -40,11 +40,11 @@ | |||
| 40 | 40 | ).join(''); | |
| 41 | 41 | ||
| 42 | 42 | return ` | |
| 43 | - | <div class="card contact-card" onclick="GoingsOn.contacts.open('${escArg(c.id)}')" | |
| 43 | + | <div class="card contact-card" data-act="contacts.open" data-a1="${escAttr(c.id)}" | |
| 44 | 44 | tabindex="0" role="button" aria-label="Open contact ${esc(c.displayName)}"> | |
| 45 | 45 | <div class="card-header contact-card-header"> | |
| 46 | 46 | <input type="checkbox" class="bulk-checkbox contact-select-cb" data-id="${escAttr(c.id)}" | |
| 47 | - | onclick="event.stopPropagation(); GoingsOn.contacts.toggleSelection('${escArg(c.id)}', event)" | |
| 47 | + | data-act="contacts.toggleSelection" data-a1="${escAttr(c.id)}" data-a2="@event" | |
| 48 | 48 | aria-label="Select ${esc(c.displayName)}"> | |
| 49 | 49 | <div class="avatar">${esc(initials)}</div> | |
| 50 | 50 | <div class="contact-card-body"> | |
| @@ -52,7 +52,7 @@ | |||
| 52 | 52 | ${nickname} | |
| 53 | 53 | ${company} | |
| 54 | 54 | </div> | |
| 55 | - | <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); GoingsOn.contacts.openEdit('${escArg(c.id)}')" title="Edit">...</button> | |
| 55 | + | <button class="btn btn-sm btn-secondary" data-act="contacts.openEdit" data-a1="${escAttr(c.id)}" title="Edit">...</button> | |
| 56 | 56 | </div> | |
| 57 | 57 | ${emailLine ? `<p class="card-description">${emailLine}</p>` : ''} | |
| 58 | 58 | ${tagPills ? `<div class="card-meta">${tagPills}</div>` : ''} | |
| @@ -86,7 +86,7 @@ | |||
| 86 | 86 | const emailRows = (contact.emails || []).map(e => ` | |
| 87 | 87 | <div class="sub-item"> | |
| 88 | 88 | <span>${esc(e.address)} ${e.label ? `<small>(${esc(e.label)})</small>` : ''} ${e.isPrimary ? '<strong>Primary</strong>' : ''}</span> | |
| 89 | - | <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeEmail('${escArg(contact.id)}', '${escArg(e.id)}')" title="Remove">x</button> | |
| 89 | + | <button class="btn btn-sm btn-danger" data-act="contacts.removeEmail" data-a1="${escAttr(contact.id)}" data-a2="${escAttr(e.id)}" title="Remove">x</button> | |
| 90 | 90 | </div> | |
| 91 | 91 | `).join(''); | |
| 92 | 92 | ||
| @@ -94,7 +94,7 @@ | |||
| 94 | 94 | const phoneRows = (contact.phones || []).map(p => ` | |
| 95 | 95 | <div class="sub-item"> | |
| 96 | 96 | <span>${esc(p.number)} ${p.label ? `<small>(${esc(p.label)})</small>` : ''} ${p.isPrimary ? '<strong>Primary</strong>' : ''}</span> | |
| 97 | - | <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removePhone('${escArg(contact.id)}', '${escArg(p.id)}')" title="Remove">x</button> | |
| 97 | + | <button class="btn btn-sm btn-danger" data-act="contacts.removePhone" data-a1="${escAttr(contact.id)}" data-a2="${escAttr(p.id)}" title="Remove">x</button> | |
| 98 | 98 | </div> | |
| 99 | 99 | `).join(''); | |
| 100 | 100 | ||
| @@ -102,7 +102,7 @@ | |||
| 102 | 102 | const socialRows = (contact.socialHandles || []).map(s => ` | |
| 103 | 103 | <div class="sub-item"> | |
| 104 | 104 | <span><strong>${esc(s.platform)}:</strong> ${s.url ? `<a href="${escAttrVal(safeUrl(s.url))}" target="_blank">${esc(s.handle)}</a>` : esc(s.handle)}</span> | |
| 105 | - | <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeSocialHandle('${escArg(contact.id)}', '${escArg(s.id)}')" title="Remove">x</button> | |
| 105 | + | <button class="btn btn-sm btn-danger" data-act="contacts.removeSocialHandle" data-a1="${escAttr(contact.id)}" data-a2="${escAttr(s.id)}" title="Remove">x</button> | |
| 106 | 106 | </div> | |
| 107 | 107 | `).join(''); | |
| 108 | 108 | ||
| @@ -110,7 +110,7 @@ | |||
| 110 | 110 | const customFieldRows = (contact.customFields || []).map(f => ` | |
| 111 | 111 | <div class="sub-item"> | |
| 112 | 112 | <span><strong>${esc(f.label)}:</strong> ${f.url ? `<a href="${escAttrVal(safeUrl(f.url))}" target="_blank">${esc(f.value)}</a>` : esc(f.value)}</span> | |
| 113 | - | <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeCustomField('${escArg(contact.id)}', '${escArg(f.id)}')" title="Remove">x</button> | |
| 113 | + | <button class="btn btn-sm btn-danger" data-act="contacts.removeCustomField" data-a1="${escAttr(contact.id)}" data-a2="${escAttr(f.id)}" title="Remove">x</button> | |
| 114 | 114 | </div> | |
| 115 | 115 | `).join(''); | |
| 116 | 116 | ||
| @@ -131,7 +131,7 @@ | |||
| 131 | 131 | <div class="sub-collection"> | |
| 132 | 132 | <div class="sub-collection-header"> | |
| 133 | 133 | <h4>Email Addresses</h4> | |
| 134 | - | <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escArg(contact.id)}')">+ Add</button> | |
| 134 | + | <button class="btn btn-sm btn-primary" data-act="contacts.openAddEmail" data-a1="${escAttr(contact.id)}">+ Add</button> | |
| 135 | 135 | </div> | |
| 136 | 136 | ${emailRows || '<div class="sub-empty">No email addresses</div>'} | |
| 137 | 137 | </div> | |
| @@ -139,7 +139,7 @@ | |||
| 139 | 139 | <div class="sub-collection"> | |
| 140 | 140 | <div class="sub-collection-header"> | |
| 141 | 141 | <h4>Phone Numbers</h4> | |
| 142 | - | <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escArg(contact.id)}')">+ Add</button> | |
| 142 | + | <button class="btn btn-sm btn-primary" data-act="contacts.openAddPhone" data-a1="${escAttr(contact.id)}">+ Add</button> | |
| 143 | 143 | </div> | |
| 144 | 144 | ${phoneRows || '<div class="sub-empty">No phone numbers</div>'} | |
| 145 | 145 | </div> | |
| @@ -147,7 +147,7 @@ | |||
| 147 | 147 | <div class="sub-collection"> | |
| 148 | 148 | <div class="sub-collection-header"> | |
| 149 | 149 | <h4>Social Handles</h4> | |
| 150 | - | <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escArg(contact.id)}')">+ Add</button> | |
| 150 | + | <button class="btn btn-sm btn-primary" data-act="contacts.openAddSocial" data-a1="${escAttr(contact.id)}">+ Add</button> | |
| 151 | 151 | </div> | |
| 152 | 152 | ${socialRows || '<div class="sub-empty">No social handles</div>'} | |
| 153 | 153 | </div> | |
| @@ -155,13 +155,13 @@ | |||
| 155 | 155 | <div class="sub-collection"> | |
| 156 | 156 | <div class="sub-collection-header"> | |
| 157 | 157 | <h4>Custom Fields</h4> | |
| 158 | - | <button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escArg(contact.id)}')">+ Add</button> | |
| 158 | + | <button class="btn btn-sm btn-primary" data-act="contacts.openAddCustomField" data-a1="${escAttr(contact.id)}">+ Add</button> | |
| 159 | 159 | </div> | |
| 160 | 160 | ${customFieldRows || '<div class="sub-empty">No custom fields</div>'} | |
| 161 | 161 | </div> | |
| 162 | 162 | ||
| 163 | 163 | <div class="form-actions form-actions--spaced"> | |
| 164 | - | <button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escArg(contact.id)}')">Edit Contact</button> | |
| 164 | + | <button class="btn btn-secondary" data-act="contacts.openEdit" data-a1="${escAttr(contact.id)}">Edit Contact</button> | |
| 165 | 165 | </div> | |
| 166 | 166 | </div> | |
| 167 | 167 | `; |
| @@ -292,16 +292,13 @@ | |||
| 292 | 292 | const isEdit = !!editingId; | |
| 293 | 293 | const submitFn = isEdit ? EDIT_SUBMIT_FN[type] : ADD_SUBMIT_FN[type]; | |
| 294 | 294 | const submitText = isEdit ? (config.editButtonText || 'Save') : config.submitButtonText; | |
| 295 | - | const onclick = isEdit | |
| 296 | - | ? `GoingsOn.contacts.${submitFn}('${escArg(contactId)}', '${escArg(editingId)}')` | |
| 297 | - | : `GoingsOn.contacts.${submitFn}('${escArg(contactId)}')`; | |
| 298 | 295 | ||
| 299 | 296 | return ` | |
| 300 | 297 | <form id="${escAttr(config.formId)}"> | |
| 301 | 298 | ${fieldHtml} | |
| 302 | 299 | <div class="form-actions"> | |
| 303 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Cancel</button> | |
| 304 | - | <button type="button" class="btn btn-primary" onclick="${onclick}">${esc(submitText)}</button> | |
| 300 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Cancel</button> | |
| 301 | + | <button type="button" class="btn btn-primary" data-act="contacts.${submitFn}" data-a1="${escAttr(contactId)}"${isEdit ? ` data-a2="${escAttr(editingId)}"` : ''}>${esc(submitText)}</button> | |
| 305 | 302 | </div> | |
| 306 | 303 | </form> | |
| 307 | 304 | `; | |
| @@ -578,7 +575,7 @@ | |||
| 578 | 575 | ||
| 579 | 576 | if (filtered.length === 0) { | |
| 580 | 577 | if (contacts.length === 0) { | |
| 581 | - | grid.innerHTML = GoingsOn.ui.renderEmptyState('No contacts yet.', 'Add Contact', 'GoingsOn.contacts.openNew()', 'contacts'); | |
| 578 | + | grid.innerHTML = GoingsOn.ui.renderEmptyState('No contacts yet.', 'Add Contact', 'contacts.openNew', 'contacts'); | |
| 582 | 579 | } else { | |
| 583 | 580 | grid.innerHTML = GoingsOn.ui.renderEmptyState('No contacts match your filters.'); | |
| 584 | 581 | } | |
| @@ -629,8 +626,8 @@ | |||
| 629 | 626 | // `GoingsOn.state.contacts` rather than smuggled through HTML attrs. | |
| 630 | 627 | const rowActions = (kind, rowId) => ` | |
| 631 | 628 | <span class="sub-item-actions"> | |
| 632 | - | <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.contacts.openEdit${kind}('${escArg(id)}', '${escArg(rowId)}')" title="Edit">Edit</button> | |
| 633 | - | <button type="button" class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.remove${kind === 'Social' ? 'SocialHandle' : kind}('${escArg(id)}', '${escArg(rowId)}')" title="Remove">x</button> | |
| 629 | + | <button type="button" class="btn btn-sm btn-secondary" data-act="contacts.openEdit${kind}" data-a1="${escAttr(id)}" data-a2="${escAttr(rowId)}" title="Edit">Edit</button> | |
| 630 | + | <button type="button" class="btn btn-sm btn-danger" data-act="contacts.remove${kind === 'Social' ? 'SocialHandle' : kind}" data-a1="${escAttr(id)}" data-a2="${escAttr(rowId)}" title="Remove">x</button> | |
| 634 | 631 | </span> | |
| 635 | 632 | `; | |
| 636 | 633 | ||
| @@ -662,37 +659,37 @@ | |||
| 662 | 659 | <div class="edit-sub-section"> | |
| 663 | 660 | <div class="edit-sub-header"> | |
| 664 | 661 | <strong>Emails</strong> | |
| 665 | - | <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escArg(id)}')">+ Add</button> | |
| 662 | + | <button type="button" class="btn btn-sm btn-primary" data-act="contacts.openAddEmail" data-a1="${escAttr(id)}">+ Add</button> | |
| 666 | 663 | </div> | |
| 667 | 664 | ${emailSummary} | |
| 668 | 665 | </div> | |
| 669 | 666 | <div class="edit-sub-section"> | |
| 670 | 667 | <div class="edit-sub-header"> | |
| 671 | 668 | <strong>Phones</strong> | |
| 672 | - | <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escArg(id)}')">+ Add</button> | |
| 669 | + | <button type="button" class="btn btn-sm btn-primary" data-act="contacts.openAddPhone" data-a1="${escAttr(id)}">+ Add</button> | |
| 673 | 670 | </div> | |
| 674 | 671 | ${phoneSummary} | |
| 675 | 672 | </div> | |
| 676 | 673 | <div class="edit-sub-section"> | |
| 677 | 674 | <div class="edit-sub-header"> | |
| 678 | 675 | <strong>Social</strong> | |
| 679 | - | <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escArg(id)}')">+ Add</button> | |
| 676 | + | <button type="button" class="btn btn-sm btn-primary" data-act="contacts.openAddSocial" data-a1="${escAttr(id)}">+ Add</button> | |
| 680 | 677 | </div> | |
| 681 | 678 | ${socialSummary} | |
| 682 | 679 | </div> | |
| 683 | 680 | <div class="edit-sub-section"> | |
| 684 | 681 | <div class="edit-sub-header"> | |
| 685 | 682 | <strong>Custom Fields</strong> | |
| 686 | - | <button type="button" class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escArg(id)}')">+ Add</button> | |
| 683 | + | <button type="button" class="btn btn-sm btn-primary" data-act="contacts.openAddCustomField" data-a1="${escAttr(id)}">+ Add</button> | |
| 687 | 684 | </div> | |
| 688 | 685 | ${customFieldSummary} | |
| 689 | 686 | </div> | |
| 690 | 687 | <div style="margin-top: 0.5rem;"> | |
| 691 | - | <button type="button" class="btn btn-sm btn-secondary" onclick="GoingsOn.contacts.open('${escArg(id)}')">View Full Detail</button> | |
| 688 | + | <button type="button" class="btn btn-sm btn-secondary" data-act="contacts.open" data-a1="${escAttr(id)}">View Full Detail</button> | |
| 692 | 689 | </div> | |
| 693 | 690 | </div> | |
| 694 | 691 | <div style="margin-top: 1rem;"> | |
| 695 | - | <button type="button" class="btn btn-danger" onclick="GoingsOn.contacts.deleteContact('${escArg(id)}')">Delete Contact</button> | |
| 692 | + | <button type="button" class="btn btn-danger" data-act="contacts.deleteContact" data-a1="${escAttr(id)}">Delete Contact</button> | |
| 696 | 693 | </div> | |
| 697 | 694 | `, | |
| 698 | 695 | }); |
| @@ -119,7 +119,7 @@ | |||
| 119 | 119 | <form id="painted-event-form"> | |
| 120 | 120 | <div class="form-group"> | |
| 121 | 121 | <label class="form-label">What to create</label> | |
| 122 | - | <select class="form-select" name="item_mode" onchange="GoingsOn.dayPlan.togglePaintMode(this)"> | |
| 122 | + | <select class="form-select" name="item_mode" data-change="dayPlan.togglePaintMode" data-a1="@el"> | |
| 123 | 123 | <option value="event">Event</option> | |
| 124 | 124 | <option value="block">Time Block</option> | |
| 125 | 125 | <option value="task">Link to Task</option> | |
| @@ -164,12 +164,12 @@ | |||
| 164 | 164 | <div class="form-group"> | |
| 165 | 165 | <label class="form-label">Start</label> | |
| 166 | 166 | <input type="datetime-local" class="form-input" name="start_time" value="${startISO}" | |
| 167 | - | onchange="GoingsOn.dayPlan.updatePaintTimePreview()"> | |
| 167 | + | data-change="dayPlan.updatePaintTimePreview"> | |
| 168 | 168 | </div> | |
| 169 | 169 | <div class="form-group"> | |
| 170 | 170 | <label class="form-label">End</label> | |
| 171 | 171 | <input type="datetime-local" class="form-input" name="end_time" value="${endISO}" | |
| 172 | - | onchange="GoingsOn.dayPlan.updatePaintTimePreview()"> | |
| 172 | + | data-change="dayPlan.updatePaintTimePreview"> | |
| 173 | 173 | </div> | |
| 174 | 174 | </div> | |
| 175 | 175 | ||
| @@ -179,8 +179,8 @@ | |||
| 179 | 179 | </div> | |
| 180 | 180 | ||
| 181 | 181 | <div class="form-actions"> | |
| 182 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Cancel</button> | |
| 183 | - | <button type="button" class="btn btn-primary" onclick="GoingsOn.dayPlan.submitPaintedEvent()">Create</button> | |
| 182 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Cancel</button> | |
| 183 | + | <button type="button" class="btn btn-primary" data-act="dayPlan.submitPaintedEvent">Create</button> | |
| 184 | 184 | </div> | |
| 185 | 185 | </form> | |
| 186 | 186 | `; |
| @@ -66,15 +66,15 @@ | |||
| 66 | 66 | // Desktop: drag-paint via mouse events. Touch: tap via onclick, long-press wired post-render. | |
| 67 | 67 | const paintHandlers = isTouch | |
| 68 | 68 | ? '' | |
| 69 | - | : ` onmousedown="GoingsOn.dayPlan.onPaintStart(event, ${slotIdx}, '${escArg(slotTimestamp)}')" | |
| 70 | - | onmouseenter="GoingsOn.dayPlan.onPaintMove(event, ${slotIdx}, '${escArg(slotTimestamp)}')"`; | |
| 69 | + | : ` data-mousedown="dayPlan.onPaintStart" data-a1="${escAttr(slotTimestamp)}" data-args='["@event", ${slotIdx}, "@a1"]' | |
| 70 | + | data-mouseenter="dayPlan.onPaintMove" data-a1="${escAttr(slotTimestamp)}" data-args='["@event", ${slotIdx}, "@a1"]'`; | |
| 71 | 71 | ||
| 72 | 72 | slotsHtml += ` | |
| 73 | 73 | <div class="timeline-slot${isHourStart ? ' hour-start' : ''}" | |
| 74 | 74 | data-time="${escAttr(slotTimestamp)}" | |
| 75 | 75 | data-hour="${hour}" | |
| 76 | 76 | data-slot-index="${slotIdx}"${paintHandlers} | |
| 77 | - | onclick="GoingsOn.dayPlan.onSlotTap(event, ${slotIdx}, '${escArg(slotTimestamp)}')"> | |
| 77 | + | data-act="dayPlan.onSlotTap" data-a1="${escAttr(slotTimestamp)}" data-args='["@event", ${slotIdx}, "@a1"]'> | |
| 78 | 78 | <div class="timeline-time">${isHourStart ? timeStr : ''}</div> | |
| 79 | 79 | <div class="timeline-slot-area"></div> | |
| 80 | 80 | </div> | |
| @@ -117,7 +117,7 @@ | |||
| 117 | 117 | // Touch: tap opens, long-press opens action sheet (wired post-render). No mouse drag. | |
| 118 | 118 | const dragHandler = isTouch | |
| 119 | 119 | ? '' | |
| 120 | - | : ` onmousedown="GoingsOn.dayPlan.onItemDragStart(event, '${escArg(item.id)}', '${escArg(item.itemType)}')"`; | |
| 120 | + | : ` data-mousedown="dayPlan.onItemDragStart" data-a1="@event" data-a2="${escAttr(item.id)}" data-a3="${escAttr(item.itemType)}"`; | |
| 121 | 121 | const titleHint = isTouch ? '' : ' (drag to reschedule)'; | |
| 122 | 122 | itemsHtml += ` | |
| 123 | 123 | <div class="timeline-item ${item.itemType} ${blockClass} ${hasConflict ? 'conflict' : ''}" | |
| @@ -125,8 +125,8 @@ | |||
| 125 | 125 | data-id="${escAttr(item.id)}" | |
| 126 | 126 | data-type="${escAttr(item.itemType)}" | |
| 127 | 127 | data-duration="${duration}" | |
| 128 | - | onclick="GoingsOn.dayPlan.openTimelineItem('${escArg(item.id)}', '${escArg(item.itemType)}')"${dragHandler} | |
| 129 | - | onkeydown="GoingsOn.dayPlan.handleTimelineItemKeydown(event, '${escArg(item.id)}', '${escArg(item.itemType)}')" | |
| 128 | + | data-act="dayPlan.openTimelineItem" data-a1="${escAttr(item.id)}" data-a2="${escAttr(item.itemType)}"${dragHandler} | |
| 129 | + | data-keydown="dayPlan.handleTimelineItemKeydown" data-a1="@event" data-a2="${escAttr(item.id)}" data-a3="${escAttr(item.itemType)}" | |
| 130 | 130 | title="${escAttr(item.title)}${titleHint}${keyboardHint}" | |
| 131 | 131 | tabindex="0" role="button" aria-label="${escAttr(item.title)}${keyboardHint}"> | |
| 132 | 132 | <div class="timeline-item-title">${esc(item.title)}</div> | |
| @@ -146,16 +146,16 @@ | |||
| 146 | 146 | return ` | |
| 147 | 147 | <div class="unscheduled-task priority-${task.priority.toLowerCase()}" | |
| 148 | 148 | data-id="${escAttr(task.id)}" | |
| 149 | - | onclick="GoingsOn.tasks.openSubtasks('${escArg(task.id)}')" | |
| 150 | - | onkeydown="GoingsOn.dayPlan.handleUnscheduledTaskKeydown(event, '${escArg(task.id)}')" | |
| 149 | + | data-act="tasks.openSubtasks" data-a1="${escAttr(task.id)}" | |
| 150 | + | data-keydown="dayPlan.handleUnscheduledTaskKeydown" data-a1="@event" data-a2="${escAttr(task.id)}" | |
| 151 | 151 | tabindex="0" role="listitem" aria-label="Unscheduled task: ${esc(task.description)} (Press S to schedule)"> | |
| 152 | 152 | <div class="unscheduled-task-title">${esc(task.description)}</div> | |
| 153 | 153 | <div class="unscheduled-task-meta"> | |
| 154 | 154 | ${task.projectName ? esc(task.projectName) + ' - ' : ''}${task.priority} | |
| 155 | 155 | </div> | |
| 156 | - | <div class="unscheduled-task-actions" onclick="event.stopPropagation()"> | |
| 157 | - | <button class="btn btn-sm btn-ghost" onclick="GoingsOn.timeTracking.startTimer('${escArg(task.id)}')" title="Track Time">Track</button> | |
| 158 | - | <button class="btn btn-sm btn-ghost" onclick="GoingsOn.focusTimer.start('${escArg(task.id)}')" title="Focus Mode">Focus</button> | |
| 156 | + | <div class="unscheduled-task-actions" data-act="ui.noop"> | |
| 157 | + | <button class="btn btn-sm btn-ghost" data-act="timeTracking.startTimer" data-a1="${escAttr(task.id)}" title="Track Time">Track</button> | |
| 158 | + | <button class="btn btn-sm btn-ghost" data-act="focusTimer.start" data-a1="${escAttr(task.id)}" title="Focus Mode">Focus</button> | |
| 159 | 159 | </div> | |
| 160 | 160 | </div> | |
| 161 | 161 | `; |
| @@ -33,7 +33,7 @@ | |||
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | const timeSlotsHtml = timeSlots.slice(0, 12).map(slot => ` | |
| 36 | - | <button class="btn btn-sm btn-secondary time-block-quick-btn" onclick="GoingsOn.dayPlan.selectTimeSlot(this, '${slot.time.toISOString()}')"> | |
| 36 | + | <button class="btn btn-sm btn-secondary time-block-quick-btn" data-act="dayPlan.selectTimeSlot" data-a1="@el" data-a2="${escAttr(slot.time.toISOString())}"> | |
| 37 | 37 | ${slot.label} | |
| 38 | 38 | </button> | |
| 39 | 39 | `).join(''); | |
| @@ -57,12 +57,12 @@ | |||
| 57 | 57 | <div class="form-group"> | |
| 58 | 58 | <label class="form-label" id="schedule-duration-label">Duration</label> | |
| 59 | 59 | <div class="duration-presets" role="group" aria-labelledby="schedule-duration-label"> | |
| 60 | - | <button class="duration-preset" onclick="GoingsOn.dayPlan.selectDuration(this, 15)">15m</button> | |
| 61 | - | <button class="duration-preset selected" onclick="GoingsOn.dayPlan.selectDuration(this, 30)">30m</button> | |
| 62 | - | <button class="duration-preset" onclick="GoingsOn.dayPlan.selectDuration(this, 45)">45m</button> | |
| 63 | - | <button class="duration-preset" onclick="GoingsOn.dayPlan.selectDuration(this, 60)">1h</button> | |
| 64 | - | <button class="duration-preset" onclick="GoingsOn.dayPlan.selectDuration(this, 90)">1.5h</button> | |
| 65 | - | <button class="duration-preset" onclick="GoingsOn.dayPlan.selectDuration(this, 120)">2h</button> | |
| 60 | + | <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 15]'>15m</button> | |
| 61 | + | <button class="duration-preset selected" data-act="dayPlan.selectDuration" data-args='["@el", 30]'>30m</button> | |
| 62 | + | <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 45]'>45m</button> | |
| 63 | + | <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 60]'>1h</button> | |
| 64 | + | <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 90]'>1.5h</button> | |
| 65 | + | <button class="duration-preset" data-act="dayPlan.selectDuration" data-args='["@el", 120]'>2h</button> | |
| 66 | 66 | </div> | |
| 67 | 67 | <input type="hidden" id="schedule-duration" value="30"> | |
| 68 | 68 | </div> | |
| @@ -70,8 +70,8 @@ | |||
| 70 | 70 | <div id="schedule-conflict-warning"></div> | |
| 71 | 71 | ||
| 72 | 72 | <div class="form-actions"> | |
| 73 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Cancel</button> | |
| 74 | - | <button type="button" class="btn btn-primary" onclick="GoingsOn.dayPlan.scheduleTaskFromModal('${escArg(id)}')">Schedule Task</button> | |
| 73 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Cancel</button> | |
| 74 | + | <button type="button" class="btn btn-primary" data-act="dayPlan.scheduleTaskFromModal" data-a1="${escAttr(id)}">Schedule Task</button> | |
| 75 | 75 | </div> | |
| 76 | 76 | </div> | |
| 77 | 77 | `; | |
| @@ -244,14 +244,14 @@ | |||
| 244 | 244 | bar.classList.remove('hidden'); | |
| 245 | 245 | if (state === 'current') { | |
| 246 | 246 | bar.innerHTML = ` | |
| 247 | - | <button class="btn btn-primary finish-review-btn" id="day-finish-review-btn" onclick="GoingsOn.dayPlanSchedule.openFinishReviewModal()"> | |
| 247 | + | <button class="btn btn-primary finish-review-btn" id="day-finish-review-btn" data-act="dayPlanSchedule.openFinishReviewModal"> | |
| 248 | 248 | Finish & Review | |
| 249 | 249 | </button> | |
| 250 | 250 | `; | |
| 251 | 251 | } else { | |
| 252 | 252 | const label = isReviewed ? 'View Past Review' : 'Review Past Day'; | |
| 253 | 253 | bar.innerHTML = ` | |
| 254 | - | <button class="btn btn-secondary finish-review-btn" id="day-finish-review-btn" onclick="GoingsOn.dayPlanSchedule.openFinishReviewModal()"> | |
| 254 | + | <button class="btn btn-secondary finish-review-btn" id="day-finish-review-btn" data-act="dayPlanSchedule.openFinishReviewModal"> | |
| 255 | 255 | ${label} | |
| 256 | 256 | </button> | |
| 257 | 257 | `; | |
| @@ -306,7 +306,7 @@ | |||
| 306 | 306 | ${banner} | |
| 307 | 307 | ${reflectionHtml} | |
| 308 | 308 | <div class="review-actions-grid"> | |
| 309 | - | <button type="button" class="btn btn-primary" onclick="GoingsOn.dayPlan.saveDailyReview()"> | |
| 309 | + | <button type="button" class="btn btn-primary" data-act="dayPlan.saveDailyReview"> | |
| 310 | 310 | ${isReviewed ? 'Update Review' : 'Save Review'} | |
| 311 | 311 | </button> | |
| 312 | 312 | </div> |
| @@ -16,14 +16,22 @@ | |||
| 16 | 16 | * `data-keydown`, `data-contextmenu`, `data-mousedown` hold a dot-path | |
| 17 | 17 | * resolved against `window.GoingsOn`. | |
| 18 | 18 | * - Positional args come from `data-a1`, `data-a2`, … (strings), or a single | |
| 19 | - | * `data-args` holding a JSON array (for typed / null / multi args). | |
| 20 | - | * - Handlers are called `fn(...args, ctx)` with `ctx = { event, el }`. Methods | |
| 21 | - | * that need the event or element read the trailing `ctx`. | |
| 22 | - | * - `change` / `input` pass the element's value first: `fn(value, ...args, ctx)`, | |
| 23 | - | * matching the old `onchange="foo(this.value)"` idiom. | |
| 24 | - | * - Nested clickables: the innermost `[data-act]` wins via `closest()`, so no | |
| 25 | - | * `event.stopPropagation()` is needed — a row and a button inside it can both | |
| 26 | - | * carry `data-act` and only the button fires. | |
| 19 | + | * `data-args` holding a JSON array (for typed / null / mixed args; string | |
| 20 | + | * elements may reference the data-a* values via `@a1`, `@a2`, …). | |
| 21 | + | * - Sentinel arg values, substituted before the call, preserve existing method | |
| 22 | + | * signatures that took `event`/`this`: | |
| 23 | + | * `@event` → the DOM event, `@el` → the target element, | |
| 24 | + | * `@value` → the element's `.value`, `@aN` → `data-aN`. | |
| 25 | + | * So `onclick="ctx.showTask(event, '${id}')"` becomes | |
| 26 | + | * `data-act="ctx.showTask" data-a1="@event" data-a2="123"` with no change to | |
| 27 | + | * `showTask(event, id)`. | |
| 28 | + | * - `change` / `input` with *no* explicit args pass the element's value: | |
| 29 | + | * `data-change="foo"` → `foo(el.value)`, matching `onchange="foo(this.value)"`. | |
| 30 | + | * - Nested clickables: the innermost `[data-act]` wins via `closest()`, so a row | |
| 31 | + | * and a button inside it can both carry `data-act` and only the button fires — | |
| 32 | + | * no `event.stopPropagation()` needed. | |
| 33 | + | * - The dispatcher never appends its own context argument, so a method with | |
| 34 | + | * optional trailing params is never surprised by an extra value. | |
| 27 | 35 | */ | |
| 28 | 36 | ||
| 29 | 37 | (function () { | |
| @@ -40,22 +48,35 @@ | |||
| 40 | 48 | return typeof obj === 'function' ? obj : null; | |
| 41 | 49 | } | |
| 42 | 50 | ||
| 43 | - | /** Collect positional args: `data-args` JSON array, else data-a1..aN. */ | |
| 44 | - | function argsFrom(el) { | |
| 51 | + | /** Substitute a sentinel string with the live event/element value. */ | |
| 52 | + | function subst(v, event, el) { | |
| 53 | + | if (typeof v !== 'string' || v.charCodeAt(0) !== 64 /* '@' */) return v; | |
| 54 | + | if (v === '@event') return event; | |
| 55 | + | if (v === '@el') return el; | |
| 56 | + | if (v === '@value') return el.value; | |
| 57 | + | if (v === '@checked') return el.checked; | |
| 58 | + | var m = /^@a(\d+)$/.exec(v); | |
| 59 | + | if (m) return el.dataset['a' + m[1]]; | |
| 60 | + | return v; | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | /** Build the argument list for a handler element. */ | |
| 64 | + | function argsFrom(el, event, valueFirst) { | |
| 65 | + | var raw; | |
| 45 | 66 | if (el.dataset.args !== undefined) { | |
| 46 | - | try { return JSON.parse(el.dataset.args); } catch (e) { return []; } | |
| 67 | + | try { raw = JSON.parse(el.dataset.args); } catch (e) { raw = []; } | |
| 68 | + | } else { | |
| 69 | + | raw = []; | |
| 70 | + | var i = 1, v; | |
| 71 | + | while ((v = el.dataset['a' + i]) !== undefined) { raw.push(v); i++; } | |
| 72 | + | if (raw.length === 0 && valueFirst) return [el.value]; | |
| 47 | 73 | } | |
| 48 | - | var out = [], i = 1, v; | |
| 49 | - | while ((v = el.dataset['a' + i]) !== undefined) { out.push(v); i++; } | |
| 50 | - | return out; | |
| 74 | + | return raw.map(function (x) { return subst(x, event, el); }); | |
| 51 | 75 | } | |
| 52 | 76 | ||
| 53 | 77 | /** | |
| 54 | 78 | * Register a delegated listener for `eventType`, dispatching elements that | |
| 55 | 79 | * carry `data-<attr>`. | |
| 56 | - | * @param {string} eventType DOM event name. | |
| 57 | - | * @param {string} attr dataset key holding the method path. | |
| 58 | - | * @param {{valueFirst?:boolean, preventDefault?:boolean}} [opts] | |
| 59 | 80 | */ | |
| 60 | 81 | function bind(eventType, attr, opts) { | |
| 61 | 82 | opts = opts || {}; | |
| @@ -67,10 +88,8 @@ | |||
| 67 | 88 | if (!el) return; | |
| 68 | 89 | var fn = resolve(el.dataset[attr]); | |
| 69 | 90 | if (!fn) return; | |
| 70 | - | var args = argsFrom(el); | |
| 71 | - | if (opts.valueFirst) args = [el.value].concat(args); | |
| 72 | 91 | if (opts.preventDefault) event.preventDefault(); | |
| 73 | - | fn.apply(el, args.concat([{ event: event, el: el }])); | |
| 92 | + | fn.apply(el, argsFrom(el, event, opts.valueFirst)); | |
| 74 | 93 | }); | |
| 75 | 94 | } | |
| 76 | 95 | ||
| @@ -81,4 +100,10 @@ | |||
| 81 | 100 | bind('keydown', 'keydown'); | |
| 82 | 101 | bind('contextmenu', 'contextmenu'); | |
| 83 | 102 | bind('mousedown', 'mousedown'); | |
| 103 | + | bind('dragstart', 'dragstart'); | |
| 104 | + | bind('dragover', 'dragover'); | |
| 105 | + | bind('drop', 'drop'); | |
| 106 | + | // `mouseenter` does not bubble, so delegate via `mouseover` (which does). | |
| 107 | + | // Handlers should be idempotent per target element. | |
| 108 | + | bind('mouseover', 'mouseenter'); | |
| 84 | 109 | })(); |
| @@ -28,14 +28,15 @@ | |||
| 28 | 28 | * @param {Object} opts | |
| 29 | 29 | * @param {string} opts.formId - Form element ID | |
| 30 | 30 | * @param {string} opts.idPrefix - ID prefix for inputs (e.g. 'acct' or 'edit-acct') | |
| 31 | - | * @param {string} opts.onSubmit - Form onsubmit attribute value | |
| 31 | + | * @param {string} opts.submitAct - Delegated submit action dot-path | |
| 32 | + | * @param {string} [opts.submitId] - Optional id arg for the submit action | |
| 32 | 33 | * @param {Object} opts.values - Current field values (empty strings / defaults for add) | |
| 33 | 34 | * @param {boolean} opts.isEdit - Whether this is the edit form | |
| 34 | 35 | * @param {string} opts.submitLabel - Submit button label | |
| 35 | 36 | * @returns {string} HTML string for the form | |
| 36 | 37 | */ | |
| 37 | 38 | function buildAccountFormHtml(opts) { | |
| 38 | - | const { formId, idPrefix, onSubmit, values, isEdit, submitLabel } = opts; | |
| 39 | + | const { formId, idPrefix, submitAct, submitId, values, isEdit, submitLabel } = opts; | |
| 39 | 40 | ||
| 40 | 41 | const passwordLabel = isEdit ? 'Password (leave empty to keep current)' : 'Password'; | |
| 41 | 42 | const passwordRequired = isEdit ? '' : 'required'; | |
| @@ -76,7 +77,7 @@ | |||
| 76 | 77 | }); | |
| 77 | 78 | ||
| 78 | 79 | return ` | |
| 79 | - | <form id="${formId}" onsubmit="${escAttr(onSubmit)}"> | |
| 80 | + | <form id="${formId}" data-submit="${escAttr(submitAct)}" data-a1="@event"${submitId ? ` data-a2="${escAttr(submitId)}"` : ''}> | |
| 80 | 81 | ${ff({ kind: 'text', name: 'account_name', id: `${idPrefix}-name`, label: 'Account Name', value: values.accountName || '', placeholder: 'Personal, Work, etc.', required: true })} | |
| 81 | 82 | <div class="form-group"> | |
| 82 | 83 | <label class="form-label" for="${idPrefix}-email">Email Address</label> | |
| @@ -88,7 +89,7 @@ | |||
| 88 | 89 | ${ff({ kind: 'password', name: 'password', id: `${idPrefix}-password`, label: passwordLabel, placeholder: passwordPlaceholder, required: !isEdit })} | |
| 89 | 90 | ${ff({ kind: 'text', name: 'archive_folder_name', id: `${idPrefix}-archive-folder`, label: 'Archive Folder Name', value: values.archiveFolderName || 'Archive', placeholder: 'Archive', hint: archiveHint })} | |
| 90 | 91 | ${ff({ kind: 'textarea', name: 'email_signature', id: `${idPrefix}-signature`, label: 'Email Signature', value: values.emailSignature || '', placeholder: '-- \nYour Name', hint: 'Appended to outbound emails. Plain text only.', attrs: { rows: 3 } })} | |
| 91 | - | <button type="button" class="form-more-toggle ${advancedExpanded}" onclick="this.classList.toggle('expanded'); this.nextElementSibling.classList.toggle('hidden');">Advanced settings</button> | |
| 92 | + | <button type="button" class="form-more-toggle ${advancedExpanded}" data-act="ui.toggleExpand" data-a1="@el">Advanced settings</button> | |
| 92 | 93 | <div class="${advancedHidden}"> | |
| 93 | 94 | <div class="form-grid-2"> | |
| 94 | 95 | ${ff({ kind: 'text', name: 'imap_server', id: `${idPrefix}-imap-server`, label: 'IMAP Server', value: values.imapServer || '', placeholder: 'imap.example.com', required: true })} | |
| @@ -108,7 +109,7 @@ | |||
| 108 | 109 | ${ff({ kind: 'select', name: 'sync_interval_minutes', id: `${idPrefix}-sync-interval`, label: 'Auto-sync Interval', options: syncIntervalOpts, hint: 'Automatically check for new emails at this interval.' })} | |
| 109 | 110 | </div> | |
| 110 | 111 | <div class="form-actions"> | |
| 111 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.refreshAccountsView()">Cancel</button> | |
| 112 | + | <button type="button" class="btn btn-secondary" data-act="emails.refreshAccountsView">Cancel</button> | |
| 112 | 113 | <button type="submit" class="btn btn-primary">${esc(submitLabel)}</button> | |
| 113 | 114 | </div> | |
| 114 | 115 | </form> | |
| @@ -255,8 +256,8 @@ | |||
| 255 | 256 | : ''; | |
| 256 | 257 | ||
| 257 | 258 | const editBtn = isOAuth | |
| 258 | - | ? `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.reconnectOAuth('${escArg(a.id)}')">Reconnect</button>` | |
| 259 | - | : `<button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.editAccount('${escArg(a.id)}')">Edit</button>`; | |
| 259 | + | ? `<button class="btn btn-sm btn-secondary" data-act="emails.reconnectOAuth" data-a1="${escAttr(a.id)}">Reconnect</button>` | |
| 260 | + | : `<button class="btn btn-sm btn-secondary" data-act="emails.editAccount" data-a1="${escAttr(a.id)}">Edit</button>`; | |
| 260 | 261 | ||
| 261 | 262 | return ` | |
| 262 | 263 | <div class="account-row"> | |
| @@ -267,12 +268,12 @@ | |||
| 267 | 268 | <div class="account-row-sync">Last sync: ${a.lastSyncFormatted}</div> | |
| 268 | 269 | </div> | |
| 269 | 270 | ${editBtn} | |
| 270 | - | <button class="btn btn-sm btn-danger" onclick="GoingsOn.emails.deleteAccount('${escArg(a.id)}')" aria-label="Delete account">×</button> | |
| 271 | + | <button class="btn btn-sm btn-danger" data-act="emails.deleteAccount" data-a1="${escAttr(a.id)}" aria-label="Delete account">×</button> | |
| 271 | 272 | </div> | |
| 272 | 273 | <div class="account-row-quick"> | |
| 273 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.testAccount('${escArg(a.id)}')">Test</button> | |
| 274 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.syncAccount('${escArg(a.id)}', false)">Sync New</button> | |
| 275 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.syncAccount('${escArg(a.id)}', true)">Full Sync</button> | |
| 274 | + | <button class="btn btn-sm btn-secondary" data-act="emails.testAccount" data-a1="${escAttr(a.id)}">Test</button> | |
| 275 | + | <button class="btn btn-sm btn-secondary" data-act="emails.syncAccount" data-a1="${escAttr(a.id)}" data-args='["@a1", false]'>Sync New</button> | |
| 276 | + | <button class="btn btn-sm btn-secondary" data-act="emails.syncAccount" data-a1="${escAttr(a.id)}" data-args='["@a1", true]'>Full Sync</button> | |
| 276 | 277 | </div> | |
| 277 | 278 | </div> | |
| 278 | 279 | `; | |
| @@ -288,9 +289,9 @@ | |||
| 288 | 289 | ${accountsList} | |
| 289 | 290 | </div> | |
| 290 | 291 | <div class="form-actions"> | |
| 291 | - | <button type="button" class="btn btn-primary" onclick="GoingsOn.emails.openAddAccountModal()">+ Add Account</button> | |
| 292 | + | <button type="button" class="btn btn-primary" data-act="emails.openAddAccountModal">+ Add Account</button> | |
| 292 | 293 | <div class="form-actions-spacer"></div> | |
| 293 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button> | |
| 294 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Close</button> | |
| 294 | 295 | </div> | |
| 295 | 296 | `; | |
| 296 | 297 | GoingsOn.ui.openModal('Email Accounts', content); | |
| @@ -311,7 +312,7 @@ | |||
| 311 | 312 | ${accountsList} | |
| 312 | 313 | </div> | |
| 313 | 314 | <div class="form-actions"> | |
| 314 | - | <button type="button" class="btn btn-primary" onclick="GoingsOn.emails.openAddAccountModal()">+ Add Account</button> | |
| 315 | + | <button type="button" class="btn btn-primary" data-act="emails.openAddAccountModal">+ Add Account</button> | |
| 315 | 316 | </div> | |
| 316 | 317 | </div> | |
| 317 | 318 | `; | |
| @@ -365,7 +366,7 @@ | |||
| 365 | 366 | <div class="oauth-block"> | |
| 366 | 367 | <div class="oauth-buttons"> | |
| 367 | 368 | ${oauthProviders.map(p => ` | |
| 368 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.startOAuth('${escArg(p.id)}')"> | |
| 369 | + | <button type="button" class="btn btn-secondary" data-act="emails.startOAuth" data-a1="${escAttr(p.id)}"> | |
| 369 | 370 | ${esc(p.name)} | |
| 370 | 371 | </button> | |
| 371 | 372 | `).join('')} | |
| @@ -378,7 +379,7 @@ | |||
| 378 | 379 | const formHtml = buildAccountFormHtml({ | |
| 379 | 380 | formId: 'email-account-form', | |
| 380 | 381 | idPrefix: 'acct', | |
| 381 | - | onSubmit: "GoingsOn.emails.createAccount(event)", | |
| 382 | + | submitAct: 'emails.createAccount', | |
| 382 | 383 | values: {}, | |
| 383 | 384 | isEdit: false, | |
| 384 | 385 | submitLabel: 'Add Account', | |
| @@ -432,7 +433,7 @@ | |||
| 432 | 433 | const content = buildAccountFormHtml({ | |
| 433 | 434 | formId: 'edit-email-account-form', | |
| 434 | 435 | idPrefix: 'edit-acct', | |
| 435 | - | onSubmit: `GoingsOn.emails.updateAccount(event, '${escArg(id)}')`, | |
| 436 | + | submitAct: 'emails.updateAccount', submitId: id, | |
| 436 | 437 | values: account, | |
| 437 | 438 | isEdit: true, | |
| 438 | 439 | submitLabel: 'Save Changes', | |
| @@ -531,7 +532,7 @@ | |||
| 531 | 532 | ${foldersHtml} | |
| 532 | 533 | </div> | |
| 533 | 534 | <div class="form-actions"> | |
| 534 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.refreshAccountsView()">Back</button> | |
| 535 | + | <button type="button" class="btn btn-secondary" data-act="emails.refreshAccountsView">Back</button> | |
| 535 | 536 | </div> | |
| 536 | 537 | `; | |
| 537 | 538 | GoingsOn.ui.openModal('Connection Test Results', content); | |
| @@ -575,7 +576,7 @@ | |||
| 575 | 576 | ` : ''} | |
| 576 | 577 | </div> | |
| 577 | 578 | <div class="form-actions"> | |
| 578 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.refreshAccountsView()">Back</button> | |
| 579 | + | <button type="button" class="btn btn-secondary" data-act="emails.refreshAccountsView">Back</button> | |
| 579 | 580 | </div> | |
| 580 | 581 | `; | |
| 581 | 582 | GoingsOn.ui.openModal('Sync Results', content); | |
| @@ -662,7 +663,7 @@ | |||
| 662 | 663 | A browser window should have opened. Please sign in and authorize the app. | |
| 663 | 664 | </div> | |
| 664 | 665 | <div class="spinner oauth-waiting-spinner"></div> | |
| 665 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.emails.cancelOAuth()">Cancel</button> | |
| 666 | + | <button type="button" class="btn btn-secondary" data-act="emails.cancelOAuth">Cancel</button> | |
| 666 | 667 | </div> | |
| 667 | 668 | `; | |
| 668 | 669 | GoingsOn.ui.openModal('Connecting Account', content); |
| @@ -12,6 +12,7 @@ | |||
| 12 | 12 | ||
| 13 | 13 | const esc = GoingsOn.utils.escapeHtml; | |
| 14 | 14 | const escArg = GoingsOn.utils.escapeHandlerArg; | |
| 15 | + | const escAttr = GoingsOn.utils.escapeAttrValue; | |
| 15 | 16 | ||
| 16 | 17 | /** | |
| 17 | 18 | * Open the email compose interface (new window on desktop, modal on mobile). | |
| @@ -79,7 +80,7 @@ function openComposeModal(prefill) { | |||
| 79 | 80 | }; | |
| 80 | 81 | ||
| 81 | 82 | const content = ` | |
| 82 | - | <form id="compose-modal-form" onsubmit="event.preventDefault(); return false;"> | |
| 83 | + | <form id="compose-modal-form" data-submit="ui.noop"> | |
| 83 | 84 | <span id="reply-indicator" class="reply-indicator"></span> | |
| 84 | 85 | ${fieldsHtml} | |
| 85 | 86 | <div class="form-actions" style="margin-top: 0.75rem;"> | |
| @@ -229,7 +230,7 @@ async function openDraftsModal() { | |||
| 229 | 230 | const subject = d.subject || '(no subject)'; | |
| 230 | 231 | return ` | |
| 231 | 232 | <div class="email-item email-draft-item" | |
| 232 | - | onclick="GoingsOn.emails.openDraft('${escArg(d.id)}')"> | |
| 233 | + | data-act="emails.openDraft" data-a1="${escAttr(d.id)}"> | |
| 233 | 234 | <div class="email-draft-subject">${esc(subject)}</div> | |
| 234 | 235 | <div class="email-draft-meta">To: ${esc(to)} · ${d.receivedFormatted}</div> | |
| 235 | 236 | </div> |
| @@ -33,17 +33,17 @@ function renderEmailItem(thread, index) { | |||
| 33 | 33 | return ` | |
| 34 | 34 | <div class="email-item email-item-with-checkbox ${thread.hasUnread ? 'unread' : ''} ${isSnoozed ? 'email-snoozed' : ''}" | |
| 35 | 35 | data-id="${escAttr(e.id)}" | |
| 36 | - | oncontextmenu="GoingsOn.contextMenus.showEmail(event, '${escArg(e.id)}')" | |
| 36 | + | data-contextmenu="contextMenus.showEmail" data-a1="@event" data-a2="${escAttr(e.id)}" | |
| 37 | 37 | data-email-archived="${e.isArchived}" data-email-read="${e.isRead}" | |
| 38 | 38 | data-email-snoozed="${isSnoozed}" | |
| 39 | 39 | tabindex="0" role="listitem" aria-label="Email from ${esc(e.from)}"> | |
| 40 | - | <div class="email-checkbox-cell" onclick="event.stopPropagation();"> | |
| 40 | + | <div class="email-checkbox-cell" data-act="ui.noop"> | |
| 41 | 41 | <input type="checkbox" class="bulk-checkbox" data-id="${escAttr(e.id)}" | |
| 42 | 42 | ${isSelected ? 'checked' : ''} | |
| 43 | - | onchange="GoingsOn.emails.toggleSelection('${escArg(e.id)}', this, event)" | |
| 43 | + | data-change="emails.toggleSelection" data-a1="${escAttr(e.id)}" data-a2="@el" data-a3="@event" | |
| 44 | 44 | aria-label="Select email"> | |
| 45 | 45 | </div> | |
| 46 | - | <div class="email-content" onclick="GoingsOn.emails.open('${escArg(e.id)}')" role="button"> | |
| 46 | + | <div class="email-content" data-act="emails.open" data-a1="${escAttr(e.id)}" role="button"> | |
| 47 | 47 | <div class="email-header"> | |
| 48 | 48 | <span class="email-from">${esc(e.from)}</span> | |
| 49 | 49 | ${threadBadge} | |
| @@ -53,7 +53,7 @@ function renderEmailItem(thread, index) { | |||
| 53 | 53 | <div class="email-subject">${esc(e.subject)}${labelBadges ? ' ' + labelBadges : ''}</div> | |
| 54 | 54 | <div class="email-preview">${esc(e.bodyPreview)}...</div> | |
| 55 | 55 | </div> | |
| 56 | - | <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEmail(event, '${escArg(e.id)}')" title="Actions" aria-label="Email actions">⋮</button> | |
| 56 | + | <button class="btn-icon kebab-btn" data-act="contextMenus.showEmail" data-a1="@event" data-a2="${escAttr(e.id)}" title="Actions" aria-label="Email actions">⋮</button> | |
| 57 | 57 | </div> | |
| 58 | 58 | `; | |
| 59 | 59 | } |
| @@ -172,8 +172,8 @@ | |||
| 172 | 172 | if (response.total === 0) { | |
| 173 | 173 | const hasAccounts = GoingsOn.getEmailAccountsCache().length > 0; | |
| 174 | 174 | container.innerHTML = hasAccounts | |
| 175 | - | ? GoingsOn.ui.renderEmptyState('No emails yet.', 'Compose', 'GoingsOn.emails.openCompose()', 'emails') | |
| 176 | - | : GoingsOn.ui.renderEmptyState('Set up an email account to get started.', 'Add Account', 'GoingsOn.emails.openAccountsModal()', 'inbox'); | |
| 175 | + | ? GoingsOn.ui.renderEmptyState('No emails yet.', 'Compose', 'emails.openCompose', 'emails') | |
| 176 | + | : GoingsOn.ui.renderEmptyState('Set up an email account to get started.', 'Add Account', 'emails.openAccountsModal', 'inbox'); | |
| 177 | 177 | // Hide pagination | |
| 178 | 178 | const paginationEl = document.getElementById('email-pagination'); | |
| 179 | 179 | if (paginationEl) paginationEl.classList.add('hidden'); | |
| @@ -207,7 +207,7 @@ | |||
| 207 | 207 | } | |
| 208 | 208 | GoingsOn.cache.markLoaded('emails'); | |
| 209 | 209 | } catch (err) { | |
| 210 | - | container.innerHTML = `<div class="loading loading--error">Failed to load emails. <button class="btn-link" onclick="GoingsOn.emails.load()">Try again</button></div>`; | |
| 210 | + | container.innerHTML = `<div class="loading loading--error">Failed to load emails. <button class="btn-link" data-act="emails.load">Try again</button></div>`; | |
| 211 | 211 | GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load emails'), 'error', { | |
| 212 | 212 | action: { label: 'Retry', fn: load }, | |
| 213 | 213 | duration: 8000, | |
| @@ -273,14 +273,14 @@ | |||
| 273 | 273 | // Build action buttons for the most recent email | |
| 274 | 274 | const latestEmail = threadEmails[threadEmails.length - 1]; | |
| 275 | 275 | const archiveBtn = latestEmail.isArchived | |
| 276 | - | ? `<button class="btn btn-secondary" onclick="GoingsOn.emails.unarchive('${escArg(latestEmail.id)}')">Unarchive</button>` | |
| 277 | - | : `<button class="btn btn-secondary" onclick="GoingsOn.emails.archive('${escArg(latestEmail.id)}')">Archive</button>`; | |
| 276 | + | ? `<button class="btn btn-secondary" data-act="emails.unarchive" data-a1="${escAttr(latestEmail.id)}">Unarchive</button>` | |
| 277 | + | : `<button class="btn btn-secondary" data-act="emails.archive" data-a1="${escAttr(latestEmail.id)}">Archive</button>`; | |
| 278 | 278 | ||
| 279 | 279 | // Use pre-computed field from backend | |
| 280 | 280 | const isSnoozed = latestEmail.isSnoozed; | |
| 281 | 281 | const snoozeBtn = isSnoozed | |
| 282 | - | ? `<button class="btn btn-secondary" onclick="GoingsOn.snooze.unsnooze('email', '${escArg(latestEmail.id)}')">Unsnooze</button>` | |
| 283 | - | : `<button class="btn btn-secondary" onclick="GoingsOn.snooze.openModal('email', '${escArg(latestEmail.id)}')">Snooze</button>`; | |
| 282 | + | ? `<button class="btn btn-secondary" data-act="snooze.unsnooze" data-a1="email" data-a2="${escAttr(latestEmail.id)}">Unsnooze</button>` | |
| 283 | + | : `<button class="btn btn-secondary" data-act="snooze.openModal" data-a1="email" data-a2="${escAttr(latestEmail.id)}">Snooze</button>`; | |
| 284 | 284 | ||
| 285 | 285 | // Look up contact from sender email | |
| 286 | 286 | const parsed = GoingsOn.utils.parseEmailAddress(email.from); | |
| @@ -306,7 +306,7 @@ | |||
| 306 | 306 | <span class="email-sender-name">${esc(senderContact.displayName || senderContact.display_name)}</span> | |
| 307 | 307 | ${company ? `<span class="email-sender-company">${company}</span>` : ''} | |
| 308 | 308 | </div> | |
| 309 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.contacts.open('${escArg(senderContact.id)}')">View Contact</button> | |
| 309 | + | <button class="btn btn-sm btn-secondary" data-act="ui.closeModalThen" data-a1="contacts.open" data-a2="${escAttr(senderContact.id)}">View Contact</button> | |
| 310 | 310 | </div> | |
| 311 | 311 | `; | |
| 312 | 312 | } else if (parsed.email) { | |
| @@ -316,7 +316,7 @@ | |||
| 316 | 316 | <div class="email-sender-info"> | |
| 317 | 317 | <span class="email-sender-name">${esc(parsed.name || parsed.email)}</span> | |
| 318 | 318 | </div> | |
| 319 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.createContactFromSender('${escArg(id)}')">+ Save Contact</button> | |
| 319 | + | <button class="btn btn-sm btn-secondary" data-act="emails.createContactFromSender" data-a1="${escAttr(id)}">+ Save Contact</button> | |
| 320 | 320 | </div> | |
| 321 | 321 | `; | |
| 322 | 322 | } | |
| @@ -335,8 +335,8 @@ | |||
| 335 | 335 | <span class="attachment-filename email-attachment-name" | |
| 336 | 336 | title="${escAttr(a.filename)}">${esc(a.filename)}</span> | |
| 337 | 337 | <span class="email-attachment-size">${esc(a.sizeFormatted)}</span> | |
| 338 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.openBlob('${escArg(a.blobHash)}', '${escArg(a.filename)}')" title="Open">Open</button> | |
| 339 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.saveBlob('${escArg(a.blobHash)}', '${escArg(a.filename)}')" title="Save">Save</button> | |
| 338 | + | <button class="btn btn-sm btn-secondary" data-act="emails.openBlob" data-a1="${escAttr(a.blobHash)}" data-a2="${escAttr(a.filename)}" title="Open">Open</button> | |
| 339 | + | <button class="btn btn-sm btn-secondary" data-act="emails.saveBlob" data-a1="${escAttr(a.blobHash)}" data-a2="${escAttr(a.filename)}" title="Save">Save</button> | |
| 340 | 340 | </div> | |
| 341 | 341 | `; | |
| 342 | 342 | }).join(''); | |
| @@ -360,7 +360,7 @@ | |||
| 360 | 360 | const truncatedNotice = e.bodyTruncated | |
| 361 | 361 | ? `<div class="email-body-truncated" id="email-trunc-${escAttr(e.id)}"> | |
| 362 | 362 | <span>Message truncated.</span> | |
| 363 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.emails.loadFullBody('${escArg(e.id)}')">Load full message</button> | |
| 363 | + | <button class="btn btn-sm btn-secondary" data-act="emails.loadFullBody" data-a1="${escAttr(e.id)}">Load full message</button> | |
| 364 | 364 | </div>` | |
| 365 | 365 | : ''; | |
| 366 | 366 | ||
| @@ -398,34 +398,34 @@ | |||
| 398 | 398 | ${threadContent} | |
| 399 | 399 | </div> | |
| 400 | 400 | <div class="form-actions email-actions-bar"> | |
| 401 | - | <button class="btn btn-primary" onclick="GoingsOn.emails.reply('${escArg(latestEmail.id)}')">Reply</button> | |
| 402 | - | <button class="btn btn-secondary" onclick="GoingsOn.emails.replyAll('${escArg(latestEmail.id)}')">Reply All</button> | |
| 403 | - | <button class="btn btn-secondary" onclick="GoingsOn.emails.forward('${escArg(latestEmail.id)}')">Forward</button> | |
| 404 | - | <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.emails.delete('${escArg(latestEmail.id)}')">Delete</button> | |
| 401 | + | <button class="btn btn-primary" data-act="emails.reply" data-a1="${escAttr(latestEmail.id)}">Reply</button> | |
| 402 | + | <button class="btn btn-secondary" data-act="emails.replyAll" data-a1="${escAttr(latestEmail.id)}">Reply All</button> | |
| 403 | + | <button class="btn btn-secondary" data-act="emails.forward" data-a1="${escAttr(latestEmail.id)}">Forward</button> | |
| 404 | + | <button class="btn btn-secondary text-accent-red" data-act="emails.delete" data-a1="${escAttr(latestEmail.id)}">Delete</button> | |
| 405 | 405 | ${archiveBtn} | |
| 406 | 406 | ${snoozeBtn} | |
| 407 | - | <button class="btn btn-secondary" onclick="GoingsOn.emails.createTaskFromEmail('${escArg(latestEmail.id)}')">Create Task</button> | |
| 407 | + | <button class="btn btn-secondary" data-act="emails.createTaskFromEmail" data-a1="${escAttr(latestEmail.id)}">Create Task</button> | |
| 408 | 408 | <div class="dropdown" style="position: relative;"> | |
| 409 | - | <button class="btn btn-secondary" onclick="this.nextElementSibling.classList.toggle('show')"> | |
| 409 | + | <button class="btn btn-secondary" data-act="ui.toggleMenu" data-a1="@el"> | |
| 410 | 410 | Actions ▾ | |
| 411 | 411 | </button> | |
| 412 | 412 | <div class="dropdown-menu"> | |
| 413 | - | <button class="dropdown-item" onclick="GoingsOn.emails.createTaskFromEmail('${escArg(latestEmail.id)}'); this.parentElement.classList.remove('show');"> | |
| 413 | + | <button class="dropdown-item" data-act="ui.menuAction" data-a1="@el" data-a2="emails.createTaskFromEmail" data-a3="${escAttr(latestEmail.id)}"> | |
| 414 | 414 | Convert to Task | |
| 415 | 415 | </button> | |
| 416 | - | <button class="dropdown-item" onclick="GoingsOn.emails.createEventFromEmail('${escArg(latestEmail.id)}'); this.parentElement.classList.remove('show');"> | |
| 416 | + | <button class="dropdown-item" data-act="ui.menuAction" data-a1="@el" data-a2="emails.createEventFromEmail" data-a3="${escAttr(latestEmail.id)}"> | |
| 417 | 417 | Convert to Event | |
| 418 | 418 | </button> | |
| 419 | - | <button class="dropdown-item" onclick="GoingsOn.emails.editLabels('${escArg(latestEmail.id)}', ${escAttrVal(JSON.stringify(latestEmail.labels || []))}); this.parentElement.classList.remove('show');"> | |
| 419 | + | <button class="dropdown-item" data-act="ui.menuAction" data-a1="@el" data-a2="emails.editLabels" data-a3="${escAttr(latestEmail.id)}" data-a4="${escAttr(JSON.stringify(latestEmail.labels || []))}"> | |
| 420 | 420 | Edit Labels | |
| 421 | 421 | </button> | |
| 422 | - | <button class="dropdown-item" onclick="GoingsOn.emails.moveToFolder('${escArg(latestEmail.id)}'); this.parentElement.classList.remove('show');"> | |
| 422 | + | <button class="dropdown-item" data-act="ui.menuAction" data-a1="@el" data-a2="emails.moveToFolder" data-a3="${escAttr(latestEmail.id)}"> | |
| 423 | 423 | Move to Folder | |
| 424 | 424 | </button> | |
| 425 | 425 | </div> | |
| 426 | 426 | </div> | |
| 427 | 427 | <div class="flex-1"></div> | |
| 428 | - | <button class="btn btn-secondary" onclick="GoingsOn.emails.openInBrowser('${escArg(latestEmail.id)}')" title="Open in browser">Open in Browser</button> | |
| 428 | + | <button class="btn btn-secondary" data-act="emails.openInBrowser" data-a1="${escAttr(latestEmail.id)}" title="Open in browser">Open in Browser</button> | |
| 429 | 429 | </div> | |
| 430 | 430 | </div> | |
| 431 | 431 | `; | |
| @@ -809,7 +809,7 @@ | |||
| 809 | 809 | async function editLabels(emailId, currentLabels) { | |
| 810 | 810 | const existing = await GoingsOn.api.emails.listLabels(); | |
| 811 | 811 | const content = ` | |
| 812 | - | <form id="label-form" onsubmit="event.preventDefault(); GoingsOn.emails._saveLabels('${escArg(emailId)}');"> | |
| 812 | + | <form id="label-form" data-submit="emails._saveLabels" data-a1="${escAttr(emailId)}"> | |
| 813 | 813 | <div class="form-group"> | |
| 814 | 814 | <label class="form-label">Labels (comma-separated)</label> | |
| 815 | 815 | <input type="text" class="form-input" id="label-input" value="${escAttrVal((currentLabels || []).join(', '))}" | |
| @@ -817,7 +817,7 @@ | |||
| 817 | 817 | ${existing.length > 0 ? `<div class="label-existing-line">Existing: ${existing.map(l => esc(l)).join(', ')}</div>` : ''} | |
| 818 | 818 | </div> | |
| 819 | 819 | <div class="form-actions"> | |
| 820 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Cancel</button> | |
| 820 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Cancel</button> | |
| 821 | 821 | <button type="submit" class="btn btn-primary">Save</button> | |
| 822 | 822 | </div> | |
| 823 | 823 | </form> | |
| @@ -855,7 +855,7 @@ | |||
| 855 | 855 | const folders = await GoingsOn.api.emails.listFolders(); | |
| 856 | 856 | // Also try to get IMAP folders from account | |
| 857 | 857 | const content = ` | |
| 858 | - | <form onsubmit="event.preventDefault(); GoingsOn.emails._doMoveToFolder('${escArg(emailId)}');"> | |
| 858 | + | <form data-submit="emails._doMoveToFolder" data-a1="${escAttr(emailId)}"> | |
| 859 | 859 | <div class="form-group"> | |
| 860 | 860 | <label class="form-label">Move to folder</label> | |
| 861 | 861 | <input type="text" class="form-input" id="move-folder-input" placeholder="INBOX, Archive, Sent, ..." | |
| @@ -865,7 +865,7 @@ | |||
| 865 | 865 | </datalist> | |
| 866 | 866 | </div> | |
| 867 | 867 | <div class="form-actions"> | |
| 868 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Cancel</button> | |
| 868 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Cancel</button> | |
| 869 | 869 | <button type="submit" class="btn btn-primary">Move</button> | |
| 870 | 870 | </div> | |
| 871 | 871 | </form> | |
| @@ -958,7 +958,7 @@ | |||
| 958 | 958 | if (emailScroller) { emailScroller.destroy(); emailScroller = null; } | |
| 959 | 959 | container.innerHTML = response.results.map(r => ` | |
| 960 | 960 | <div class="email-item" data-id="${escAttr(r.id)}" | |
| 961 | - | onclick="GoingsOn.emails.open('${escArg(r.id)}')" | |
| 961 | + | data-act="emails.open" data-a1="${escAttr(r.id)}" | |
| 962 | 962 | tabindex="0" role="listitem"> | |
| 963 | 963 | <div class="email-content"> | |
| 964 | 964 | <div class="email-header"> |
| @@ -122,13 +122,13 @@ | |||
| 122 | 122 | if (!isCurrentMonth) classes.push('other-month'); | |
| 123 | 123 | if (isToday) classes.push('today'); | |
| 124 | 124 | ||
| 125 | - | html += `<div class="${classes.join(' ')}" data-date="${escAttr(dateKey)}" onclick="GoingsOn.eventsCalendar.toggleDayDetail('${escArg(dateKey)}')">`; | |
| 125 | + | html += `<div class="${classes.join(' ')}" data-date="${escAttr(dateKey)}" data-act="eventsCalendar.toggleDayDetail" data-a1="${escAttr(dateKey)}">`; | |
| 126 | 126 | html += `<div class="cal-month-cell-header"><span class="cal-day-number">${cursor.getDate()}</span></div>`; | |
| 127 | 127 | ||
| 128 | 128 | const maxShow = 3; | |
| 129 | 129 | dayEvents.slice(0, maxShow).forEach(e => { | |
| 130 | 130 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 131 | - | html += `<div class="cal-event-chip ${blockClass}" onclick="event.stopPropagation(); GoingsOn.events.open('${escArg(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`; | |
| 131 | + | html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`; | |
| 132 | 132 | }); | |
| 133 | 133 | if (dayEvents.length > maxShow) { | |
| 134 | 134 | html += `<div class="cal-event-more">+${dayEvents.length - maxShow} more</div>`; | |
| @@ -166,7 +166,7 @@ | |||
| 166 | 166 | } else { | |
| 167 | 167 | dayEvents.forEach(e => { | |
| 168 | 168 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 169 | - | html += `<div class="cal-day-detail-event ${blockClass}" onclick="GoingsOn.events.open('${escArg(e.id)}')"> | |
| 169 | + | html += `<div class="cal-day-detail-event ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}"> | |
| 170 | 170 | <span class="cal-detail-time">${esc(e.timeFormatted)}</span> | |
| 171 | 171 | <span class="cal-detail-title">${esc(e.title)}</span> | |
| 172 | 172 | ${e.location ? `<span class="cal-detail-location">${esc(e.location)}</span>` : ''} | |
| @@ -249,7 +249,7 @@ | |||
| 249 | 249 | html += '<div class="cal-week-allday-cell">'; | |
| 250 | 250 | dayEvts.forEach(e => { | |
| 251 | 251 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 252 | - | html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escArg(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`; | |
| 252 | + | html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`; | |
| 253 | 253 | }); | |
| 254 | 254 | html += '</div>'; | |
| 255 | 255 | adCursor.setDate(adCursor.getDate() + 1); | |
| @@ -293,7 +293,7 @@ | |||
| 293 | 293 | const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT); | |
| 294 | 294 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 295 | 295 | ||
| 296 | - | html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escArg(e.id)}')" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}"> | |
| 296 | + | html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" data-act="events.open" data-a1="${escAttr(e.id)}" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}"> | |
| 297 | 297 | <div class="cal-week-event-title">${esc(truncate(e.title, 20))}</div> | |
| 298 | 298 | <div class="cal-week-event-time">${esc(e.timeFormatted)}</div> | |
| 299 | 299 | </div>`; | |
| @@ -347,7 +347,7 @@ | |||
| 347 | 347 | html += '<div class="cal-mobile-allday">'; | |
| 348 | 348 | allDayEvts.forEach(e => { | |
| 349 | 349 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 350 | - | html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escArg(e.id)}')">${esc(truncate(e.title, 30))}</div>`; | |
| 350 | + | html += `<div class="cal-event-chip ${blockClass}" data-act="events.open" data-a1="${escAttr(e.id)}">${esc(truncate(e.title, 30))}</div>`; | |
| 351 | 351 | }); | |
| 352 | 352 | html += '</div>'; | |
| 353 | 353 | } | |
| @@ -374,7 +374,7 @@ | |||
| 374 | 374 | const topPx = ((startMinutes - HOURS_START * 60) / 15) * SLOT_HEIGHT; | |
| 375 | 375 | const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT); | |
| 376 | 376 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 377 | - | html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escArg(e.id)}')"> | |
| 377 | + | html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" data-act="events.open" data-a1="${escAttr(e.id)}"> | |
| 378 | 378 | <div class="cal-week-event-title">${esc(truncate(e.title, 30))}</div> | |
| 379 | 379 | <div class="cal-week-event-time">${esc(e.timeFormatted)}</div> | |
| 380 | 380 | </div>`; |
| @@ -305,7 +305,7 @@ | |||
| 305 | 305 | pastSection.classList.add('hidden'); | |
| 306 | 306 | recurringSection.classList.add('hidden'); | |
| 307 | 307 | if (futureHeading) futureHeading.classList.add('hidden'); | |
| 308 | - | upcomingContainer.innerHTML = GoingsOn.ui.renderEmptyState('No events scheduled.', 'Add Event', 'GoingsOn.events.openNew()', 'events'); | |
| 308 | + | upcomingContainer.innerHTML = GoingsOn.ui.renderEmptyState('No events scheduled.', 'Add Event', 'events.openNew', 'events'); | |
| 309 | 309 | [upcomingEventsScroller, pastEventsScroller, recurringEventsScroller].forEach(s => s && s.destroy()); | |
| 310 | 310 | upcomingEventsScroller = pastEventsScroller = recurringEventsScroller = null; | |
| 311 | 311 | return; | |
| @@ -397,7 +397,7 @@ | |||
| 397 | 397 | } | |
| 398 | 398 | GoingsOn.cache.markLoaded('events'); | |
| 399 | 399 | } catch (err) { | |
| 400 | - | upcomingContainer.innerHTML = `<div class="error-state error-state--padded">Failed to load events. <button class="btn-link" onclick="GoingsOn.cache.invalidate('events'); GoingsOn.events.load()">Try again</button></div>`; | |
| 400 | + | upcomingContainer.innerHTML = `<div class="error-state error-state--padded">Failed to load events. <button class="btn-link" data-act="events.reload">Try again</button></div>`; | |
| 401 | 401 | GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load events'), 'error', { | |
| 402 | 402 | action: { label: 'Retry', fn: () => { GoingsOn.cache.invalidate('events'); load(); } }, | |
| 403 | 403 | duration: 8000, | |
| @@ -424,15 +424,15 @@ | |||
| 424 | 424 | return ` | |
| 425 | 425 | <div class="event-row-virtual event-recurring" | |
| 426 | 426 | data-id="${escAttr(e.id)}" | |
| 427 | - | onclick="GoingsOn.events.open('${escArg(e.id)}')" | |
| 428 | - | oncontextmenu="GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')" | |
| 427 | + | data-act="events.open" data-a1="${escAttr(e.id)}" | |
| 428 | + | data-contextmenu="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}" | |
| 429 | 429 | tabindex="0" role="row"> | |
| 430 | 430 | <div class="event-cell event-cell-date"><span class="event-recurrence-pattern">${esc(patternLabel)}</span></div> | |
| 431 | 431 | <div class="event-cell event-cell-time">${e.timeFormatted}</div> | |
| 432 | 432 | <div class="event-cell event-cell-title">${esc(displayTitle)}</div> | |
| 433 | 433 | <div class="event-cell event-cell-location">${e.location ? esc(e.location) : '-'}</div> | |
| 434 | - | <div class="event-cell" style="text-align: right;" onclick="event.stopPropagation();"> | |
| 435 | - | <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')" title="Actions" aria-label="Event actions">⋮</button> | |
| 434 | + | <div class="event-cell" style="text-align: right;" data-act="ui.noop"> | |
| 435 | + | <button class="btn-icon kebab-btn" data-act="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}" title="Actions" aria-label="Event actions">⋮</button> | |
| 436 | 436 | </div> | |
| 437 | 437 | </div> | |
| 438 | 438 | `; | |
| @@ -460,12 +460,12 @@ | |||
| 460 | 460 | ${dateHeader} | |
| 461 | 461 | <div class="event-row-virtual ${e.isPast || isPast ? 'event-past' : ''}" | |
| 462 | 462 | data-id="${escAttr(e.id)}" | |
| 463 | - | onclick="GoingsOn.events.open('${escArg(e.id)}')" | |
| 464 | - | oncontextmenu="GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')" | |
| 463 | + | data-act="events.open" data-a1="${escAttr(e.id)}" | |
| 464 | + | data-contextmenu="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}" | |
| 465 | 465 | tabindex="0" role="row"> | |
| 466 | 466 | <div class="event-cell event-cell--shrink"> | |
| 467 | 467 | <input type="checkbox" class="bulk-checkbox event-select-cb" data-id="${escAttr(e.id)}" | |
| 468 | - | onclick="event.stopPropagation(); GoingsOn.events.toggleEventSelection('${escArg(e.id)}', event)" | |
| 468 | + | data-act="events.toggleEventSelection" data-a1="${escAttr(e.id)}" data-a2="@event" | |
| 469 | 469 | aria-label="Select event"> | |
| 470 | 470 | </div> | |
| 471 | 471 | <div class="event-cell event-cell-date"> | |
| @@ -475,8 +475,8 @@ | |||
| 475 | 475 | <div class="event-cell event-cell-time">${e.timeFormatted}</div> | |
| 476 | 476 | <div class="event-cell event-cell-title">${esc(displayTitle)}</div> | |
| 477 | 477 | <div class="event-cell event-cell-location">${e.location ? esc(e.location) : '-'}</div> | |
| 478 | - | <div class="event-cell" style="text-align: right;" onclick="event.stopPropagation();"> | |
| 479 | - | <button class="btn-icon kebab-btn" onclick="event.stopPropagation(); GoingsOn.contextMenus.showEvent(event, '${escArg(e.id)}')" title="Actions" aria-label="Event actions">⋮</button> | |
| 478 | + | <div class="event-cell" style="text-align: right;" data-act="ui.noop"> | |
| 479 | + | <button class="btn-icon kebab-btn" data-act="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}" title="Actions" aria-label="Event actions">⋮</button> | |
| 480 | 480 | </div> | |
| 481 | 481 | </div> | |
| 482 | 482 | `; | |
| @@ -581,8 +581,8 @@ | |||
| 581 | 581 | ? GoingsOn.snooze.formatTime(event.snoozedUntil) | |
| 582 | 582 | : null; | |
| 583 | 583 | const snoozeButton = event.isSnoozed | |
| 584 | - | ? `<button class="btn btn-secondary" onclick="GoingsOn.snooze.unsnooze('event', '${escArg(id)}')">Unsnooze</button>` | |
| 585 | - | : `<button class="btn btn-secondary" onclick="GoingsOn.snooze.openModal('event', '${escArg(id)}')">Snooze</button>`; | |
| 584 | + | ? `<button class="btn btn-secondary" data-act="snooze.unsnooze" data-a1="event" data-a2="${escAttr(id)}">Unsnooze</button>` | |
| 585 | + | : `<button class="btn btn-secondary" data-act="snooze.openModal" data-a1="event" data-a2="${escAttr(id)}">Snooze</button>`; | |
| 586 | 586 | const snoozeStatus = snoozeUntilLabel | |
| 587 | 587 | ? `<p><strong>Snoozed until:</strong> ${esc(snoozeUntilLabel)}</p>` | |
| 588 | 588 | : ''; | |
| @@ -603,11 +603,11 @@ | |||
| 603 | 603 | ${reminderStatus} | |
| 604 | 604 | </div> | |
| 605 | 605 | <div class="form-actions"> | |
| 606 | - | <button class="btn btn-secondary text-accent-red" onclick="GoingsOn.events.delete('${escArg(id)}')">Delete</button> | |
| 606 | + | <button class="btn btn-secondary text-accent-red" data-act="events.delete" data-a1="${escAttr(id)}">Delete</button> | |
| 607 | 607 | <div class="form-actions-spacer"></div> | |
| 608 | 608 | ${snoozeButton} | |
| 609 | - | <button class="btn btn-secondary" onclick="GoingsOn.events.openEdit('${escArg(id)}')">Edit</button> | |
| 610 | - | <button class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button> | |
| 609 | + | <button class="btn btn-secondary" data-act="events.openEdit" data-a1="${escAttr(id)}">Edit</button> | |
| 610 | + | <button class="btn btn-secondary" data-act="ui.closeModal">Close</button> | |
| 611 | 611 | </div> | |
| 612 | 612 | `; | |
| 613 | 613 | GoingsOn.ui.openModal('Event Details', content); | |
| @@ -802,6 +802,8 @@ | |||
| 802 | 802 | ||
| 803 | 803 | GoingsOn.events = { | |
| 804 | 804 | load, | |
| 805 | + | // Invalidate the events cache and reload (error-retry button). | |
| 806 | + | reload: () => { GoingsOn.cache.invalidate('events'); load(); }, | |
| 805 | 807 | onFilterChange, | |
| 806 | 808 | openNew, | |
| 807 | 809 | openNewForProject, |
| @@ -119,8 +119,8 @@ | |||
| 119 | 119 | <div class="backup-item-meta">${dateStr} - ${sizeStr}</div> | |
| 120 | 120 | </div> | |
| 121 | 121 | <div class="backup-item-actions"> | |
| 122 | - | <button class="btn btn-sm btn-secondary" onclick="GoingsOn.export.restoreFromBackup('${escArg(backup.filePath)}')">Restore</button> | |
| 123 | - | <button class="btn btn-sm btn-danger" onclick="GoingsOn.export.deleteBackup('${escArg(backup.filePath)}')">Delete</button> | |
| 122 | + | <button class="btn btn-sm btn-secondary" data-act="export.restoreFromBackup" data-a1="${escAttr(backup.filePath)}">Restore</button> | |
| 123 | + | <button class="btn btn-sm btn-danger" data-act="export.deleteBackup" data-a1="${escAttr(backup.filePath)}">Delete</button> | |
| 124 | 124 | </div> | |
| 125 | 125 | </div> | |
| 126 | 126 | `; | |
| @@ -132,11 +132,11 @@ | |||
| 132 | 132 | </div> | |
| 133 | 133 | ||
| 134 | 134 | <div class="form-actions" style="margin-top: 1.5rem;"> | |
| 135 | - | <button class="btn btn-secondary" onclick="GoingsOn.export.createBackup(); GoingsOn.ui.closeModal(); setTimeout(() => GoingsOn.export.openBackupsModal(), 500);"> | |
| 135 | + | <button class="btn btn-secondary" data-act="export.backupThenManage"> | |
| 136 | 136 | Create New Backup | |
| 137 | 137 | </button> | |
| 138 | 138 | <div class="flex-1"></div> | |
| 139 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button> | |
| 139 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModal">Close</button> | |
| 140 | 140 | </div> | |
| 141 | 141 | `; | |
| 142 | 142 | ||
| @@ -283,8 +283,8 @@ | |||
| 283 | 283 | </div> | |
| 284 | 284 | ||
| 285 | 285 | <div class="form-actions form-actions--spaced"> | |
| 286 | - | <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.settings.open();">Cancel</button> | |
| 287 | - | <button type="button" class="btn btn-primary" onclick="GoingsOn.export.saveBackupSettings()">Save Settings</button> | |
| 286 | + | <button type="button" class="btn btn-secondary" data-act="ui.closeModalThen" data-a1="settings.open">Cancel</button> | |
| 287 | + | <button type="button" class="btn btn-primary" data-act="export.saveBackupSettings">Save Settings</button> | |
| 288 | 288 | </div> | |
| 289 | 289 | `; | |
| 290 | 290 | ||
| @@ -320,6 +320,12 @@ | |||
| 320 | 320 | exportEventsICS, | |
| 321 | 321 | createBackup, | |
| 322 | 322 | openBackupsModal, | |
| 323 | + | // Create a backup, close the modal, then reopen the backups list. | |
| 324 | + | backupThenManage: async () => { | |
| 325 | + | await createBackup(); | |
| 326 | + | GoingsOn.ui.closeModal(); | |
| 327 | + | setTimeout(() => openBackupsModal(), 500); | |
| 328 | + | }, | |
| 323 | 329 | restoreFromBackup, | |
| 324 | 330 | deleteBackup, | |
| 325 | 331 | openBackupSettingsModal, |
| @@ -76,7 +76,7 @@ function openFormModal(config) { | |||
| 76 | 76 | if (hasExtended && field.extended && !inExtended) { | |
| 77 | 77 | inExtended = true; | |
| 78 | 78 | const expanded = extendedHasValues ? 'expanded' : ''; | |
| 79 | - | prefix = `<button type="button" class="form-more-toggle ${expanded}" onclick="this.classList.toggle('expanded'); this.nextElementSibling.classList.toggle('hidden'); this.textContent = this.classList.contains('expanded') ? 'Less options' : 'More options';">${extendedHasValues ? 'Less options' : 'More options'}</button><div class="form-extended-fields ${extendedHasValues ? '' : 'hidden'}">`; | |
| 79 | + | prefix = `<button type="button" class="form-more-toggle ${expanded}" data-act="ui.toggleExpand" data-a1="@el" data-text-expanded="Less options" data-text-collapsed="More options">${extendedHasValues ? 'Less options' : 'More options'}</button><div class="form-extended-fields ${extendedHasValues ? '' : 'hidden'}">`; | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | 82 | const groupHtml = GoingsOn.ui.renderFormField({ |