/** * Sticky Notes App - Offline Web Application * A complete sticky notes application with local storage persistence */ // Global namespace for the app const StickyApp = (function() { 'use strict'; // Utility functions const Utils = { /** * Generate a UUID v4 * @returns {string} UUID */ generateUUID: function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, /** * Debounce function to limit how often a function can be called * @param {Function} func - Function to debounce * @param {number} wait - Wait time in milliseconds * @returns {Function} Debounced function */ debounce: function(func, wait) { let timeout; return function(...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; }, /** * Sanitize HTML to prevent XSS * @param {string} str - String to sanitize * @returns {string} Sanitized string */ sanitizeHTML: function(str) { const temp = document.createElement('div'); temp.textContent = str; return temp.innerHTML; }, /** * Extract tags from text content * @param {string} content - Text content * @returns {Array} Array of tags */ extractTags: function(content) { const tagRegex = /#(\w+)/g; const tags = []; let match; while ((match = tagRegex.exec(content)) !== null) { tags.push(match[1]); } return [...new Set(tags)]; // Remove duplicates }, /** * Format date for display * @param {Date|string} date - Date to format * @returns {string} Formatted date */ formatDate: function(date) { const d = new Date(date); return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); }, /** * Show notification * @param {string} message - Message to display * @param {string} type - Type of notification (success, error, info) */ showNotification: function(message, type = 'info') { const container = document.getElementById('notification-container'); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; container.appendChild(notification); // Trigger animation setTimeout(() => { notification.classList.add('show'); }, 10); // Remove after 3 seconds setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { container.removeChild(notification); }, 300); }, 3000); } }; /** * Note model class */ class Note { constructor(data = {}) { this.id = data.id || Utils.generateUUID(); this.content = data.content || ''; this.x = data.x || 20 + Math.random() * 200; this.y = data.y || 20 + Math.random() * 200; this.width = data.width || 250; this.height = data.height || 250; this.color = data.color || '#fff9a8'; this.zIndex = data.zIndex || 10; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); this.pinned = data.pinned || false; this.tags = data.tags || []; this.reminder = data.reminder || null; this.category = data.category || 'Uncategorized'; this.isMarkdown = data.isMarkdown !== undefined ? data.isMarkdown : false; this.kanbanStatus = data.kanbanStatus || 'todo'; } /** * Update note content and extract tags * @param {string} content - New content */ updateContent(content) { this.content = content; this.tags = Utils.extractTags(content); this.updatedAt = new Date().toISOString(); } /** * Convert note to JSON * @returns {Object} Note data */ toJSON() { return { id: this.id, content: this.content, x: this.x, y: this.y, width: this.width, height: this.height, color: this.color, zIndex: this.zIndex, createdAt: this.createdAt, updatedAt: this.updatedAt, pinned: this.pinned, tags: this.tags, reminder: this.reminder, category: this.category, isMarkdown: this.isMarkdown, kanbanStatus: this.kanbanStatus }; } /** * Create note from JSON * @param {Object} data - Note data * @returns {Note} Note instance */ static fromJSON(data) { return new Note(data); } } /** * Storage manager class for handling localStorage */ class StorageManager { constructor(key = 'stickyNotesApp') { this.key = key; this.maxRecycleBinSize = 10; } /** * Save all data to localStorage * @param {Object} data - Data to save */ save(data) { try { localStorage.setItem(this.key, JSON.stringify(data)); return true; } catch (error) { if (error.name === 'QuotaExceededError') { Utils.showNotification('Storage quota exceeded. Please export and delete some notes.', 'error'); } else { Utils.showNotification('Failed to save data.', 'error'); } return false; } } /** * Load all data from localStorage * @returns {Object} Loaded data */ load() { try { const data = localStorage.getItem(this.key); return data ? JSON.parse(data) : this.getEmptyData(); } catch (error) { Utils.showNotification('Failed to load data.', 'error'); return this.getEmptyData(); } } /** * Get empty data structure * @returns {Object} Empty data structure */ getEmptyData() { return { notes: [], settings: { theme: 'light', lastZIndex: 10, nextId: 1 }, recycleBin: [] }; } /** * Export data as JSON string * @returns {string} JSON string */ export() { const data = this.load(); return JSON.stringify(data, null, 2); } /** * Import data from JSON string * @param {string} jsonString - JSON string to import * @param {string} mode - Import mode ('replace' or 'merge') * @returns {boolean} Success status */ import(jsonString, mode = 'merge') { try { const importedData = JSON.parse(jsonString); // Validate data structure if (!importedData.notes || !Array.isArray(importedData.notes)) { throw new Error('Invalid data format'); } const currentData = this.load(); if (mode === 'replace') { currentData.notes = importedData.notes; } else { // Merge notes, avoiding duplicates by ID const existingIds = new Set(currentData.notes.map(note => note.id)); const newNotes = importedData.notes.filter(note => !existingIds.has(note.id)); currentData.notes.push(...newNotes); } // Update settings if available if (importedData.settings) { currentData.settings = {...currentData.settings, ...importedData.settings}; } return this.save(currentData); } catch (error) { Utils.showNotification('Failed to import data: ' + error.message, 'error'); return false; } } /** * Add note to recycle bin * @param {Note} note - Note to add to recycle bin */ addToRecycleBin(note) { const data = this.load(); data.recycleBin.unshift(note.toJSON()); // Limit recycle bin size if (data.recycleBin.length > this.maxRecycleBinSize) { data.recycleBin = data.recycleBin.slice(0, this.maxRecycleBinSize); } this.save(data); } /** * Restore note from recycle bin * @param {string} noteId - ID of note to restore * @returns {Note|null} Restored note or null */ restoreFromRecycleBin(noteId) { const data = this.load(); const noteIndex = data.recycleBin.findIndex(note => note.id === noteId); if (noteIndex !== -1) { const note = data.recycleBin.splice(noteIndex, 1)[0]; data.notes.push(note); this.save(data); return Note.fromJSON(note); } return null; } /** * Empty recycle bin */ emptyRecycleBin() { const data = this.load(); data.recycleBin = []; this.save(data); } } /** * UI Manager class for handling DOM operations */ class UIManager { constructor(boardElement) { this.board = boardElement; this.notes = new Map(); // Map of note ID to DOM element this.selectedNoteId = null; this.isDragging = false; this.isResizing = false; this.dragOffset = {x: 0, y: 0}; this.activeColorPicker = null; this.activeMenu = null; this.setupEventListeners(); } /** * Setup global event listeners */ setupEventListeners() { // Close color pickers and menus when clicking outside document.addEventListener('click', (e) => { if (this.activeColorPicker && !this.activeColorPicker.contains(e.target)) { this.activeColorPicker.remove(); this.activeColorPicker = null; } if (this.activeMenu && !this.activeMenu.contains(e.target)) { this.activeMenu.remove(); this.activeMenu = null; } }); // Handle window resize window.addEventListener('resize', () => { this.adjustNotesPosition(); }); } /** * Render a note * @param {Note} note - Note to render * @returns {HTMLElement} Note DOM element */ renderNote(note) { // Check if note already exists if (this.notes.has(note.id)) { return this.updateNote(note); } // Create note element const noteEl = document.createElement('div'); noteEl.className = 'note'; noteEl.id = `note-${note.id}`; noteEl.style.left = `${note.x}px`; noteEl.style.top = `${note.y}px`; noteEl.style.width = `${note.width}px`; noteEl.style.height = `${note.height}px`; noteEl.style.backgroundColor = note.color; noteEl.style.zIndex = note.zIndex; if (note.pinned) { noteEl.classList.add('pinned'); } // Reminder indicator if (note.reminder && new Date(note.reminder) > new Date()) { const reminderIndicator = document.createElement('div'); reminderIndicator.className = 'reminder-indicator'; reminderIndicator.innerHTML = 'alarm' + new Date(note.reminder).toLocaleDateString(); noteEl.appendChild(reminderIndicator); } // Create note header const header = document.createElement('div'); header.className = 'note-header'; // Drag handle const dragHandle = document.createElement('div'); dragHandle.className = 'drag-handle'; dragHandle.innerHTML = ''; dragHandle.setAttribute('aria-label', 'Drag to move'); // Note actions const actions = document.createElement('div'); actions.className = 'note-actions'; // Pin button const pinBtn = document.createElement('button'); pinBtn.className = 'note-action pin-btn'; pinBtn.innerHTML = note.pinned ? 'push_pin' : 'push_pin'; if (note.pinned) pinBtn.classList.add('active'); pinBtn.setAttribute('aria-label', note.pinned ? 'Unpin note' : 'Pin note'); // Markdown toggle button const markdownBtn = document.createElement('button'); markdownBtn.className = 'note-action markdown-btn'; markdownBtn.innerHTML = 'code'; if (note.isMarkdown) markdownBtn.classList.add('active'); markdownBtn.setAttribute('aria-label', 'Toggle markdown'); // Color picker button const colorBtn = document.createElement('button'); colorBtn.className = 'note-action color-btn'; colorBtn.innerHTML = ''; colorBtn.setAttribute('aria-label', 'Change color'); // Menu button const menuBtn = document.createElement('button'); menuBtn.className = 'note-action menu-btn'; menuBtn.innerHTML = ''; menuBtn.setAttribute('aria-label', 'Menu'); actions.appendChild(pinBtn); actions.appendChild(markdownBtn); actions.appendChild(colorBtn); actions.appendChild(menuBtn); header.appendChild(dragHandle); header.appendChild(actions); // Rich text toolbar const toolbar = document.createElement('div'); toolbar.className = 'rich-text-toolbar'; toolbar.style.display = 'none'; const toolbarButtons = [ {cmd: 'bold', icon: 'B', title: 'Bold'}, {cmd: 'italic', icon: 'I', title: 'Italic'}, {cmd: 'underline', icon: 'U', title: 'Underline'}, {cmd: 'strikeThrough', icon: 'S', title: 'Strikethrough'}, {cmd: 'insertUnorderedList', icon: '•', title: 'Bullet List'}, {cmd: 'insertOrderedList', icon: '1.', title: 'Numbered List'} ]; toolbarButtons.forEach(btn => { const button = document.createElement('button'); button.className = 'toolbar-btn'; button.textContent = btn.icon; button.title = btn.title; button.setAttribute('data-command', btn.cmd); toolbar.appendChild(button); }); // Create note content const content = document.createElement('div'); content.className = 'note-content'; content.contentEditable = true; if (note.isMarkdown && note.content) { content.innerHTML = marked.parse(note.content); content.classList.add('preview-mode'); content.contentEditable = false; } else { content.textContent = note.content; } content.setAttribute('role', 'textbox'); content.setAttribute('aria-label', 'Note content'); // Create note footer const footer = document.createElement('div'); footer.className = 'note-footer'; const timestamp = document.createElement('div'); timestamp.className = 'note-timestamp'; timestamp.textContent = Utils.formatDate(note.updatedAt); const tags = document.createElement('div'); tags.className = 'note-tags'; // Category badge if (note.category && note.category !== 'Uncategorized') { const categoryBadge = document.createElement('span'); categoryBadge.className = 'category-badge'; categoryBadge.textContent = note.category; tags.appendChild(categoryBadge); } note.tags.forEach(tag => { const tagEl = document.createElement('span'); tagEl.className = 'tag'; tagEl.textContent = `#${tag}`; tags.appendChild(tagEl); }); footer.appendChild(timestamp); footer.appendChild(tags); // Create resize handle const resizeHandle = document.createElement('div'); resizeHandle.className = 'resize-handle'; // Assemble note noteEl.appendChild(header); noteEl.appendChild(toolbar); noteEl.appendChild(content); noteEl.appendChild(footer); noteEl.appendChild(resizeHandle); // Add event listeners this.setupNoteEventListeners(noteEl, note); // Add to board and track this.board.appendChild(noteEl); this.notes.set(note.id, noteEl); return noteEl; } /** * Update an existing note * @param {Note} note - Note to update * @returns {HTMLElement} Updated note DOM element */ updateNote(note) { const noteEl = this.notes.get(note.id); if (!noteEl) return null; // Update position and size noteEl.style.left = `${note.x}px`; noteEl.style.top = `${note.y}px`; noteEl.style.width = `${note.width}px`; noteEl.style.height = `${note.height}px`; noteEl.style.backgroundColor = note.color; noteEl.style.zIndex = note.zIndex; // Update pinned state if (note.pinned) { noteEl.classList.add('pinned'); } else { noteEl.classList.remove('pinned'); } // Update content const contentEl = noteEl.querySelector('.note-content'); contentEl.textContent = note.content; // Update tags const tagsEl = noteEl.querySelector('.note-tags'); tagsEl.innerHTML = ''; note.tags.forEach(tag => { const tagEl = document.createElement('span'); tagEl.className = 'tag'; tagEl.textContent = `#${tag}`; tagsEl.appendChild(tagEl); }); // Update timestamp const timestampEl = noteEl.querySelector('.note-timestamp'); timestampEl.textContent = Utils.formatDate(note.updatedAt); // Update pin button const pinBtn = noteEl.querySelector('.pin-btn'); pinBtn.innerHTML = 'push_pin'; if (note.pinned) pinBtn.classList.add('active'); else pinBtn.classList.remove('active'); pinBtn.setAttribute('aria-label', note.pinned ? 'Unpin note' : 'Pin note'); return noteEl; } /** * Setup event listeners for a note * @param {HTMLElement} noteEl - Note DOM element * @param {Note} note - Note model */ setupNoteEventListeners(noteEl, note) { const header = noteEl.querySelector('.note-header'); const content = noteEl.querySelector('.note-content'); const toolbar = noteEl.querySelector('.rich-text-toolbar'); const pinBtn = noteEl.querySelector('.pin-btn'); const markdownBtn = noteEl.querySelector('.markdown-btn'); const colorBtn = noteEl.querySelector('.color-btn'); const menuBtn = noteEl.querySelector('.menu-btn'); const resizeHandle = noteEl.querySelector('.resize-handle'); // Note selection noteEl.addEventListener('mousedown', (e) => { if (e.target === noteEl || e.target === header || e.target === header.querySelector('.drag-handle')) { this.selectNote(note.id); } }); // Drag functionality header.addEventListener('pointerdown', (e) => { // If the pointerdown originated from any action button (pin, color, menu, etc.) // don't start dragging. Use closest() to handle clicks on inner elements // (icons/text nodes) inside the button rather than relying on strict // reference equality which can fail for nested targets. if (e.target.closest && e.target.closest('.note-action')) return; this.isDragging = true; this.dragOffset.x = e.clientX - note.x; this.dragOffset.y = e.clientY - note.y; const handlePointerMove = (e) => { if (!this.isDragging) return; const newX = e.clientX - this.dragOffset.x; const newY = e.clientY - this.dragOffset.y; // Keep note within viewport const maxX = window.innerWidth - note.width; const maxY = window.innerHeight - note.height - 80; // Account for header note.x = Math.max(0, Math.min(newX, maxX)); note.y = Math.max(0, Math.min(newY, maxY)); noteEl.style.left = `${note.x}px`; noteEl.style.top = `${note.y}px`; }; const handlePointerUp = () => { this.isDragging = false; document.removeEventListener('pointermove', handlePointerMove); document.removeEventListener('pointerup', handlePointerUp); // Trigger save this.onNoteChange(note); }; document.addEventListener('pointermove', handlePointerMove); document.addEventListener('pointerup', handlePointerUp); }); // Resize functionality resizeHandle.addEventListener('pointerdown', (e) => { this.isResizing = true; const startX = e.clientX; const startY = e.clientY; const startWidth = note.width; const startHeight = note.height; const handlePointerMove = (e) => { if (!this.isResizing) return; const newWidth = startWidth + (e.clientX - startX); const newHeight = startHeight + (e.clientY - startY); // Minimum size note.width = Math.max(150, newWidth); note.height = Math.max(150, newHeight); noteEl.style.width = `${note.width}px`; noteEl.style.height = `${note.height}px`; }; const handlePointerUp = () => { this.isResizing = false; document.removeEventListener('pointermove', handlePointerMove); document.removeEventListener('pointerup', handlePointerUp); // Trigger save this.onNoteChange(note); }; document.addEventListener('pointermove', handlePointerMove); document.addEventListener('pointerup', handlePointerUp); }); // Content change content.addEventListener('input', Utils.debounce(() => { if (note.isMarkdown) { note.updateContent(content.textContent); } else { note.updateContent(content.textContent); } this.updateNote(note); this.onNoteChange(note); }, 500)); // Show/hide toolbar on focus content.addEventListener('focus', () => { if (!note.isMarkdown) { toolbar.style.display = 'flex'; } }); content.addEventListener('blur', () => { setTimeout(() => { if (!toolbar.contains(document.activeElement)) { toolbar.style.display = 'none'; } }, 200); }); // Toolbar buttons toolbar.querySelectorAll('.toolbar-btn').forEach(btn => { btn.addEventListener('mousedown', (e) => { e.preventDefault(); const command = btn.getAttribute('data-command'); document.execCommand(command, false, null); content.focus(); }); }); // Pin toggle pinBtn.addEventListener('click', () => { note.pinned = !note.pinned; this.updateNote(note); this.onNoteChange(note); }); // Markdown toggle markdownBtn.addEventListener('click', () => { note.isMarkdown = !note.isMarkdown; if (note.isMarkdown) { // Switch to markdown preview mode const plainText = content.textContent; note.content = plainText; content.innerHTML = marked.parse(plainText); content.classList.add('preview-mode'); content.contentEditable = false; toolbar.style.display = 'none'; markdownBtn.classList.add('active'); } else { // Switch back to edit mode const plainText = note.content; content.textContent = plainText; content.classList.remove('preview-mode'); content.contentEditable = true; markdownBtn.classList.remove('active'); } this.onNoteChange(note); }); // Color picker (render as floating element attached to body to avoid clipping) colorBtn.addEventListener('click', (e) => { e.stopPropagation(); // Close existing color picker if (this.activeColorPicker) { this.activeColorPicker.remove(); this.activeColorPicker = null; } // Create color picker and append to body const colorPicker = this.createColorPicker(note); document.body.appendChild(colorPicker); this.positionFloatingElement(colorPicker, colorBtn); colorPicker.style.display = 'flex'; this.activeColorPicker = colorPicker; // Prevent the header drag from starting when clicking inside the picker colorPicker.addEventListener('pointerdown', (ev) => ev.stopPropagation()); }); // Menu (render as floating element attached to body to avoid clipping) menuBtn.addEventListener('click', (e) => { e.stopPropagation(); // Close existing menu if (this.activeMenu) { this.activeMenu.remove(); this.activeMenu = null; } // Create menu and append to body const menu = this.createNoteMenu(note); document.body.appendChild(menu); this.positionFloatingElement(menu, menuBtn); menu.style.display = 'block'; this.activeMenu = menu; // Prevent the header drag from starting when clicking inside the menu menu.addEventListener('pointerdown', (ev) => ev.stopPropagation()); }); } /** * Create color picker for a note * @param {Note} note - Note to create color picker for * @returns {HTMLElement} Color picker element */ createColorPicker(note) { const colorPicker = document.createElement('div'); colorPicker.className = 'color-picker'; const colors = ['#fff9a8', '#ffebcc', '#d4f0f0', '#ffd6d6', '#e6ccff', '#ccffcc']; colors.forEach(color => { const colorOption = document.createElement('div'); colorOption.className = 'color-option'; colorOption.style.backgroundColor = color; colorOption.setAttribute('aria-label', `Select color ${color}`); colorOption.addEventListener('click', () => { note.color = color; this.updateNote(note); this.onNoteChange(note); colorPicker.style.display = 'none'; this.activeColorPicker = null; }); colorPicker.appendChild(colorOption); }); return colorPicker; } /** * Position a floating element (appended to body) next to a reference element * and keep it inside the viewport. * @param {HTMLElement} floatingEl - Element appended to body * @param {HTMLElement} referenceEl - Element to position next to */ positionFloatingElement(floatingEl, referenceEl) { const refRect = referenceEl.getBoundingClientRect(); const floatRect = floatingEl.getBoundingClientRect(); // Default position: below and aligned to right of reference let top = refRect.bottom + window.scrollY + 4; // small gap let left = refRect.right + window.scrollX - floatRect.width; // If it goes off the right edge, align to the left of the reference if (left + floatRect.width > window.innerWidth + window.scrollX) { left = refRect.left + window.scrollX; } // If it goes off the bottom edge, position above reference if (top + floatRect.height > window.innerHeight + window.scrollY) { top = refRect.top + window.scrollY - floatRect.height - 4; } // Clamp to viewport top = Math.max(window.scrollY + 4, Math.min(top, window.scrollY + window.innerHeight - floatRect.height - 4)); left = Math.max(window.scrollX + 4, Math.min(left, window.scrollX + window.innerWidth - floatRect.width - 4)); floatingEl.style.position = 'absolute'; floatingEl.style.top = `${top}px`; floatingEl.style.left = `${left}px`; floatingEl.style.zIndex = 2000; } /** * Create menu for a note * @param {Note} note - Note to create menu for * @returns {HTMLElement} Menu element */ createNoteMenu(note) { const menu = document.createElement('div'); menu.className = 'note-menu'; // Set reminder option const reminderOption = document.createElement('div'); reminderOption.className = 'menu-item'; reminderOption.textContent = 'Set Reminder'; reminderOption.addEventListener('click', () => { this.onSetReminder(note); menu.style.display = 'none'; this.activeMenu = null; }); // Set category option const categoryOption = document.createElement('div'); categoryOption.className = 'menu-item'; categoryOption.textContent = 'Set Category'; categoryOption.addEventListener('click', () => { this.onSetCategory(note); menu.style.display = 'none'; this.activeMenu = null; }); // Duplicate option const duplicateOption = document.createElement('div'); duplicateOption.className = 'menu-item'; duplicateOption.textContent = 'Duplicate'; duplicateOption.addEventListener('click', () => { this.onNoteDuplicate(note); menu.style.display = 'none'; this.activeMenu = null; }); // Delete option const deleteOption = document.createElement('div'); deleteOption.className = 'menu-item'; deleteOption.textContent = 'Delete'; deleteOption.addEventListener('click', () => { this.onNoteDelete(note); menu.style.display = 'none'; this.activeMenu = null; }); // Export option const exportOption = document.createElement('div'); exportOption.className = 'menu-item'; exportOption.textContent = 'Export'; exportOption.addEventListener('click', () => { this.onNoteExport(note); menu.style.display = 'none'; this.activeMenu = null; }); menu.appendChild(reminderOption); menu.appendChild(categoryOption); menu.appendChild(duplicateOption); menu.appendChild(deleteOption); menu.appendChild(exportOption); return menu; } /** * Select a note * @param {string} noteId - ID of note to select */ selectNote(noteId) { // Deselect previous note if (this.selectedNoteId) { const prevNoteEl = this.notes.get(this.selectedNoteId); if (prevNoteEl) { prevNoteEl.classList.remove('selected'); } } // Select new note this.selectedNoteId = noteId; const noteEl = this.notes.get(noteId); if (noteEl) { noteEl.classList.add('selected'); this.onNoteSelect(noteId); } } /** * Remove a note from the DOM * @param {string} noteId - ID of note to remove */ removeNote(noteId) { const noteEl = this.notes.get(noteId); if (noteEl) { this.board.removeChild(noteEl); this.notes.delete(noteId); if (this.selectedNoteId === noteId) { this.selectedNoteId = null; } } } /** * Clear all notes from the DOM */ clearAllNotes() { this.notes.forEach(noteEl => { this.board.removeChild(noteEl); }); this.notes.clear(); this.selectedNoteId = null; } /** * Filter notes based on search query * @param {string} query - Search query */ filterNotes(query) { const lowerQuery = query.toLowerCase(); this.notes.forEach((noteEl, noteId) => { const note = this.onGetNote(noteId); if (!note) return; const matchesContent = note.content.toLowerCase().includes(lowerQuery); const matchesTags = note.tags.some(tag => tag.toLowerCase().includes(lowerQuery)); if (matchesContent || matchesTags) { noteEl.style.display = 'flex'; } else { noteEl.style.display = 'none'; } }); } /** * Show all notes */ showAllNotes() { this.notes.forEach(noteEl => { noteEl.style.display = 'flex'; }); } /** * Adjust notes position to fit within viewport */ adjustNotesPosition() { this.notes.forEach((noteEl, noteId) => { const note = this.onGetNote(noteId); if (!note) return; const maxX = window.innerWidth - note.width; const maxY = window.innerHeight - note.height - 80; // Account for header if (note.x > maxX) { note.x = maxX; noteEl.style.left = `${note.x}px`; } if (note.y > maxY) { note.y = maxY; noteEl.style.top = `${note.y}px`; } }); } /** * Set theme * @param {string} theme - Theme name ('light' or 'dark') */ setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); } /** * Show recycle bin modal * @param {Array} deletedNotes - Array of deleted notes */ showRecycleBin(deletedNotes) { const modal = document.getElementById('recycle-bin-modal'); const content = document.getElementById('recycle-bin-content'); content.innerHTML = ''; if (deletedNotes.length === 0) { content.innerHTML = '
🗑️

