Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Notion Lite - Professional Note Taking</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.sidebar { | |
transition: all 0.3s ease; | |
} | |
.note-content { | |
min-height: calc(100vh - 150px); | |
} | |
.drag-handle { | |
cursor: move; | |
} | |
.block-menu { | |
opacity: 0; | |
transition: opacity 0.2s ease; | |
} | |
.note-block:hover .block-menu { | |
opacity: 1; | |
} | |
.prose :where(h1):not(:where([class~="not-prose"] *)) { | |
font-size: 2em; | |
margin-top: 0; | |
margin-bottom: 0.5em; | |
font-weight: 700; | |
} | |
.prose :where(h2):not(:where([class~="not-prose"] *)) { | |
font-size: 1.5em; | |
margin-top: 1.5em; | |
margin-bottom: 0.5em; | |
font-weight: 600; | |
} | |
.prose :where(p):not(:where([class~="not-prose"] *)) { | |
margin-top: 0.5em; | |
margin-bottom: 0.5em; | |
} | |
.prose :where(ul):not(:where([class~="not-prose"] *)) { | |
list-style-type: disc; | |
padding-left: 1.5em; | |
margin-top: 0.5em; | |
margin-bottom: 0.5em; | |
} | |
.prose :where(ol):not(:where([class~="not-prose"] *)) { | |
list-style-type: decimal; | |
padding-left: 1.5em; | |
margin-top: 0.5em; | |
margin-bottom: 0.5em; | |
} | |
.prose :where(blockquote):not(:where([class~="not-prose"] *)) { | |
border-left: 4px solid #e5e7eb; | |
padding-left: 1em; | |
margin-left: 0; | |
font-style: italic; | |
color: #6b7280; | |
} | |
.prose :where(code):not(:where([class~="not-prose"] *)) { | |
background-color: #f3f4f6; | |
padding: 0.2em 0.4em; | |
border-radius: 0.25em; | |
font-family: monospace; | |
} | |
.prose :where(pre):not(:where([class~="not-prose"] *)) { | |
background-color: #1e293b; | |
color: white; | |
padding: 1em; | |
border-radius: 0.5em; | |
overflow-x: auto; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 text-gray-800 flex h-screen overflow-hidden"> | |
<!-- Sidebar --> | |
<div class="sidebar bg-white w-64 border-r border-gray-200 flex flex-col h-full"> | |
<div class="p-4 border-b border-gray-200 flex items-center justify-between"> | |
<h1 class="text-xl font-bold text-gray-800 flex items-center"> | |
<i class="fas fa-book-open mr-2 text-blue-500"></i> | |
Notion Lite | |
</h1> | |
<button id="sidebar-toggle" class="text-gray-500 hover:text-gray-700"> | |
<i class="fas fa-chevron-left"></i> | |
</button> | |
</div> | |
<div class="p-4"> | |
<button id="new-note-btn" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md flex items-center justify-center"> | |
<i class="fas fa-plus mr-2"></i> New Note | |
</button> | |
</div> | |
<div class="px-2"> | |
<div class="flex items-center px-2 py-1 text-gray-500 text-sm font-medium"> | |
<i class="fas fa-thumbtack mr-2 text-xs"></i> | |
<span>Pinned</span> | |
</div> | |
<div id="pinned-notes" class="mt-1"> | |
<!-- Pinned notes will appear here --> | |
</div> | |
<div class="flex items-center px-2 py-1 mt-4 text-gray-500 text-sm font-medium"> | |
<i class="fas fa-file-alt mr-2 text-xs"></i> | |
<span>All Notes</span> | |
</div> | |
<div id="all-notes" class="mt-1"> | |
<!-- All notes will appear here --> | |
</div> | |
</div> | |
<div class="mt-auto p-4 border-t border-gray-200"> | |
<div class="flex items-center"> | |
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-500"> | |
<i class="fas fa-user"></i> | |
</div> | |
<div class="ml-2"> | |
<div class="text-sm font-medium">User</div> | |
<div class="text-xs text-gray-500">Free Plan</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Main Content --> | |
<div class="flex-1 flex flex-col overflow-hidden"> | |
<!-- Toolbar --> | |
<div class="bg-white border-b border-gray-200 p-3 flex items-center justify-between"> | |
<div class="flex items-center"> | |
<button id="sidebar-toggle-mobile" class="mr-4 text-gray-500 md:hidden"> | |
<i class="fas fa-bars"></i> | |
</button> | |
<div id="note-title" class="text-xl font-semibold px-2 py-1 rounded hover:bg-gray-100 cursor-text" contenteditable="true">Untitled Note</div> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<button class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded"> | |
<i class="fas fa-search"></i> | |
</button> | |
<button class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded"> | |
<i class="fas fa-share-alt"></i> | |
</button> | |
<button class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded"> | |
<i class="fas fa-ellipsis-h"></i> | |
</button> | |
</div> | |
</div> | |
<!-- Formatting Toolbar --> | |
<div class="bg-white border-b border-gray-200 p-2 flex items-center overflow-x-auto"> | |
<div class="flex space-x-1"> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="bold"> | |
<i class="fas fa-bold"></i> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="italic"> | |
<i class="fas fa-italic"></i> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="underline"> | |
<i class="fas fa-underline"></i> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="strikethrough"> | |
<i class="fas fa-strikethrough"></i> | |
</button> | |
<div class="border-l border-gray-300 mx-1 h-6"></div> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="heading1"> | |
<span class="font-bold">H1</span> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="heading2"> | |
<span class="font-bold">H2</span> | |
</button> | |
<div class="border-l border-gray-300 mx-1 h-6"></div> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="bulletedList"> | |
<i class="fas fa-list-ul"></i> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="numberedList"> | |
<i class="fas fa-list-ol"></i> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="checklist"> | |
<i class="fas fa-tasks"></i> | |
</button> | |
<div class="border-l border-gray-300 mx-1 h-6"></div> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="quote"> | |
<i class="fas fa-quote-right"></i> | |
</button> | |
<button class="format-btn p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" data-format="code"> | |
<i class="fas fa-code"></i> | |
</button> | |
<div class="border-l border-gray-300 mx-1 h-6"></div> | |
<button class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" id="add-image-btn"> | |
<i class="fas fa-image"></i> | |
</button> | |
<button class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded" id="add-table-btn"> | |
<i class="fas fa-table"></i> | |
</button> | |
</div> | |
</div> | |
<!-- Note Content --> | |
<div id="note-editor" class="flex-1 overflow-y-auto p-6 prose max-w-4xl mx-auto note-content"> | |
<div class="note-block relative" data-block-type="paragraph"> | |
<div class="block-menu absolute -left-8 top-0 text-gray-400 flex flex-col"> | |
<button class="drag-handle p-1 hover:text-gray-600 hover:bg-gray-100 rounded"> | |
<i class="fas fa-grip-vertical"></i> | |
</button> | |
<button class="delete-block p-1 hover:text-red-500 hover:bg-gray-100 rounded"> | |
<i class="fas fa-trash"></i> | |
</button> | |
</div> | |
<div class="block-content" contenteditable="true" data-placeholder="Type '/' for commands or start writing..."></div> | |
</div> | |
</div> | |
<!-- Status Bar --> | |
<div class="bg-white border-t border-gray-200 p-2 text-xs text-gray-500 flex justify-between items-center"> | |
<div> | |
<span id="word-count">0 words</span> | |
<span class="mx-2">•</span> | |
<span id="char-count">0 characters</span> | |
</div> | |
<div> | |
<button id="toggle-dark-mode" class="p-1 hover:bg-gray-100 rounded"> | |
<i class="fas fa-moon"></i> | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Block Menu (hidden by default) --> | |
<div id="block-menu" class="hidden absolute z-10 w-56 bg-white rounded-md shadow-lg border border-gray-200"> | |
<div class="p-1"> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="heading1"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<span class="font-bold text-sm">H1</span> | |
</div> | |
<span>Heading 1</span> | |
</div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="heading2"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<span class="font-bold text-sm">H2</span> | |
</div> | |
<span>Heading 2</span> | |
</div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="paragraph"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-paragraph text-xs"></i> | |
</div> | |
<span>Text</span> | |
</div> | |
<div class="border-t border-gray-200 my-1"></div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="bulletedList"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-list-ul text-xs"></i> | |
</div> | |
<span>Bulleted List</span> | |
</div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="numberedList"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-list-ol text-xs"></i> | |
</div> | |
<span>Numbered List</span> | |
</div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="checklist"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-tasks text-xs"></i> | |
</div> | |
<span>Checklist</span> | |
</div> | |
<div class="border-t border-gray-200 my-1"></div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="quote"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-quote-right text-xs"></i> | |
</div> | |
<span>Quote</span> | |
</div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="code"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-code text-xs"></i> | |
</div> | |
<span>Code Block</span> | |
</div> | |
<div class="border-t border-gray-200 my-1"></div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="image"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-image text-xs"></i> | |
</div> | |
<span>Image</span> | |
</div> | |
<div class="block-option px-3 py-2 text-sm hover:bg-gray-100 rounded cursor-pointer flex items-center" data-block-type="table"> | |
<div class="w-6 h-6 rounded bg-gray-100 flex items-center justify-center mr-2"> | |
<i class="fas fa-table text-xs"></i> | |
</div> | |
<span>Table</span> | |
</div> | |
</div> | |
</div> | |
<script> | |
// State management | |
let notes = []; | |
let currentNoteId = null; | |
let isDarkMode = false; | |
// DOM elements | |
const sidebar = document.querySelector('.sidebar'); | |
const sidebarToggle = document.getElementById('sidebar-toggle'); | |
const sidebarToggleMobile = document.getElementById('sidebar-toggle-mobile'); | |
const newNoteBtn = document.getElementById('new-note-btn'); | |
const noteEditor = document.getElementById('note-editor'); | |
const noteTitle = document.getElementById('note-title'); | |
const pinnedNotesContainer = document.getElementById('pinned-notes'); | |
const allNotesContainer = document.getElementById('all-notes'); | |
const blockMenu = document.getElementById('block-menu'); | |
const formatButtons = document.querySelectorAll('.format-btn'); | |
const wordCountEl = document.getElementById('word-count'); | |
const charCountEl = document.getElementById('char-count'); | |
const toggleDarkModeBtn = document.getElementById('toggle-dark-mode'); | |
const addImageBtn = document.getElementById('add-image-btn'); | |
const addTableBtn = document.getElementById('add-table-btn'); | |
// Initialize the app | |
document.addEventListener('DOMContentLoaded', () => { | |
loadNotes(); | |
setupEventListeners(); | |
createNewNote(); | |
updateCounts(); | |
}); | |
function setupEventListeners() { | |
// Sidebar toggle | |
sidebarToggle.addEventListener('click', toggleSidebar); | |
sidebarToggleMobile.addEventListener('click', toggleSidebar); | |
// New note button | |
newNoteBtn.addEventListener('click', createNewNote); | |
// Note title editing | |
noteTitle.addEventListener('blur', saveCurrentNote); | |
noteTitle.addEventListener('keydown', (e) => { | |
if (e.key === 'Enter') { | |
e.preventDefault(); | |
noteTitle.blur(); | |
} | |
}); | |
// Formatting buttons | |
formatButtons.forEach(btn => { | |
btn.addEventListener('click', () => { | |
const format = btn.dataset.format; | |
applyFormat(format); | |
}); | |
}); | |
// Block menu | |
document.addEventListener('click', (e) => { | |
if (e.target.closest('.block-content') && e.target.closest('.block-content').textContent === '') { | |
const blockContent = e.target.closest('.block-content'); | |
if (blockContent.textContent === '' || blockContent.textContent === '/') { | |
showBlockMenu(blockContent); | |
} | |
} | |
}); | |
// Block options | |
document.querySelectorAll('.block-option').forEach(option => { | |
option.addEventListener('click', (e) => { | |
const blockType = option.dataset.blockType; | |
const blockContent = blockMenu.dataset.targetBlock; | |
if (blockContent) { | |
changeBlockType(blockContent, blockType); | |
} | |
hideBlockMenu(); | |
}); | |
}); | |
// Add image button | |
addImageBtn.addEventListener('click', () => { | |
addBlock('image'); | |
}); | |
// Add table button | |
addTableBtn.addEventListener('click', () => { | |
addBlock('table'); | |
}); | |
// Dark mode toggle | |
toggleDarkModeBtn.addEventListener('click', toggleDarkMode); | |
// Click outside block menu to hide it | |
document.addEventListener('click', (e) => { | |
if (!e.target.closest('#block-menu') && !e.target.closest('.block-content')) { | |
hideBlockMenu(); | |
} | |
}); | |
// Handle slash commands | |
noteEditor.addEventListener('keydown', (e) => { | |
const blockContent = e.target.closest('.block-content'); | |
if (e.key === '/' && blockContent && blockContent.textContent === '') { | |
e.preventDefault(); | |
showBlockMenu(blockContent); | |
} else if (e.key === 'Enter' && blockContent) { | |
handleEnterKey(blockContent, e); | |
} else if (e.key === 'Backspace' && blockContent) { | |
handleBackspaceKey(blockContent, e); | |
} | |
}); | |
// Update counts on input | |
noteEditor.addEventListener('input', () => { | |
updateCounts(); | |
saveCurrentNote(); | |
}); | |
} | |
function toggleSidebar() { | |
sidebar.classList.toggle('-ml-64'); | |
sidebar.classList.toggle('md:-ml-64'); | |
sidebarToggle.querySelector('i').classList.toggle('fa-chevron-left'); | |
sidebarToggle.querySelector('i').classList.toggle('fa-chevron-right'); | |
} | |
function createNewNote() { | |
const newNote = { | |
id: Date.now().toString(), | |
title: 'Untitled Note', | |
content: '', | |
createdAt: new Date(), | |
updatedAt: new Date(), | |
pinned: false | |
}; | |
notes.push(newNote); | |
currentNoteId = newNote.id; | |
saveNotes(); | |
renderNotesList(); | |
renderNote(newNote); | |
} | |
function loadNotes() { | |
const savedNotes = localStorage.getItem('notion-lite-notes'); | |
if (savedNotes) { | |
notes = JSON.parse(savedNotes); | |
if (notes.length > 0) { | |
renderNotesList(); | |
currentNoteId = notes[0].id; | |
renderNote(notes[0]); | |
} | |
} | |
} | |
function saveNotes() { | |
localStorage.setItem('notion-lite-notes', JSON.stringify(notes)); | |
} | |
function saveCurrentNote() { | |
if (!currentNoteId) return; | |
const noteIndex = notes.findIndex(note => note.id === currentNoteId); | |
if (noteIndex === -1) return; | |
// Update title | |
notes[noteIndex].title = noteTitle.textContent || 'Untitled Note'; | |
// Update content | |
const blocks = Array.from(document.querySelectorAll('.note-block')).map(blockEl => { | |
return { | |
type: blockEl.dataset.blockType, | |
content: blockEl.querySelector('.block-content').innerHTML | |
}; | |
}); | |
notes[noteIndex].content = JSON.stringify(blocks); | |
notes[noteIndex].updatedAt = new Date(); | |
saveNotes(); | |
renderNotesList(); | |
} | |
function renderNotesList() { | |
// Clear existing notes | |
pinnedNotesContainer.innerHTML = ''; | |
allNotesContainer.innerHTML = ''; | |
// Separate pinned and unpinned notes | |
const pinned = notes.filter(note => note.pinned); | |
const unpinned = notes.filter(note => !note.pinned); | |
// Sort by updated date (newest first) | |
pinned.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
unpinned.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); | |
// Render pinned notes | |
pinned.forEach(note => { | |
pinnedNotesContainer.appendChild(createNoteListItem(note)); | |
}); | |
// Render unpinned notes | |
unpinned.forEach(note => { | |
allNotesContainer.appendChild(createNoteListItem(note)); | |
}); | |
} | |
function createNoteListItem(note) { | |
const noteEl = document.createElement('div'); | |
noteEl.className = `note-item flex items-center px-2 py-2 rounded cursor-pointer ${note.id === currentNoteId ? 'bg-blue-50 text-blue-600' : 'hover:bg-gray-100'}`; | |
noteEl.innerHTML = ` | |
<div class="flex-1 truncate"> | |
<div class="font-medium truncate">${note.title}</div> | |
<div class="text-xs text-gray-500 truncate">${formatDate(note.updatedAt)}</div> | |
</div> | |
<button class="note-pin-btn p-1 text-gray-400 hover:text-yellow-500 rounded-full ml-2"> | |
<i class="fas fa-thumbtack ${note.pinned ? 'text-yellow-500' : ''}"></i> | |
</button> | |
`; | |
noteEl.addEventListener('click', () => { | |
currentNoteId = note.id; | |
renderNote(note); | |
renderNotesList(); | |
}); | |
const pinBtn = noteEl.querySelector('.note-pin-btn'); | |
pinBtn.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
note.pinned = !note.pinned; | |
saveNotes(); | |
renderNotesList(); | |
}); | |
return noteEl; | |
} | |
function renderNote(note) { | |
// Update title | |
noteTitle.textContent = note.title; | |
// Clear editor | |
noteEditor.innerHTML = ''; | |
// Parse and render content | |
let blocks = []; | |
try { | |
blocks = JSON.parse(note.content || '[]'); | |
} catch (e) { | |
blocks = []; | |
} | |
if (blocks.length === 0) { | |
// Default empty block | |
addBlock('paragraph'); | |
} else { | |
blocks.forEach(block => { | |
addBlock(block.type, block.content); | |
}); | |
} | |
// Focus the first block | |
const firstBlock = noteEditor.querySelector('.block-content'); | |
if (firstBlock) { | |
firstBlock.focus(); | |
} | |
updateCounts(); | |
} | |
function addBlock(type, content = '') { | |
const blockEl = document.createElement('div'); | |
blockEl.className = 'note-block relative my-2 group'; | |
blockEl.dataset.blockType = type; | |
let blockContent = ''; | |
let placeholder = 'Type \'/\' for commands or start writing...'; | |
switch (type) { | |
case 'heading1': | |
blockContent = `<h1>${content || ''}</h1>`; | |
placeholder = 'Heading 1'; | |
break; | |
case 'heading2': | |
blockContent = `<h2>${content || ''}</h2>`; | |
placeholder = 'Heading 2'; | |
break; | |
case 'bulletedList': | |
blockContent = `<ul><li>${content || ''}</li></ul>`; | |
placeholder = 'List item'; | |
break; | |
case 'numberedList': | |
blockContent = `<ol><li>${content || ''}</li></ol>`; | |
placeholder = 'List item'; | |
break; | |
case 'checklist': | |
blockContent = `<div class="flex items-center"><input type="checkbox" class="mr-2"><span>${content || ''}</span></div>`; | |
placeholder = 'Todo item'; | |
break; | |
case 'quote': | |
blockContent = `<blockquote>${content || ''}</blockquote>`; | |
placeholder = 'Quote'; | |
break; | |
case 'code': | |
blockContent = `<pre><code>${content || ''}</code></pre>`; | |
placeholder = 'Code'; | |
break; | |
case 'image': | |
blockContent = `<div class="image-block"> | |
<div class="flex items-center justify-center border-2 border-dashed border-gray-300 rounded p-4"> | |
<button class="bg-blue-500 hover:bg-blue-600 text-white py-1 px-3 rounded text-sm"> | |
<i class="fas fa-upload mr-1"></i> Upload Image | |
</button> | |
</div> | |
</div>`; | |
break; | |
case 'table': | |
blockContent = `<div class="table-block"> | |
<table class="border-collapse w-full"> | |
<thead> | |
<tr> | |
<th class="border border-gray-300 p-2 bg-gray-100" contenteditable="true">Header 1</th> | |
<th class="border border-gray-300 p-2 bg-gray-100" contenteditable="true">Header 2</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td class="border border-gray-300 p-2" contenteditable="true">Cell 1</td> | |
<td class="border border-gray-300 p-2" contenteditable="true">Cell 2</td> | |
</tr> | |
</tbody> | |
</table> | |
<div class="mt-2 flex"> | |
<button class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded mr-1"> | |
<i class="fas fa-plus mr-1"></i> Row | |
</button> | |
<button class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded mr-1"> | |
<i class="fas fa-plus mr-1"></i> Column | |
</button> | |
<button class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded"> | |
<i class="fas fa-trash mr-1"></i> Delete | |
</button> | |
</div> | |
</div>`; | |
break; | |
default: | |
blockContent = `<p>${content || ''}</p>`; | |
} | |
blockEl.innerHTML = ` | |
<div class="block-menu absolute -left-8 top-0 text-gray-400 flex flex-col opacity-0 group-hover:opacity-100 transition-opacity"> | |
<button class="drag-handle p-1 hover:text-gray-600 hover:bg-gray-100 rounded"> | |
<i class="fas fa-grip-vertical"></i> | |
</button> | |
<button class="delete-block p-1 hover:text-red-500 hover:bg-gray-100 rounded"> | |
<i class="fas fa-trash"></i> | |
</button> | |
</div> | |
<div class="block-content" contenteditable="true" data-placeholder="${placeholder}"> | |
${blockContent} | |
</div> | |
`; | |
noteEditor.appendChild(blockEl); | |
// Set up event listeners for the new block | |
const blockContentEl = blockEl.querySelector('.block-content'); | |
const deleteBtn = blockEl.querySelector('.delete-block'); | |
deleteBtn.addEventListener('click', () => { | |
blockEl.remove(); | |
saveCurrentNote(); | |
}); | |
blockContentEl.addEventListener('focus', () => { | |
blockEl.querySelector('.block-menu').classList.remove('opacity-0'); | |
}); | |
blockContentEl.addEventListener('blur', () => { | |
if (blockContentEl.innerHTML === '' || blockContentEl.innerHTML === '<br>') { | |
if (type === 'heading1') { | |
blockContentEl.innerHTML = '<h1><br></h1>'; | |
} else if (type === 'heading2') { | |
blockContentEl.innerHTML = '<h2><br></h2>'; | |
} else if (type === 'bulletedList') { | |
blockContentEl.innerHTML = '<ul><li><br></li></ul>'; | |
} else if (type === 'numberedList') { | |
blockContentEl.innerHTML = '<ol><li><br></li></ol>'; | |
} else if (type === 'quote') { | |
blockContentEl.innerHTML = '<blockquote><br></blockquote>'; | |
} else if (type === 'code') { | |
blockContentEl.innerHTML = '<pre><code><br></code></pre>'; | |
} else { | |
blockContentEl.innerHTML = '<p><br></p>'; | |
} | |
} | |
saveCurrentNote(); | |
}); | |
// Focus the new block if it's empty | |
if (content === '') { | |
setTimeout(() => { | |
blockContentEl.focus(); | |
// Move cursor to the end | |
const range = document.createRange(); | |
const selection = window.getSelection(); | |
range.selectNodeContents(blockContentEl); | |
range.collapse(false); | |
selection.removeAllRanges(); | |
selection.addRange(range); | |
}, 10); | |
} | |
return blockEl; | |
} | |
function changeBlockType(blockContentEl, newType) { | |
const blockEl = blockContentEl.closest('.note-block'); | |
const currentContent = blockContentEl.innerHTML; | |
// Remove all child nodes to get clean text content | |
const tempDiv = document.createElement('div'); | |
tempDiv.innerHTML = currentContent; | |
const textContent = tempDiv.textContent || ''; | |
blockEl.dataset.blockType = newType; | |
switch (newType) { | |
case 'heading1': | |
blockContentEl.innerHTML = `<h1>${textContent}</h1>`; | |
break; | |
case 'heading2': | |
blockContentEl.innerHTML = `<h2>${textContent}</h2>`; | |
break; | |
case 'bulletedList': | |
blockContentEl.innerHTML = `<ul><li>${textContent}</li></ul>`; | |
break; | |
case 'numberedList': | |
blockContentEl.innerHTML = `<ol><li>${textContent}</li></ol>`; | |
break; | |
case 'checklist': | |
blockContentEl.innerHTML = `<div class="flex items-center"><input type="checkbox" class="mr-2"><span>${textContent}</span></div>`; | |
break; | |
case 'quote': | |
blockContentEl.innerHTML = `<blockquote>${textContent}</blockquote>`; | |
break; | |
case 'code': | |
blockContentEl.innerHTML = `<pre><code>${textContent}</code></pre>`; | |
break; | |
default: | |
blockContentEl.innerHTML = `<p>${textContent}</p>`; | |
} | |
saveCurrentNote(); | |
blockContentEl.focus(); | |
} | |
function showBlockMenu(blockContentEl) { | |
hideBlockMenu(); | |
const rect = blockContentEl.getBoundingClientRect(); | |
blockMenu.style.top = `${rect.top + window.scrollY}px`; | |
blockMenu.style.left = `${rect.left + window.scrollX}px`; | |
blockMenu.dataset.targetBlock = blockContentEl.id || Date.now().toString(); | |
blockContentEl.id = blockMenu.dataset.targetBlock; | |
blockMenu.classList.remove('hidden'); | |
} | |
function hideBlockMenu() { | |
blockMenu.classList.add('hidden'); | |
delete blockMenu.dataset.targetBlock; | |
} | |
function applyFormat(format) { | |
document.execCommand(format, false, null); | |
// Special handling for headings and other complex formats | |
const selection = window.getSelection(); | |
if (!selection.rangeCount) return; | |
const range = selection.getRangeAt(0); | |
const blockContentEl = range.startContainer.closest('.block-content'); | |
if (!blockContentEl) return; | |
switch (format) { | |
case 'heading1': | |
changeBlockType(blockContentEl, 'heading1'); | |
break; | |
case 'heading2': | |
changeBlockType(blockContentEl, 'heading2'); | |
break; | |
case 'bulletedList': | |
changeBlockType(blockContentEl, 'bulletedList'); | |
break; | |
case 'numberedList': | |
changeBlockType(blockContentEl, 'numberedList'); | |
break; | |
case 'checklist': | |
changeBlockType(blockContentEl, 'checklist'); | |
break; | |
case 'quote': | |
changeBlockType(blockContentEl, 'quote'); | |
break; | |
case 'code': | |
changeBlockType(blockContentEl, 'code'); | |
break; | |
} | |
} | |
function handleEnterKey(blockContentEl, event) { | |
const blockEl = blockContentEl.closest('.note-block'); | |
const blockType = blockEl.dataset.blockType; | |
// For lists, continue the same list type | |
if (blockType === 'bulletedList' || blockType === 'numberedList') { | |
// If the list item is empty, break out of the list | |
if (blockContentEl.textContent.trim() === '') { | |
event.preventDefault(); | |
changeBlockType(blockContentEl, 'paragraph'); | |
} else { | |
// Otherwise continue the list | |
event.preventDefault(); | |
const newBlock = addBlock(blockType); | |
setTimeout(() => { | |
newBlock.querySelector('.block-content').focus(); | |
}, 10); | |
} | |
} else { | |
// For other blocks, just add a new paragraph | |
event.preventDefault(); | |
const newBlock = addBlock('paragraph'); | |
setTimeout(() => { | |
newBlock.querySelector('.block-content').focus(); | |
}, 10); | |
} | |
} | |
function handleBackspaceKey(blockContentEl, event) { | |
// Only handle if the block is empty | |
if (blockContentEl.textContent.trim() !== '') return; | |
const blockEl = blockContentEl.closest('.note-block'); | |
const blockType = blockEl.dataset.blockType; | |
// For headings, convert to paragraph | |
if (blockType === 'heading1' || blockType === 'heading2') { | |
event.preventDefault(); | |
changeBlockType(blockContentEl, 'paragraph'); | |
} | |
// For lists, convert to paragraph | |
if (blockType === 'bulletedList' || blockType === 'numberedList' || blockType === 'checklist') { | |
event.preventDefault(); | |
changeBlockType(blockContentEl, 'paragraph'); | |
} | |
// For quotes and code blocks, convert to paragraph | |
if (blockType === 'quote' || blockType === 'code') { | |
event.preventDefault(); | |
changeBlockType(blockContentEl, 'paragraph'); | |
} | |
} | |
function updateCounts() { | |
const text = noteEditor.textContent; | |
const wordCount = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; | |
const charCount = text.length; | |
wordCountEl.textContent = `${wordCount} ${wordCount === 1 ? 'word' : 'words'}`; | |
charCountEl.textContent = `${charCount} ${charCount === 1 ? 'character' : 'characters'}`; | |
} | |
function toggleDarkMode() { | |
isDarkMode = !isDarkMode; | |
if (isDarkMode) { | |
document.body.classList.add('bg-gray-900', 'text-gray-100'); | |
document.body.classList.remove('bg-gray-50', 'text-gray-800'); | |
toggleDarkModeBtn.innerHTML = '<i class="fas fa-sun"></i>'; | |
// Update sidebar | |
sidebar.classList.add('bg-gray-800', 'border-gray-700'); | |
sidebar.classList.remove('bg-white', 'border-gray-200'); | |
// Update editor | |
noteEditor.classList.add('bg-gray-900'); | |
noteEditor.classList.remove('bg-gray-50'); | |
} else { | |
document.body.classList.remove('bg-gray-900', 'text-gray-100'); | |
document.body.classList.add('bg-gray-50', 'text-gray-800'); | |
toggleDarkModeBtn.innerHTML = '<i class="fas fa-moon"></i>'; | |
// Update sidebar | |
sidebar.classList.remove('bg-gray-800', 'border-gray-700'); | |
sidebar.classList.add('bg-white', 'border-gray-200'); | |
// Update editor | |
noteEditor.classList.remove('bg-gray-900'); | |
noteEditor.classList.add('bg-gray-50'); | |
} | |
} | |
function formatDate(dateString) { | |
const date = new Date(dateString); | |
return date.toLocaleDateString('en-US', { | |
month: 'short', | |
day: 'numeric', | |
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined | |
}); | |
} | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=sumitjangir/notion-lite" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |