Notepad Online - Advanced Web Text Editor Notepad Online - Advanced Web Text Editor
Words: 0 | Characters: 0
Not saved yet
Recording... Click mic button again to stop
`;
mimeType = 'text/html';
break;
case 'md':
content = convertHtmlToMarkdownService(activeNote.content);
mimeType = 'text/markdown';
break;
case 'json':
content = JSON.stringify({
id: activeNote.id,
title: activeNote.title,
content: activeNote.content, // Save HTML content for JSON
created: activeNote.created,
modified: activeNote.modified,
tags: activeNote.tags,
reminder: activeNote.reminder ? JSON.parse(activeNote.reminder) : null
}, null, 2);
mimeType = 'application/json';
break;
}
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}.${extension}`;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
closeModal(saveFileModal);
showToast('File Downloaded', `${name}.${extension} downloaded.`, 'success');
}function printNote() {
window.print();
}// Share functions
function openShareModal() {
const note = notes.find(note => note.id === activeNoteId);
if (note) {
const baseUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
// For simplicity, we'll just use note ID. A more robust solution might involve a backend.
const shareableUrl = `${baseUrl}#note=${note.id}`; // Use fragment for client-side routing idea
shareLink.value = shareableUrl;
}
openModal(shareModal);
}
function copyShareLink() {
shareLink.select();
shareLink.setSelectionRange(0, 99999);
try {
// Modern clipboard API
navigator.clipboard.writeText(shareLink.value)
.then(() => {
showToast('Link Copied', 'Share link copied to clipboard!', 'success');
})
.catch(err => { // Fallback for older browsers
document.execCommand('copy');
showToast('Link Copied', 'Share link copied (fallback).', 'success');
});
} catch (err) {
showToast('Copy Failed', 'Could not copy link.', 'error');
}
}
function shareToSocialMedia(platform) {
const note = notes.find(note => note.id === activeNoteId);
if (!note) return;
const baseUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
const shareableUrl = encodeURIComponent(`${baseUrl}#note=${note.id}`); // Using fragment
const title = encodeURIComponent(note.title);
const text = encodeURIComponent(`Check out my note: "${note.title}"`);
let shareUrl = '';
switch (platform) {
case 'twitter':
shareUrl = `https://twitter.com/intent/tweet?text=${text}&url=${shareableUrl}`;
break;
case 'facebook':
shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${shareableUrl}"e=${text}`;
break;
case 'linkedin':
// LinkedIn requires a proper URL, fragments might not work well for previews.
// For a real app, a backend would generate a unique page for each shared note.
shareUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(window.location.href)}&title=${title}&summary=${text}`;
break;
case 'whatsapp':
shareUrl = `https://wa.me/?text=${text}%20${shareableUrl}`;
break;
case 'telegram':
shareUrl = `https://t.me/share/url?url=${shareableUrl}&text=${text}`;
break;
default:
showToast('Share Error', 'Invalid platform.', 'error');
return;
}
window.open(shareUrl, '_blank', 'width=600,height=400,noopener,noreferrer');
}
// Rich Text Editing Functions
function formatText(command, value = null) {
document.execCommand(command, false, value);
editor.focus();
onEditorInput(); // To trigger save and update UI
}function formatAlignment(alignment) {
formatText('justify' + alignment.charAt(0).toUpperCase() + alignment.slice(1));
}function formatList(type) {
formatText(type === 'bullet' ? 'insertUnorderedList' : 'insertOrderedList');
}function formatIndent(direction) {
formatText(direction === 'increase' ? 'indent' : 'outdent');
}function applyFontStyle() {
// This directly styles the editor, which is fine for overall font.
// For specific selections, execCommand('fontName', false, fontFamily.value) and
// execCommand('fontSize', false, index) (1-7 for sizes) would be needed,
// but direct styling is simpler for a global change.
editor.style.fontFamily = fontFamily.value;
editor.style.fontSize = fontSize.value;
// To make this apply to new text or persist, would need to wrap content in spans or use CSS more deeply.
// For simplicity, this global style change is kept.
}function insertLink() {
const url = linkUrl.value.trim();
const text = linkText.value.trim() || url; // Default to URL if text is empty
const target = openNewTab.checked ? '_blank' : '';
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showToast('Invalid URL', 'Please enter a valid URL starting with http:// or https://', 'error');
return;
}
const linkHtml = `
${text}`;
formatText('insertHTML', linkHtml);
closeModal(linkModal);
}function insertImage() {
let url = imageUrl.value.trim();
const alt = imageAlt.value.trim() || 'Image';
if (!url && imageUpload.files.length === 0) {
showToast('No Image', 'Please enter an image URL or upload a file.', 'error');
return;
}
if (imageUpload.files.length > 0) {
const file = imageUpload.files[0];
if (!file.type.startsWith('image/')) {
showToast('Invalid File', 'Please upload an image file.', 'error');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const imgHtml = `

`;
formatText('insertHTML', imgHtml);
showToast('Image Inserted', 'Image added to note.', 'success');
};
reader.onerror = () => showToast('File Error', 'Could not read image file.', 'error');
reader.readAsDataURL(file);
// Reset file input for next upload
imageUpload.value = '';
} else { // URL case
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showToast('Invalid URL', 'Please enter a valid image URL.', 'error');
return;
}
const imgHtml = `

`;
formatText('insertHTML', imgHtml);
}
closeModal(imageModal);
imageUrl.value = '';
imageAlt.value = '';
}function insertTable() {
// Using a custom modal for this would be better. For now, `prompt` is used.
const rowsInput = window.prompt('Enter number of rows (e.g., 3):', '3');
const colsInput = window.prompt('Enter number of columns (e.g., 3):', '3');const rows = parseInt(rowsInput);
const cols = parseInt(colsInput);
if (isNaN(rows) || isNaN(cols) || rows <= 0 || cols <= 0 || rows > 20 || cols > 10) { // Added some limits
showToast('Invalid Input', 'Rows (1-20), Cols (1-10).', 'warning');
return;
}
let tableHTML = '
';
tableHTML += '';
for (let j = 0; j < cols; j++) {
tableHTML += `Header ${j+1} | `;
}
tableHTML += '
';
// Create data rows (rows input includes header, so rows-1 for body)
for (let i = 0; i < Math.max(0, rows -1) ; i++) {
tableHTML += '';
for (let j = 0; j < cols; j++) {
tableHTML += `Cell | `;
}
tableHTML += '
';
}
tableHTML += '
'; // Add some space after table
formatText('insertHTML', tableHTML);
}// Emoji functions
function loadEmojis() {
const emojiContainer = document.getElementById('emojiContainer');
emojiContainer.innerHTML = ''; // Clear previous
// Save current selection before opening emoji picker
saveCurrentSelection(); // Use the existing function
const emojis = [ // Sample, can be expanded
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉',
'😌', '😍', '�', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨',
'🧐', '🤓', '😎', '🤩', '🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️',
'😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', '🤯', '😳',
'🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶',
'😐', '😑', '😬', '🙄', '😯', '😦', '😧', '😮', '😲', '🥱', '😴', '🤤', '😪',
'😵', '🤐', '🥴', '🤢', '🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠',
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞',
'💓', '💗', '💖', '💘', '💝', '👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '👏',
'🎉', '🎈', '🎁', '✨', '🌟', '☀️', '🌙', '⭐', '🚀', '🚗', '💡', '💻', '📱'
];
emojis.forEach(emoji => {
const emojiItem = document.createElement('div');
emojiItem.className = 'emoji-item';
emojiItem.textContent = emoji;
emojiItem.setAttribute('data-emoji', emoji);
emojiItem.setAttribute('role', 'button');
emojiItem.setAttribute('aria-label', `Insert emoji ${emoji}`);
emojiItem.addEventListener('click', function() { // Single click to select
document.querySelectorAll('.emoji-item.selected').forEach(item => item.classList.remove('selected'));
this.classList.add('selected');
});
emojiItem.addEventListener('dblclick', function(e) { // Double click to insert
e.preventDefault(); e.stopPropagation();
const emojiChar = this.getAttribute('data-emoji');
editor.focus();
if (window.lastSelectionRange) { // Restore selection if possible
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(window.lastSelectionRange);
}
formatText('insertText', emojiChar);
closeModal(emojiModal);
});
emojiContainer.appendChild(emojiItem);
});
}
function filterEmojis() {
const searchTerm = document.getElementById('emojiSearch').value.toLowerCase();
const emojiItems = document.querySelectorAll('.emoji-item');
emojiItems.forEach(item => {
item.style.display = item.getAttribute('data-emoji').includes(searchTerm) ? 'flex' : 'none';
});
}
function insertSelectedEmoji() {
const selectedEmoji = document.querySelector('.emoji-item.selected');
if (selectedEmoji) {
const emoji = selectedEmoji.getAttribute('data-emoji');
editor.focus();
if (window.lastSelectionRange) { // Restore selection
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(window.lastSelectionRange);
}
formatText('insertText', emoji);
closeModal(emojiModal);
selectedEmoji.classList.remove('selected');
} else {
showToast('No Emoji Selected', 'Please select an emoji.', 'warning');
}
}
function insertCodeBlock() {
// Using a custom modal for this would be better. For now, `prompt` is used.
const language = window.prompt('Enter language (e.g., javascript, python) or leave blank for generic:', '');
const langClass = language ? `language-${language.trim().toLowerCase()}` : '';
// Basic placeholder, user can replace
const placeholderCode = language ? `// ${language} code\nfunction example() {\n console.log("Hello, ${language}!");\n}` : 'Your code here...';
const codeBlockHtml = `
${placeholderCode}
`;
formatText('insertHTML', codeBlockHtml);
}// Tag Management
function renderTagList(tagsArray = []) {
tagList.innerHTML = ''; // Clear existing tags
if (!tagsArray || tagsArray.length === 0) {
tagList.innerHTML = '
No tags for this note.
';
return;
}
tagsArray.forEach(tag => {
const tagEl = document.createElement('div');
tagEl.className = 'tag';
tagEl.style.position = 'relative';
tagEl.style.paddingRight = '20px';
const removeBtn = document.createElement('span');
removeBtn.innerHTML = '×';
removeBtn.style.cssText = 'position:absolute; right:5px; top:50%; transform:translateY(-50%); cursor:pointer; font-size:1.1em;';
removeBtn.title = `Remove tag: ${tag}`;
removeBtn.onclick = (e) => { e.stopPropagation(); removeTag(tag); };
tagEl.textContent = tag;
tagEl.appendChild(removeBtn);
tagList.appendChild(tagEl);
});
}function addTag() {
const tagValue = tagInput.value.trim();
if (!tagValue) return;
const activeNote = notes.find(note => note.id === activeNoteId);
if (!activeNote) return;
if (!activeNote.tags) activeNote.tags = [];
if (activeNote.tags.includes(tagValue)) {
showToast('Tag Exists', 'Tag already added.', 'info');
return;
}
activeNote.tags.push(tagValue);
tagInput.value = ''; // Clear input
renderTagList(activeNote.tags);
saveActiveNote(); // Save note to persist tags
showToast('Tag Added', `"${tagValue}" added.`, 'success');
}function removeTag(tagToRemove) {
const activeNote = notes.find(note => note.id === activeNoteId);
if (!activeNote || !activeNote.tags) return;
activeNote.tags = activeNote.tags.filter(tag => tag !== tagToRemove);
renderTagList(activeNote.tags);
saveActiveNote(); // Save note
showToast('Tag Removed', `"${tagToRemove}" removed.`, 'info');
}// Reminder Functions
function prepareDateTimeForReminder(reminderObjStr) {
reminderDate.value = '';
reminderTime.value = '';
reminderNote.value = '';
removeReminderBtn.disabled = true;
if (reminderObjStr) {
try {
const reminder = JSON.parse(reminderObjStr);
const date = new Date(reminder.date);
if (!isNaN(date)) {
reminderDate.value = date.toISOString().split('T')[0];
reminderTime.value = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
reminderNote.value = reminder.note || '';
removeReminderBtn.disabled = false;
} else { throw new Error("Invalid date in reminder"); }
} catch (e) {
console.error("Error parsing reminder:", e);
showToast('Reminder Error', 'Could not load reminder.', 'error');
setDefaultReminderDateTime();
}
} else {
setDefaultReminderDateTime();
}
}function setDefaultReminderDateTime() {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0); // Default to 9 AM tomorrow
reminderDate.value = tomorrow.toISOString().split('T')[0];
reminderTime.value = '09:00';
}function setReminder() {
const dateVal = reminderDate.value;
const timeVal = reminderTime.value;
const noteVal = reminderNote.value.trim();
if (!dateVal || !timeVal) {
showToast('Invalid Date/Time', 'Select date and time.', 'error');
return;
}
const reminderDateTime = new Date(`${dateVal}T${timeVal}`);
if (isNaN(reminderDateTime.getTime())) {
showToast('Invalid Date/Time', 'Selected date/time is invalid.', 'error');
return;
}
if (reminderDateTime < new Date()) {
showToast('Past Date/Time', 'Select a future date/time.', 'error');
return;
}
const reminderToSave = { date: reminderDateTime.toISOString(), note: noteVal };
const activeNote = notes.find(note => note.id === activeNoteId);
if (activeNote) {
activeNote.reminder = JSON.stringify(reminderToSave);
saveActiveNote(); // Save note
scheduleNotification(reminderDateTime, activeNote.title, noteVal);
showToast('Reminder Set', `Set for ${formatDate(reminderDateTime.toISOString())}.`, 'success');
closeModal(reminderModal);
}
}function removeReminder() {
const activeNote = notes.find(note => note.id === activeNoteId);
if (activeNote) {
activeNote.reminder = null;
saveActiveNote(); // Save note
showToast('Reminder Removed', 'Reminder cleared.', 'info');
closeModal(reminderModal);
prepareDateTimeForReminder(null); // Reset modal fields
}
}function scheduleNotification(date, title, message) {
const timeDiff = date.getTime() - new Date().getTime();
if (timeDiff > 0 && 'Notification' in window) {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
setTimeout(() => {
new Notification(`Reminder: ${title}`, {
body: message || 'Your scheduled note reminder.',
icon: 'https://placehold.co/48x48/6366f1/ffffff?text=N&fontsize=16'
});
}, timeDiff);
} else if (permission === 'denied') {
showToast('Notifications Denied', 'Reminders won\'t show as notifications.', 'warning');
}
});
}
}// Voice Recording
let recognition; // Make recognition instance accessible
function toggleVoiceRecording() {
if (!isRecording) startVoiceRecording();
else stopVoiceRecording();
}function startVoiceRecording() {
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
showToast('Not Supported', 'Voice recognition not supported.', 'error');
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition(); // Assign to the outer scope variable
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = navigator.language || 'en-US';
recognition.onstart = () => {
isRecording = true;
voiceRecording.classList.add('show');
voiceBtn.style.color = 'var(--danger)';
voiceBtn.title = "Stop Recording";
};
recognition.onresult = (event) => {
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
}
}
if (finalTranscript) {
formatText('insertText', finalTranscript.trim() + ' ');
}
};
recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
let msg = 'Speech recognition error.';
if (event.error === 'no-speech') msg = 'No speech detected.';
else if (event.error === 'audio-capture') msg = 'Microphone problem.';
else if (event.error === 'not-allowed') msg = 'Mic permission denied.';
showToast('Voice Error', msg, 'error');
stopVoiceRecordingCleanup();
};
recognition.onend = stopVoiceRecordingCleanup; // Cleanup when stops
try {
recognition.start();
} catch (e) {
showToast('Voice Error', 'Could not start voice recognition.', 'error');
stopVoiceRecordingCleanup();
}
}
function stopVoiceRecordingCleanup() {
isRecording = false;
voiceRecording.classList.remove('show');
voiceBtn.style.color = ''; // Reset color
voiceBtn.title = "Voice to Text";
if (recognition) { // Ensure recognition exists before trying to call methods on it
recognition.onstart = null;
recognition.onresult = null;
recognition.onerror = null;
recognition.onend = null;
recognition = null; // Clear instance
}
}function stopVoiceRecording() {
if (recognition && isRecording) {
recognition.stop(); // This will trigger 'onend' which calls cleanup
} else {
stopVoiceRecordingCleanup(); // Manual cleanup if not actively recording
}
}// Spell Check
function toggleSpellCheck() {
spellCheckEnabled = !spellCheckEnabled;
editor.setAttribute('spellcheck', spellCheckEnabled.toString());
spellCheckBtn.style.color = spellCheckEnabled ? 'var(--primary)' : '';
spellCheckBtn.title = spellCheckEnabled ? "Disable Spell Check" : "Enable Spell Check";
showToast('Spell Check', `Browser spell check ${spellCheckEnabled ? 'enabled' : 'disabled'}.`, 'info');
editor.focus();
}// Context Menu
function showContextMenu(e) {
const selection = window.getSelection();
const isTextSelected = selection && selection.toString().length > 0;
const isClickInsideEditor = editor.contains(e.target);if (!isClickInsideEditor && !isTextSelected && !editor.isSameNode(e.target)) { // Allow if click is on editor itself
hideContextMenu();
return;
}
e.preventDefault();
saveCurrentSelection(); // Save for context menu actions
window.lastSelectionRangeForContextMenu = window.lastSelectionRange; // Use the general onelet left = e.clientX;
let top = e.clientY;
const menuWidth = contextMenu.offsetWidth || 200;
const menuHeight = contextMenu.offsetHeight || 300; // Approximate
if (left + menuWidth > window.innerWidth) left = window.innerWidth - menuWidth - 5;
if (top + menuHeight > window.innerHeight) top = window.innerHeight - menuHeight - 5;
contextMenu.style.left = `${Math.max(0, left)}px`; // Ensure not off-screen
contextMenu.style.top = `${Math.max(0, top)}px`;
contextMenu.classList.add('show');
}function hideContextMenu() {
contextMenu.classList.remove('show');
}function setupContextMenuActions() {
contextMenu.querySelectorAll('.context-menu-item').forEach(item => {
item.addEventListener('click', () => {
const action = item.dataset.action;
editor.focus();
if (window.lastSelectionRangeForContextMenu) { // Restore selection
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(window.lastSelectionRangeForContextMenu);
}
switch (action) {
case 'selectAll': formatText('selectAll'); break;
case 'cut': formatText('cut'); break;
case 'copy': formatText('copy'); break;
case 'paste':
navigator.clipboard.readText()
.then(text => {
if (text) formatText('insertText', text);
else formatText('paste'); // Fallback
})
.catch(() => formatText('paste')); // Fallback
break;
case 'bold': formatText('bold'); break;
case 'italic': formatText('italic'); break;
case 'underline': formatText('underline'); break;
case 'link':
const selectedText = window.getSelection().toString();
if (selectedText) linkText.value = selectedText;
else linkText.value = '';
linkUrl.value = ''; // Clear URL field
openModal(linkModal);
break;
case 'lookup': lookupSelection(); break;
}
hideContextMenu();
});
});
}// Search Notes
function searchNotes() {
const query = searchInput.value.toLowerCase().trim();
if (!query) {
renderNoteList();
return;
}
const results = notes.filter(note =>
note.title.toLowerCase().includes(query) ||
note.content.replace(/<[^>]*>/g, '').toLowerCase().includes(query) ||
(note.tags && note.tags.some(tag => tag.toLowerCase().includes(query)))
);
renderNoteList(results);
}// Toast Notifications
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.setAttribute('role', 'alert');
let iconClass = type === 'success' ? 'fa-check-circle' :
type === 'error' ? 'fa-exclamation-circle' :
type === 'warning' ? 'fa-exclamation-triangle' : 'fa-info-circle';
toast.innerHTML = `
`;
toastsContainer.appendChild(toast);
toast.querySelector('.toast-close').onclick = () => toast.remove();
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 3000);
}// Editor Event Handlers
function onEditorInput() {
updateWordCount();
isNoteModified = true;
lastSaved.textContent = 'Unsaved changes...';
clearTimeout(window.autoSaveTimeout);
window.autoSaveTimeout = setTimeout(saveActiveNote, 2500); // Autosave after 2.5s
}function onEditorKeyDown(e) {
if (e.key === 's' && (e.ctrlKey || e.metaKey)) { // Ctrl+S or Cmd+S
e.preventDefault();
saveActiveNote();
}
if (e.key === 'Tab') { // Handle Tab key for indent/outdent
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer;
let parentListItem = commonAncestor.nodeType === Node.ELEMENT_NODE ? commonAncestor.closest('li') : commonAncestor.parentElement?.closest('li');
let parentCodeBlock = commonAncestor.nodeType === Node.ELEMENT_NODE ? commonAncestor.closest('pre > code') : commonAncestor.parentElement?.closest('pre > code');if (parentListItem) {
e.preventDefault();
formatText(e.shiftKey ? 'outdent' : 'indent');
} else if (parentCodeBlock) {
e.preventDefault();
formatText('insertText', ' '); // Insert 4 spaces for tab in code
}
// else: allow default tab behavior if not in list or code
}
}
}// Utility Functions
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const timeOptions = { hour: 'numeric', minute: 'numeric', hour12: true };if (diffDays === 0 && date.getDate() === now.getDate()) {
return `Today at ${date.toLocaleTimeString(navigator.language, timeOptions)}`;
}
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
if (diffDays <= 1 && date.getDate() === yesterday.getDate() && date.getMonth() === yesterday.getMonth() && date.getFullYear() === yesterday.getFullYear()) {
return `Yesterday at ${date.toLocaleTimeString(navigator.language, timeOptions)}`;
}
if (diffDays < 7) {
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
return `${days[date.getDay()]} at ${date.toLocaleTimeString(navigator.language, timeOptions)}`;
}
return date.toLocaleDateString(navigator.language, { year: 'numeric', month: 'short', day: 'numeric' });
}function formatTimeAgo(date) {
const now = new Date();
const diffMs = now - date;
const diffSecs = Math.round(diffMs / 1000);
if (diffSecs < 5) return 'just now';
if (diffSecs < 60) return `${diffSecs}s ago`;
const diffMins = Math.round(diffSecs / 60);
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.round(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.round(diffHours / 24);
if (diffDays < 30) return `${diffDays}d ago`;
const diffMonths = Math.round(diffDays / 30.44); // Average days in month
if (diffMonths < 12) return `${diffMonths}mo ago`;const diffYears = Math.round(diffDays/365.25);
return `${diffYears}y ago`;
}function convertHtmlToMarkdownService(html) {
if (turndownService) {
try {
// Add a rule for
to ensure they become newlines in Markdown
turndownService.addRule('brToNewline', {
filter: 'br',
replacement: function () { return '\n'; }
});
// Add a rule for paragraphs to ensure double newline after them
turndownService.addRule('paragraph', {
filter: 'p',
replacement: function (content) {
return '\n\n' + content + '\n\n';
}
});
// Clean up excessive newlines that might result
let markdown = turndownService.turndown(html);
markdown = markdown.replace(/\n{3,}/g, '\n\n'); // Replace 3+ newlines with 2
return markdown.trim();} catch(e) {
showToast('Markdown Error', 'Could not convert to Markdown.', 'error');
console.error("Turndown conversion error:", e);
// Basic fallback: strip tags, try to preserve line breaks from
and
let text = html.replace(/
/gi, '\n');
text = text.replace(/<\/p>/gi, '\n\n');
const tempDiv = document.createElement('div');
tempDiv.innerHTML = text; // Use browser to strip remaining tags
return (tempDiv.textContent || tempDiv.innerText || "").trim();
}
}
// Absolute fallback if Turndown is not loaded
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html.replace(/
/gi, '\n').replace(/<\/p>/gi, '\n\n');
return (tempDiv.textContent || tempDiv.innerText || "").trim();
}function lookupSelection() {
const selection = window.getSelection().toString().trim();
if (selection) {
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(selection)}`;
window.open(searchUrl, '_blank', 'noopener,noreferrer');
} else {
showToast('No Selection', 'Select text to look up.', 'info');
}
}// Storage Functions
function saveNotesToStorage() {
try {
localStorage.setItem('notes-app-data', JSON.stringify(notes)); // Changed key for uniqueness
} catch (e) {
console.error('Error saving notes to localStorage:', e);
let msg = 'Could not save notes. Storage full/disabled?';
if (e.name === 'QuotaExceededError') msg = 'Local storage is full.';
showToast('Storage Error', msg, 'error');
}
}function loadNotesFromStorage() {
try {
const storedNotes = localStorage.getItem('notes-app-data'); // Use new key
if (storedNotes) {
notes = JSON.parse(storedNotes);
if (notes.length > 0) {
// Ensure all notes have necessary fields (for backward compatibility if structure changes)
notes = notes.map(note => ({
tags: [], // Default empty array for tags
reminder: null, // Default null for reminder
...note // Spread existing note properties, overriding defaults if present
}));
activeNoteId = notes[0].id; // Default to first note
// Check if URL has a note ID to load
if (window.location.hash && window.location.hash.startsWith('#note=')) {
const noteIdFromUrl = parseInt(window.location.hash.substring(6));
const noteToLoad = notes.find(n => n.id === noteIdFromUrl);
if (noteToLoad) activeNoteId = noteIdFromUrl;
}
renderNoteList();
loadNote(activeNoteId);
} else { // Stored notes array is empty
const note = createNewNote('Welcome Note', defaultContent);
loadNote(note.id);
}
} else { // No notes found in storage
const note = createNewNote('Welcome Note', defaultContent);
loadNote(note.id);
}
} catch (e) {
console.error('Error loading notes from localStorage:', e);
showToast('Storage Error', 'Could not load notes.', 'error');
notes = []; // Clear potentially corrupted notes
const note = createNewNote('Welcome Note', defaultContent); // Fallback to default
loadNote(note.id);
}
}
// Handle loading note from URL hash on initial load and hash change
window.addEventListener('hashchange', () => {
if (window.location.hash && window.location.hash.startsWith('#note=')) {
const noteIdFromUrl = parseInt(window.location.hash.substring(6));
const noteExists = notes.some(n => n.id === noteIdFromUrl);
if (noteExists) {
loadNote(noteIdFromUrl);
} else {
showToast('Note Not Found', 'The note specified in the URL does not exist.', 'warning');
if(notes.length > 0) loadNote(notes[0].id); // Load first note if specified one not found
else { // Create default if no notes exist
const note = createNewNote('Welcome Note', defaultContent);
loadNote(note.id);
}
}
}
});// Initialize the app
init();