Recycle bin is empty

'; } else { deletedNotes.forEach(noteData => { const note = Note.fromJSON(noteData); const noteEl = document.createElement('div'); noteEl.className = 'deleted-note'; const noteContent = document.createElement('div'); noteContent.className = 'deleted-note-content'; noteContent.textContent = note.content.substring(0, 100) + (note.content.length > 100 ? '...' : ''); const noteActions = document.createElement('div'); noteActions.className = 'deleted-note-actions'; const restoreBtn = document.createElement('button'); restoreBtn.className = 'btn btn-primary'; restoreBtn.textContent = 'Restore'; restoreBtn.addEventListener('click', () => { this.onRestoreNote(note.id); }); noteActions.appendChild(restoreBtn); noteEl.appendChild(noteContent); noteEl.appendChild(noteActions); content.appendChild(noteEl); }); } modal.classList.add('active'); modal.setAttribute('aria-hidden', 'false'); } /** * Hide recycle bin modal */ hideRecycleBin() { const modal = document.getElementById('recycle-bin-modal'); modal.classList.remove('active'); modal.setAttribute('aria-hidden', 'true'); } /** * Show help modal */ showHelp() { const modal = document.getElementById('help-modal'); modal.classList.add('active'); modal.setAttribute('aria-hidden', 'false'); } /** * Hide help modal */ hideHelp() { const modal = document.getElementById('help-modal'); modal.classList.remove('active'); modal.setAttribute('aria-hidden', 'true'); } /** * Callback for when a note changes * @param {Note} note - Changed note */ onNoteChange(note) { // This will be overridden by the AppController } /** * Callback for when a note is selected * @param {string} noteId - Selected note ID */ onNoteSelect(noteId) { // This will be overridden by the AppController } /** * Callback for when a note is deleted * @param {Note} note - Deleted note */ onNoteDelete(note) { // This will be overridden by the AppController } /** * Callback for when a note is duplicated * @param {Note} note - Note to duplicate */ onNoteDuplicate(note) { // This will be overridden by the AppController } /** * Callback for when a note is exported * @param {Note} note - Note to export */ onNoteExport(note) { // This will be overridden by the AppController } /** * Callback for when a note is restored * @param {string} noteId - ID of note to restore */ onRestoreNote(noteId) { // This will be overridden by the AppController } /** * Callback for getting a note by ID * @param {string} noteId - ID of note to get * @returns {Note|null} Note or null */ onGetNote(noteId) { // This will be overridden by the AppController return null; } /** * Callback for setting reminder * @param {Note} note - Note to set reminder for */ onSetReminder(note) { // This will be overridden by the AppController } /** * Callback for setting category * @param {Note} note - Note to set category for */ onSetCategory(note) { // This will be overridden by the AppController } /** * Render Kanban board * @param {Array} notes - Array of notes */ renderKanban(notes) { const todoColumn = document.getElementById('kanban-todo'); const inProgressColumn = document.getElementById('kanban-in-progress'); const doneColumn = document.getElementById('kanban-done'); // Clear columns todoColumn.innerHTML = ''; inProgressColumn.innerHTML = ''; doneColumn.innerHTML = ''; // Count notes in each column let todoCount = 0; let inProgressCount = 0; let doneCount = 0; notes.forEach(note => { const card = this.createKanbanCard(note); if (note.kanbanStatus === 'todo') { todoColumn.appendChild(card); todoCount++; } else if (note.kanbanStatus === 'in-progress') { inProgressColumn.appendChild(card); inProgressCount++; } else if (note.kanbanStatus === 'done') { doneColumn.appendChild(card); doneCount++; } }); // Update counts document.querySelector('[data-status="todo"] .kanban-count').textContent = todoCount; document.querySelector('[data-status="in-progress"] .kanban-count').textContent = inProgressCount; document.querySelector('[data-status="done"] .kanban-count').textContent = doneCount; } /** * Create Kanban card * @param {Note} note - Note to create card for * @returns {HTMLElement} Kanban card element */ createKanbanCard(note) { const card = document.createElement('div'); card.className = 'kanban-card'; card.setAttribute('data-note-id', note.id); card.style.borderLeftColor = note.color; card.draggable = true; const header = document.createElement('div'); header.className = 'kanban-card-header'; if (note.category && note.category !== 'Uncategorized') { const categoryBadge = document.createElement('span'); categoryBadge.className = 'category-badge'; categoryBadge.textContent = note.category; header.appendChild(categoryBadge); } const content = document.createElement('div'); content.className = 'kanban-card-content'; if (note.isMarkdown) { content.innerHTML = marked.parse(note.content.substring(0, 200)); } else { content.textContent = note.content.substring(0, 200) + (note.content.length > 200 ? '...' : ''); } const footer = document.createElement('div'); footer.className = 'kanban-card-footer'; const timestamp = document.createElement('span'); timestamp.textContent = Utils.formatDate(note.updatedAt); const tags = document.createElement('div'); tags.className = 'note-tags'; note.tags.slice(0, 2).forEach(tag => { const tagEl = document.createElement('span'); tagEl.className = 'tag'; tagEl.textContent = `#${tag}`; tags.appendChild(tagEl); }); footer.appendChild(timestamp); footer.appendChild(tags); card.appendChild(header); card.appendChild(content); card.appendChild(footer); // Drag events card.addEventListener('dragstart', (e) => { card.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', note.id); }); card.addEventListener('dragend', () => { card.classList.remove('dragging'); }); // Click to edit card.addEventListener('click', () => { this.onKanbanCardClick(note.id); }); return card; } /** * Callback for Kanban card click * @param {string} noteId - ID of note clicked */ onKanbanCardClick(noteId) { // This will be overridden by the AppController } } /** * App Controller class to tie everything together */ class AppController { constructor() { this.storage = new StorageManager(); this.ui = new UIManager(document.getElementById('board')); this.notes = new Map(); // Map of note ID to Note model this.settings = {}; this.recycleBin = []; this.lastZIndex = 10; this.currentView = 'sticky'; // 'sticky' or 'kanban' this.categories = new Set(['Uncategorized']); this.templates = this.getTemplates(); this.setupUICallbacks(); this.setupGlobalEventListeners(); this.loadData(); this.updateCategoryFilter(); this.checkReminders(); } /** * Setup UI callbacks */ setupUICallbacks() { // Note change callback this.ui.onNoteChange = (note) => { this.saveNote(note); }; // Note select callback this.ui.onNoteSelect = (noteId) => { this.bringNoteToFront(noteId); }; // Note delete callback this.ui.onNoteDelete = (note) => { this.deleteNote(note.id); }; // Note duplicate callback this.ui.onNoteDuplicate = (note) => { this.duplicateNote(note.id); }; // Note export callback this.ui.onNoteExport = (note) => { this.exportSingleNote(note); }; // Note restore callback this.ui.onRestoreNote = (noteId) => { this.restoreNote(noteId); }; // Get note callback this.ui.onGetNote = (noteId) => { return this.notes.get(noteId); }; // Set reminder callback this.ui.onSetReminder = (note) => { this.setReminder(note); }; // Set category callback this.ui.onSetCategory = (note) => { this.setCategory(note); }; // Kanban card click callback this.ui.onKanbanCardClick = (noteId) => { this.switchToStickyView(); setTimeout(() => { this.ui.selectNote(noteId); const noteEl = this.ui.notes.get(noteId); if (noteEl) { noteEl.scrollIntoView({behavior: 'smooth', block: 'center'}); } }, 100); }; } /** * Setup global event listeners */ setupGlobalEventListeners() { // New note button document.getElementById('new-note-btn').addEventListener('click', () => { this.createNote(); }); // Search input document.getElementById('search-input').addEventListener('input', (e) => { const query = e.target.value.trim(); if (query) { this.ui.filterNotes(query); } else { this.ui.showAllNotes(); } }); // Export button document.getElementById('export-btn').addEventListener('click', () => { this.exportNotes(); }); // Import button document.getElementById('import-btn').addEventListener('click', () => { document.getElementById('import-file').click(); }); // Import file input document.getElementById('import-file').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { const json = event.target.result; this.importNotes(json); }; reader.readAsText(file); } // Reset input e.target.value = ''; }); // Theme toggle document.getElementById('theme-toggle').addEventListener('click', () => { this.toggleTheme(); }); // Recycle bin button document.getElementById('recycle-bin-btn').addEventListener('click', () => { this.ui.showRecycleBin(this.recycleBin); }); // Close recycle bin document.getElementById('close-recycle-bin').addEventListener('click', () => { this.ui.hideRecycleBin(); }); // Empty recycle bin document.getElementById('empty-recycle-bin').addEventListener('click', () => { this.emptyRecycleBin(); }); // Help button document.getElementById('help-btn').addEventListener('click', () => { this.ui.showHelp(); }); // Close help document.getElementById('close-help').addEventListener('click', () => { this.ui.hideHelp(); }); // Template button document.getElementById('template-btn').addEventListener('click', () => { this.showTemplateModal(); }); // Close template modal document.getElementById('close-template').addEventListener('click', () => { this.hideTemplateModal(); }); // Template selection document.querySelectorAll('.template-card').forEach(card => { card.addEventListener('click', () => { const templateType = card.getAttribute('data-template'); this.createNoteFromTemplate(templateType); this.hideTemplateModal(); }); }); // View toggle document.getElementById('view-toggle').addEventListener('click', () => { this.toggleView(); }); // Category filter document.getElementById('category-filter').addEventListener('change', (e) => { const category = e.target.value; this.filterByCategory(category); }); // Kanban drag and drop document.querySelectorAll('.kanban-content').forEach(column => { column.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }); column.addEventListener('drop', (e) => { e.preventDefault(); const noteId = e.dataTransfer.getData('text/plain'); const newStatus = column.parentElement.getAttribute('data-status'); this.updateKanbanStatus(noteId, newStatus); }); }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // N - new note if (e.key === 'n' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) { // Only create new note if not typing in a note const activeElement = document.activeElement; if (!activeElement || !activeElement.classList.contains('note-content')) { e.preventDefault(); this.createNote(); } } // Ctrl/Cmd + S - export if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); this.exportNotes(); } // Delete - delete selected note if (e.key === 'Delete' && this.ui.selectedNoteId) { this.deleteNote(this.ui.selectedNoteId); } // Ctrl/Cmd + Z - undo last delete if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); this.undoLastDelete(); } // Esc - close modals if (e.key === 'Escape') { this.ui.hideRecycleBin(); this.ui.hideHelp(); } }); // Storage events for cross-tab sync window.addEventListener('storage', (e) => { if (e.key === this.storage.key) { this.loadData(); } }); } /** * Load data from storage */ loadData() { const data = this.storage.load(); // Load settings this.settings = data.settings || { theme: 'light', lastZIndex: 10, nextId: 1 }; this.lastZIndex = this.settings.lastZIndex || 10; // Apply theme this.ui.setTheme(this.settings.theme || 'light'); // Update theme button const themeBtn = document.getElementById('theme-toggle'); themeBtn.innerHTML = this.settings.theme === 'dark' ? 'light_mode' : 'dark_mode'; const emptyState = document.getElementById('empty-state'); // Load notes this.notes.clear(); this.ui.clearAllNotes(); this.categories.clear(); this.categories.add('Uncategorized'); if (data.notes && data.notes.length > 0) { data.notes.forEach(noteData => { const note = Note.fromJSON(noteData); this.notes.set(note.id, note); // Collect categories if (note.category) { this.categories.add(note.category); } if (this.currentView === 'sticky') { this.ui.renderNote(note); } }); emptyState.style.display = 'none'; } else { emptyState.style.display = 'flex'; } // Render Kanban if in Kanban view if (this.currentView === 'kanban') { this.ui.renderKanban(Array.from(this.notes.values())); } // Load recycle bin this.recycleBin = data.recycleBin || []; // Update category filter this.updateCategoryFilter(); } /** * Save data to storage */ saveData() { const notes = Array.from(this.notes.values()).map(note => note.toJSON()); const data = { notes: notes, settings: { ...this.settings, lastZIndex: this.lastZIndex }, recycleBin: this.recycleBin }; this.storage.save(data); } /** * Save a note * @param {Note} note - Note to save */ saveNote(note) { this.notes.set(note.id, note); this.saveData(); } /** * Create a new note * @param {number} x - X position * @param {number} y - Y position * @returns {Note} Created note */ createNote(x = null, y = null) { const note = new Note({ x: x || 20 + Math.random() * 200, y: y || 20 + Math.random() * 200, zIndex: ++this.lastZIndex }); this.notes.set(note.id, note); this.ui.renderNote(note); this.saveData(); // Focus on the new note setTimeout(() => { const noteEl = this.ui.notes.get(note.id); if (noteEl) { const contentEl = noteEl.querySelector('.note-content'); if (contentEl) { contentEl.focus(); } } }, 100); return note; } /** * Delete a note * @param {string} noteId - ID of note to delete */ deleteNote(noteId) { const note = this.notes.get(noteId); if (!note) return; // Add to recycle bin this.storage.addToRecycleBin(note); this.recycleBin = this.storage.load().recycleBin; // Remove from UI and model this.ui.removeNote(noteId); this.notes.delete(noteId); this.saveData(); Utils.showNotification('Note deleted. You can restore it from the recycle bin.', 'success'); } /** * Duplicate a note * @param {string} noteId - ID of note to duplicate * @returns {Note|null} Duplicated note or null */ duplicateNote(noteId) { const originalNote = this.notes.get(noteId); if (!originalNote) return null; const duplicatedNote = new Note({ content: originalNote.content, x: originalNote.x + 20, y: originalNote.y + 20, width: originalNote.width, height: originalNote.height, color: originalNote.color, zIndex: ++this.lastZIndex, pinned: originalNote.pinned, reminder: originalNote.reminder }); this.notes.set(duplicatedNote.id, duplicatedNote); this.ui.renderNote(duplicatedNote); this.saveData(); Utils.showNotification('Note duplicated.', 'success'); return duplicatedNote; } /** * Export a single note * @param {Note} note - Note to export */ exportSingleNote(note) { const data = { notes: [note.toJSON()], settings: this.settings, recycleBin: [] }; const json = JSON.stringify(data, null, 2); const blob = new Blob([json], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `sticky-note-${note.id}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Utils.showNotification('Note exported.', 'success'); } /** * Export all notes */ exportNotes() { const json = this.storage.export(); const blob = new Blob([json], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `sticky-notes-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Utils.showNotification('Notes exported.', 'success'); } /** * Import notes * @param {string} json - JSON string to import */ importNotes(json) { // Ask user for import mode const mode = confirm('Replace all notes or merge with existing notes?\n\nOK = Replace, Cancel = Merge') ? 'replace' : 'merge'; if (this.storage.import(json, mode)) { this.loadData(); Utils.showNotification(`Notes imported (${mode} mode).`, 'success'); } } /** * Restore a note from recycle bin * @param {string} noteId - ID of note to restore */ restoreNote(noteId) { const note = this.storage.restoreFromRecycleBin(noteId); if (note) { this.notes.set(note.id, note); this.ui.renderNote(note); this.saveData(); this.loadData(); // Refresh recycle bin Utils.showNotification('Note restored.', 'success'); } } /** * Empty recycle bin */ emptyRecycleBin() { if (confirm('Are you sure you want to empty the recycle bin? This action cannot be undone.')) { this.storage.emptyRecycleBin(); this.recycleBin = []; this.ui.showRecycleBin([]); Utils.showNotification('Recycle bin emptied.', 'success'); } } /** * Undo last delete */ undoLastDelete() { if (this.recycleBin.length > 0) { const lastDeleted = this.recycleBin[0]; this.restoreNote(lastDeleted.id); } else { Utils.showNotification('Nothing to undo.', 'info'); } } /** * Bring a note to front * @param {string} noteId - ID of note to bring to front */ bringNoteToFront(noteId) { const note = this.notes.get(noteId); if (!note) return; note.zIndex = ++this.lastZIndex; this.ui.updateNote(note); this.saveData(); } /** * Toggle theme */ toggleTheme() { const currentTheme = this.settings.theme || 'light'; const newTheme = currentTheme === 'light' ? 'dark' : 'light'; this.settings.theme = newTheme; this.ui.setTheme(newTheme); this.saveData(); // Update theme button const themeBtn = document.getElementById('theme-toggle'); themeBtn.innerHTML = newTheme === 'dark' ? 'light_mode' : 'dark_mode'; } /** * Get templates * @returns {Object} Templates object */ getTemplates() { return { todo: { content: '# To-Do List\n\n- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3\n\n## Priority\n- [ ] High priority task\n\n## Notes\nAdd any additional notes here...', category: 'Tasks', color: '#d4f0f0', isMarkdown: true }, meeting: { content: '# Meeting Notes\n\n**Date:** ' + new Date().toLocaleDateString() + '\n**Attendees:** \n\n## Agenda\n1. \n2. \n3. \n\n## Discussion\n\n## Action Items\n- [ ] \n- [ ] ', category: 'Meetings', color: '#ffebcc', isMarkdown: true }, idea: { content: '# 💡 Idea\n\n## Concept\n\n## Why?\n\n## Next Steps\n- \n- ', category: 'Ideas', color: '#fff9a8', isMarkdown: true }, goals: { content: '# 🎯 Goals\n\n## Short-term\n- [ ] \n- [ ] \n\n## Long-term\n- [ ] \n- [ ] \n\n## Progress\n', category: 'Goals', color: '#e6ccff', isMarkdown: true }, shopping: { content: '# 🛒 Shopping List\n\n## Groceries\n- [ ] \n- [ ] \n\n## Other\n- [ ] \n- [ ] ', category: 'Shopping', color: '#ccffcc', isMarkdown: true }, blank: { content: '', category: 'Uncategorized', color: '#fff9a8', isMarkdown: false } }; } /** * Create note from template * @param {string} templateType - Type of template */ createNoteFromTemplate(templateType) { const template = this.templates[templateType]; if (!template) return; const note = new Note({ content: template.content, color: template.color, category: template.category, isMarkdown: template.isMarkdown, zIndex: ++this.lastZIndex }); this.notes.set(note.id, note); this.categories.add(template.category); if (this.currentView === 'sticky') { this.ui.renderNote(note); } this.saveData(); this.updateCategoryFilter(); Utils.showNotification('Note created from template!', 'success'); } /** * Show template modal */ showTemplateModal() { const modal = document.getElementById('template-modal'); modal.classList.add('active'); modal.setAttribute('aria-hidden', 'false'); } /** * Hide template modal */ hideTemplateModal() { const modal = document.getElementById('template-modal'); modal.classList.remove('active'); modal.setAttribute('aria-hidden', 'true'); } /** * Set reminder for a note * @param {Note} note - Note to set reminder for */ setReminder(note) { const dateStr = prompt('Set reminder date and time (YYYY-MM-DD HH:MM):', new Date(Date.now() + 86400000).toISOString().slice(0, 16).replace('T', ' ')); if (dateStr) { try { const reminderDate = new Date(dateStr); if (reminderDate > new Date()) { note.reminder = reminderDate.toISOString(); this.ui.updateNote(note); this.saveData(); Utils.showNotification('Reminder set!', 'success'); } else { Utils.showNotification('Please set a future date.', 'error'); } } catch (e) { Utils.showNotification('Invalid date format.', 'error'); } } } /** * Set category for a note * @param {Note} note - Note to set category for */ setCategory(note) { const categoriesList = Array.from(this.categories).join(', '); const category = prompt(`Set category for this note:\n\nExisting categories: ${categoriesList}`, note.category || 'Uncategorized'); if (category !== null && category.trim()) { note.category = category.trim(); this.categories.add(category.trim()); this.ui.updateNote(note); this.saveData(); this.updateCategoryFilter(); Utils.showNotification('Category updated!', 'success'); } } /** * Update category filter dropdown */ updateCategoryFilter() { const select = document.getElementById('category-filter'); const currentValue = select.value; select.innerHTML = ''; Array.from(this.categories).sort().forEach(category => { const option = document.createElement('option'); option.value = category; option.textContent = category; select.appendChild(option); }); select.value = currentValue; } /** * Filter notes by category * @param {string} category - Category to filter by */ filterByCategory(category) { if (!category) { this.ui.showAllNotes(); return; } this.ui.notes.forEach((noteEl, noteId) => { const note = this.notes.get(noteId); if (note && note.category === category) { noteEl.style.display = 'flex'; } else { noteEl.style.display = 'none'; } }); } /** * Toggle between sticky and kanban view */ toggleView() { const stickyBoard = document.getElementById('board'); const kanbanBoard = document.getElementById('kanban-board'); const viewToggleBtn = document.getElementById('view-toggle'); if (this.currentView === 'sticky') { // Switch to Kanban this.currentView = 'kanban'; stickyBoard.style.display = 'none'; kanbanBoard.style.display = 'flex'; viewToggleBtn.innerHTML = 'dashboard Sticky'; this.ui.renderKanban(Array.from(this.notes.values())); } else { // Switch to Sticky this.switchToStickyView(); } } /** * Switch to sticky view */ switchToStickyView() { const stickyBoard = document.getElementById('board'); const kanbanBoard = document.getElementById('kanban-board'); const viewToggleBtn = document.getElementById('view-toggle'); this.currentView = 'sticky'; stickyBoard.style.display = 'block'; kanbanBoard.style.display = 'none'; viewToggleBtn.innerHTML = 'view_kanban Kanban'; // Re-render all notes this.ui.clearAllNotes(); this.notes.forEach(note => { this.ui.renderNote(note); }); } /** * Update Kanban status for a note * @param {string} noteId - ID of note * @param {string} newStatus - New status */ updateKanbanStatus(noteId, newStatus) { const note = this.notes.get(noteId); if (note) { note.kanbanStatus = newStatus; this.saveData(); this.ui.renderKanban(Array.from(this.notes.values())); Utils.showNotification('Status updated!', 'success'); } } /** * Check reminders and show notifications */ checkReminders() { setInterval(() => { const now = new Date(); this.notes.forEach(note => { if (note.reminder) { const reminderDate = new Date(note.reminder); const diff = reminderDate - now; // Show notification 5 minutes before if (diff > 0 && diff < 300000 && !note._reminderShown) { if ('Notification' in window && Notification.permission === 'granted') { new Notification('Reminder: Sticky Note', { body: note.content.substring(0, 100), icon: 'data:image/svg+xml,📝' }); } Utils.showNotification('Reminder: ' + note.content.substring(0, 50), 'info'); note._reminderShown = true; } } }); }, 60000); // Check every minute // Request notification permission if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } } } // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { const app = new AppController(); // Register service worker if available if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js') .then(registration => { console.log('Service Worker registered with scope:', registration.scope); }) .catch(error => { console.log('Service Worker registration failed:', error); }); } }); // Return public API return { Note, StorageManager, UIManager, AppController, Utils }; })();