/** * 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