Mercurial
annotate rich_editor/rich_editor.js @ 204:e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 11:02:13 -0800 |
| parents | 6cdee35a7ba9 |
| children | 240337164a80 |
| rev | line source |
|---|---|
| 201 | 1 /** |
| 2 * Rich Editor | |
| 3 * ----------- | |
| 4 * A vanilla JavaScript rich text editor with: | |
| 5 * - Text formatting (h1-h6, paragraphs, lists) | |
| 6 * - Image/file upload via paste, drop, click, or /upload command | |
| 7 * - Debounced auto-save | |
| 8 * - Generic callback functions for uploads and saves | |
| 9 * | |
| 10 * Usage: | |
| 11 * const editor = RichEditor.init('editor-container', { | |
| 12 * uploadCallback: async (file) => { return { url: '...', key: '...' }; }, | |
| 13 * saveCallback: async (content) => { console.log('Saved:', content); }, | |
| 14 * debounceMs: 1000, | |
| 15 * placeholder: 'Start writing...' | |
| 16 * }); | |
| 17 */ | |
| 18 | |
| 19 const RichEditor = (function() { | |
| 20 'use strict'; | |
| 21 | |
| 22 const DEFAULT_OPTIONS = { | |
| 23 uploadCallback: null, | |
| 24 saveCallback: null, | |
| 25 debounceMs: 1000, | |
| 26 placeholder: 'Start writing... (Use /upload to insert files)', | |
| 27 cloudFrontUrl: '' | |
| 28 }; | |
| 29 | |
| 30 function debounce(func, wait) { | |
| 31 let timeout; | |
| 32 return function executedFunction(...args) { | |
| 33 const later = () => { | |
| 34 clearTimeout(timeout); | |
| 35 func(...args); | |
| 36 }; | |
| 37 clearTimeout(timeout); | |
| 38 timeout = setTimeout(later, wait); | |
| 39 }; | |
| 40 } | |
| 41 | |
| 42 function createToolbar(editor) { | |
| 43 const toolbar = document.createElement('div'); | |
| 44 toolbar.className = 'rich-editor-toolbar'; | |
| 45 | |
| 46 const buttons = [ | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
47 { cmd: 'h1', label: '', title: 'Heading 1', icons: "/public/icons/h1.png" }, |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
48 { cmd: 'h2', label: '', title: 'Heading 2', icons: "/public/icons/h2.png" }, |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
49 { cmd: 'h3', label: '', title: 'Heading 3', icons: "/public/icons/h3.png" }, |
| 201 | 50 { cmd: 'p', label: '¶', title: 'Paragraph' }, |
| 51 { cmd: 'ul', label: '•', title: 'Bullet List' }, | |
| 52 { cmd: 'ol', label: '1.', title: 'Numbered List' }, | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
53 { cmd: 'bold', label: '', title: 'Bold', icons: "/public/icons/bold.png" }, |
| 201 | 54 { cmd: 'separator' }, |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
55 { cmd: 'link', label: '', title: 'Insert Link', icons: "/public/icons/link.png" }, |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
56 { cmd: 'notelink', label: '', title: 'Link to Note', icons: "/public/icons/binder.png" }, |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
57 { cmd: 'upload', label: '', title: 'Upload File', icons: "/public/icons/clipy.png" }, |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
58 { cmd: 'separator' }, |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
59 { cmd: 'readonly', label: '', title: 'readonly', icons: "/public/icons/book.png" }, |
| 201 | 60 ]; |
| 61 | |
| 62 buttons.forEach(btn => { | |
| 63 if (btn.cmd === 'separator') { | |
| 64 const sep = document.createElement('span'); | |
| 65 sep.className = 'rich-editor-separator'; | |
| 66 toolbar.appendChild(sep); | |
| 67 return; | |
| 68 } | |
| 69 | |
| 70 const button = document.createElement('button'); | |
| 71 button.type = 'button'; | |
| 72 button.className = 'rich-editor-btn'; | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
73 if (btn.icons) |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
74 button.style.backgroundImage = `url("${btn.icons}")`; |
| 201 | 75 button.textContent = btn.label; |
| 76 button.title = btn.title; | |
| 77 button.dataset.cmd = btn.cmd; | |
| 78 | |
| 79 button.addEventListener('click', (e) => { | |
| 80 e.preventDefault(); | |
| 81 editor.execCommand(btn.cmd); | |
| 82 }); | |
| 83 | |
| 84 toolbar.appendChild(button); | |
| 85 }); | |
| 86 | |
| 87 return toolbar; | |
| 88 } | |
| 89 | |
| 90 function createStyles() { | |
| 91 if (document.getElementById('rich-editor-styles')) return; | |
| 92 | |
| 93 const style = document.createElement('style'); | |
| 94 style.id = 'rich-editor-styles'; | |
| 95 style.textContent = ` | |
| 96 .rich-editor-container { | |
| 97 border: 1px solid #ccc; | |
| 98 border-radius: 4px; | |
| 99 overflow: hidden; | |
| 100 } | |
| 101 | |
| 102 .rich-editor-toolbar { | |
| 103 display: flex; | |
| 104 flex-wrap: wrap; | |
| 105 gap: 4px; | |
| 106 padding: 8px; | |
| 107 background: #f5f5f5; | |
| 108 border-bottom: 1px solid #ccc; | |
| 109 } | |
| 110 | |
| 111 .rich-editor-btn { | |
| 112 padding: 6px 12px; | |
| 113 border: 1px solid #ccc; | |
| 114 border-radius: 4px; | |
| 115 background: #fff; | |
| 116 cursor: pointer; | |
| 117 font-size: 14px; | |
| 118 min-width: 32px; | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
119 background-repeat: no-repeat; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
120 background-position: center; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
121 background-size: 24px 24px; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
122 border: none; |
| 201 | 123 } |
| 124 | |
| 125 .rich-editor-btn:hover { | |
| 126 background: #e9e9e9; | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
127 background-repeat: no-repeat; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
128 background-position: center; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
129 background-size: 24px 24px; |
| 201 | 130 } |
| 131 | |
| 132 .rich-editor-btn:active { | |
| 133 background: #ddd; | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
134 background-repeat: no-repeat; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
135 background-position: center; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
136 background-size: 24px 24px; |
| 201 | 137 } |
| 138 | |
| 139 .rich-editor-separator { | |
| 140 width: 1px; | |
| 141 background: #ccc; | |
| 142 margin: 0 4px; | |
| 143 } | |
| 144 | |
| 145 .rich-editor-content { | |
| 146 min-height: 300px; | |
| 147 padding: 16px; | |
| 148 outline: none; | |
| 149 overflow-y: auto; | |
| 150 } | |
| 151 | |
| 152 .rich-editor-content:empty:before { | |
| 153 content: attr(data-placeholder); | |
| 154 color: #999; | |
| 155 pointer-events: none; | |
| 156 } | |
| 157 | |
| 158 .rich-editor-content img { | |
| 159 max-width: 100%; | |
| 160 height: auto; | |
| 161 border-radius: 4px; | |
| 162 margin: 8px 0; | |
| 163 } | |
| 164 | |
| 165 .rich-editor-content a { | |
| 166 color: #0078ff; | |
| 167 text-decoration: none; | |
| 168 } | |
| 169 | |
| 170 .rich-editor-content a:hover { | |
| 171 text-decoration: underline; | |
| 172 } | |
| 173 | |
| 174 .rich-editor-content a.note-link { | |
| 175 background: #e8f4ff; | |
| 176 padding: 2px 6px; | |
| 177 border-radius: 3px; | |
| 178 } | |
| 179 | |
| 180 .rich-editor-content a.note-link:hover { | |
| 181 background: #d0e8ff; | |
| 182 } | |
| 183 | |
| 184 .rich-editor-content .upload-placeholder { | |
| 185 display: inline-block; | |
| 186 padding: 8px 16px; | |
| 187 background: #f0f0f0; | |
| 188 border-radius: 4px; | |
| 189 color: #666; | |
| 190 font-style: italic; | |
| 191 } | |
| 192 | |
| 193 .rich-editor-content .upload-placeholder.loading { | |
| 194 padding: 8px 16px; | |
| 195 background: #fffacd; | |
| 196 border-radius: 4px; | |
| 197 animation: pulse 1.5s ease-in-out infinite; | |
| 198 } | |
| 199 | |
| 200 @keyframes pulse { | |
| 201 0%, 100% { opacity: 1; } | |
| 202 50% { opacity: 0.6; } | |
| 203 } | |
| 204 | |
| 205 .rich-editor-upload-overlay { | |
| 206 position: absolute; | |
| 207 top: 0; | |
| 208 left: 0; | |
| 209 right: 0; | |
| 210 bottom: 0; | |
| 211 background: rgba(0, 120, 255, 0.1); | |
| 212 border: 2px dashed #0078ff; | |
| 213 display: flex; | |
| 214 align-items: center; | |
| 215 justify-content: center; | |
| 216 font-size: 18px; | |
| 217 color: #0078ff; | |
| 218 pointer-events: none; | |
| 219 z-index: 10; | |
| 220 } | |
| 221 | |
| 222 .rich-editor-status { | |
| 223 padding: 4px 8px; | |
| 224 font-size: 12px; | |
| 225 color: #666; | |
| 226 background: #f9f9f9; | |
| 227 border-top: 1px solid #eee; | |
| 228 } | |
| 229 | |
| 230 .rich-editor-file-input { | |
| 231 display: none; | |
| 232 } | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
233 |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
234 ol,ul { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
235 padding: 16px; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
236 } |
| 201 | 237 `; |
| 238 document.head.appendChild(style); | |
| 239 } | |
| 240 | |
| 241 class Editor { | |
| 242 constructor(elementId, options) { | |
| 243 this.options = { ...DEFAULT_OPTIONS, ...options }; | |
| 244 this.container = document.getElementById(elementId); | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
245 this.state = { readOnly: false }; |
| 201 | 246 |
| 247 if (!this.container) { | |
| 248 throw new Error(`Element with id "${elementId}" not found`); | |
| 249 } | |
| 250 | |
| 251 this.init(); | |
| 252 } | |
| 253 | |
| 254 init() { | |
| 255 createStyles(); | |
| 256 | |
| 257 // Create wrapper | |
| 258 this.wrapper = document.createElement('div'); | |
| 259 this.wrapper.className = 'rich-editor-container'; | |
| 260 this.wrapper.style.position = 'relative'; | |
| 261 | |
| 262 // Create toolbar | |
| 263 this.toolbar = createToolbar(this); | |
| 264 this.wrapper.appendChild(this.toolbar); | |
| 265 | |
| 266 // Create content area | |
| 267 this.content = document.createElement('div'); | |
| 268 this.content.className = 'rich-editor-content'; | |
| 269 this.content.contentEditable = true; | |
| 270 this.content.dataset.placeholder = this.options.placeholder; | |
| 271 this.wrapper.appendChild(this.content); | |
| 272 | |
| 273 // Create status bar | |
| 274 this.status = document.createElement('div'); | |
| 275 this.status.className = 'rich-editor-status'; | |
| 276 this.status.textContent = 'Ready'; | |
| 277 this.wrapper.appendChild(this.status); | |
| 278 | |
| 279 // Create hidden file input | |
| 280 this.fileInput = document.createElement('input'); | |
| 281 this.fileInput.type = 'file'; | |
| 282 this.fileInput.className = 'rich-editor-file-input'; | |
| 283 this.fileInput.multiple = true; | |
| 284 this.fileInput.accept = 'image/*,application/pdf,.doc,.docx,.txt'; | |
| 285 this.wrapper.appendChild(this.fileInput); | |
| 286 | |
| 287 // Add to container | |
| 288 this.container.appendChild(this.wrapper); | |
| 289 | |
| 290 // Setup event listeners | |
| 291 this.setupEvents(); | |
| 292 | |
| 293 // Setup debounced save | |
| 294 if (this.options.saveCallback) { | |
| 295 this.debouncedSave = debounce(() => { | |
| 296 this.save(); | |
| 297 }, this.options.debounceMs); | |
| 298 } | |
| 299 } | |
| 300 | |
| 301 setupEvents() { | |
| 302 // Input events for auto-save | |
| 303 this.content.addEventListener('input', () => { | |
| 304 if (this.debouncedSave) { | |
| 305 this.setStatus('Editing...'); | |
| 306 this.debouncedSave(); | |
| 307 } | |
| 308 }); | |
| 309 | |
| 310 // Handle paste | |
| 311 this.content.addEventListener('paste', (e) => { | |
| 312 const items = e.clipboardData?.items; | |
| 313 if (!items) return; | |
| 314 | |
| 315 for (const item of items) { | |
| 316 if (item.type.startsWith('image/') || item.kind === 'file') { | |
| 317 e.preventDefault(); | |
| 318 const file = item.getAsFile(); | |
| 319 if (file) this.uploadFile(file); | |
| 320 return; | |
| 321 } | |
| 322 } | |
| 323 }); | |
| 324 | |
| 325 // Handle drop | |
| 326 this.content.addEventListener('dragover', (e) => { | |
| 327 e.preventDefault(); | |
| 328 this.showDropOverlay(); | |
| 329 }); | |
| 330 | |
| 331 this.content.addEventListener('dragleave', (e) => { | |
| 332 e.preventDefault(); | |
| 333 this.hideDropOverlay(); | |
| 334 }); | |
| 335 | |
| 336 this.content.addEventListener('drop', (e) => { | |
| 337 e.preventDefault(); | |
| 338 this.hideDropOverlay(); | |
| 339 | |
| 340 const files = e.dataTransfer?.files; | |
| 341 if (files && files.length > 0) { | |
| 342 for (const file of files) { | |
| 343 this.uploadFile(file); | |
| 344 } | |
| 345 } | |
| 346 }); | |
| 347 | |
| 348 // Handle /upload command | |
| 349 this.content.addEventListener('keydown', (e) => { | |
| 350 if (e.key === 'Enter') { | |
| 351 const selection = window.getSelection(); | |
| 352 const node = selection.anchorNode; | |
| 353 if (node && node.textContent) { | |
| 354 const text = node.textContent; | |
| 355 if (text.trim() === '/upload') { | |
| 356 e.preventDefault(); | |
| 357 node.textContent = ''; | |
| 358 this.triggerFileUpload(); | |
| 359 } | |
| 360 } | |
| 361 } | |
| 362 }); | |
| 363 | |
| 364 // File input change | |
| 365 this.fileInput.addEventListener('change', (e) => { | |
| 366 const files = e.target.files; | |
| 367 if (files && files.length > 0) { | |
| 368 for (const file of files) { | |
| 369 this.uploadFile(file); | |
| 370 } | |
| 371 } | |
| 372 this.fileInput.value = ''; | |
| 373 }); | |
| 374 | |
| 375 // Handle keyboard shortcuts | |
| 376 this.content.addEventListener('keydown', (e) => { | |
| 377 if (e.ctrlKey || e.metaKey) { | |
| 378 switch (e.key.toLowerCase()) { | |
| 379 case 'b': | |
| 380 e.preventDefault(); | |
| 381 this.execCommand('bold'); | |
| 382 break; | |
| 383 case 'i': | |
| 384 e.preventDefault(); | |
| 385 this.execCommand('italic'); | |
| 386 break; | |
| 387 case 's': | |
| 388 e.preventDefault(); | |
| 389 this.save(); | |
| 390 break; | |
| 391 } | |
| 392 } | |
| 393 }); | |
| 394 } | |
| 395 | |
| 396 showDropOverlay() { | |
| 397 if (this.dropOverlay) return; | |
| 398 | |
| 399 this.dropOverlay = document.createElement('div'); | |
| 400 this.dropOverlay.className = 'rich-editor-upload-overlay'; | |
| 401 this.dropOverlay.textContent = 'Drop files here to upload'; | |
| 402 this.wrapper.appendChild(this.dropOverlay); | |
| 403 } | |
| 404 | |
| 405 hideDropOverlay() { | |
| 406 if (this.dropOverlay) { | |
| 407 this.dropOverlay.remove(); | |
| 408 this.dropOverlay = null; | |
| 409 } | |
| 410 } | |
| 411 | |
| 412 triggerFileUpload() { | |
| 413 this.fileInput.click(); | |
| 414 } | |
| 415 | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
416 readOnly() { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
417 this.state.readOnly = !this.state.readOnly; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
418 |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
419 if (this.state.readOnly) |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
420 { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
421 this.content.contentEditable = false; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
422 this.setStatus('Read-only mode'); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
423 const buttons = this.toolbar.querySelectorAll('.rich-editor-btn'); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
424 buttons.forEach(btn => { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
425 if (btn.dataset.cmd !== 'readonly') { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
426 btn.disabled = true; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
427 } |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
428 }); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
429 } |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
430 else |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
431 { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
432 // Enable editing |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
433 this.content.contentEditable = true; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
434 this.content.style.backgroundColor = ''; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
435 this.setStatus('Ready'); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
436 |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
437 // Enable toolbar buttons |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
438 const buttons = this.toolbar.querySelectorAll('.rich-editor-btn'); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
439 buttons.forEach(btn => { |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
440 btn.disabled = false; |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
441 }); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
442 } |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
443 } |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
444 |
| 201 | 445 async uploadFile(file) { |
| 446 if (!this.options.uploadCallback) { | |
| 447 console.warn('No upload callback configured'); | |
| 448 return; | |
| 449 } | |
| 450 | |
| 451 // Insert loading placeholder | |
| 452 const placeholder = document.createElement('div'); | |
| 453 placeholder.className = 'upload-placeholder loading'; | |
| 454 placeholder.textContent = file.type.startsWith('image/') | |
| 455 ? `Processing ${file.name}... ⏳` | |
| 456 : `Uploading ${file.name}...`; | |
| 457 | |
| 458 this.insertAtCursor(placeholder); | |
| 459 | |
| 460 this.setStatus(`Uploading ${file.name}...`); | |
| 461 | |
| 462 try { | |
| 463 const result = await this.options.uploadCallback(file); | |
| 464 | |
| 465 if (result && result.url) { | |
| 466 // Replace placeholder with actual content | |
| 467 if (file.type.startsWith('image/')) { | |
| 468 const img = document.createElement('img'); | |
| 469 img.src = result.url; | |
| 470 img.alt = file.name; | |
| 471 placeholder.replaceWith(img); | |
| 472 } else { | |
| 473 const link = document.createElement('a'); | |
| 474 link.href = result.url; | |
| 475 link.textContent = file.name; | |
| 476 link.target = '_blank'; | |
| 477 placeholder.replaceWith(link); | |
| 478 } | |
| 479 | |
| 480 this.setStatus(`Uploaded ${file.name}`); | |
| 481 if (this.debouncedSave) this.debouncedSave(); | |
| 482 } else { | |
| 483 placeholder.textContent = `Failed to upload ${file.name}`; | |
| 484 placeholder.className = 'upload-placeholder'; | |
| 485 this.setStatus('Upload failed'); | |
| 486 } | |
| 487 } catch (error) { | |
| 488 console.error('Upload error:', error); | |
| 489 placeholder.textContent = `Error: ${error.message}`; | |
| 490 placeholder.className = 'upload-placeholder'; | |
| 491 this.setStatus('Upload error'); | |
| 492 } | |
| 493 } | |
| 494 | |
| 495 insertAtCursor(element) { | |
| 496 const selection = window.getSelection(); | |
| 497 if (selection.rangeCount > 0) { | |
| 498 const range = selection.getRangeAt(0); | |
| 499 range.deleteContents(); | |
| 500 range.insertNode(element); | |
| 501 range.setStartAfter(element); | |
| 502 range.collapse(true); | |
| 503 selection.removeAllRanges(); | |
| 504 selection.addRange(range); | |
| 505 } else { | |
| 506 this.content.appendChild(element); | |
| 507 } | |
| 508 this.content.focus(); | |
| 509 } | |
| 510 | |
| 511 execCommand(cmd) { | |
| 512 this.content.focus(); | |
| 513 | |
| 514 switch (cmd) { | |
| 515 case 'h1': | |
| 516 case 'h2': | |
| 517 case 'h3': | |
| 518 case 'h4': | |
| 519 case 'h5': | |
| 520 case 'h6': | |
| 521 document.execCommand('formatBlock', false, cmd); | |
| 522 break; | |
| 523 case 'p': | |
| 524 document.execCommand('formatBlock', false, 'p'); | |
| 525 break; | |
| 526 case 'ul': | |
| 527 document.execCommand('insertUnorderedList', false, null); | |
| 528 break; | |
| 529 case 'ol': | |
| 530 document.execCommand('insertOrderedList', false, null); | |
| 531 break; | |
| 532 case 'bold': | |
| 533 document.execCommand('bold', false, null); | |
| 534 break; | |
| 535 case 'italic': | |
| 536 document.execCommand('italic', false, null); | |
| 537 break; | |
| 538 case 'link': | |
| 539 this.insertLink(); | |
| 540 break; | |
| 541 case 'notelink': | |
| 542 this.insertNoteLink(); | |
| 543 break; | |
| 544 case 'upload': | |
| 545 this.triggerFileUpload(); | |
| 546 break; | |
|
204
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
547 case 'readonly': |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
548 this.readOnly(); |
|
e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
MrJuneJune <me@mrjunejune.com>
parents:
201
diff
changeset
|
549 break; |
| 201 | 550 } |
| 551 | |
| 552 if (this.debouncedSave) this.debouncedSave(); | |
| 553 } | |
| 554 | |
| 555 insertLink() { | |
| 556 const url = prompt('Enter URL:'); | |
| 557 if (!url) return; | |
| 558 | |
| 559 const selection = window.getSelection(); | |
| 560 const selectedText = selection.toString() || url; | |
| 561 | |
| 562 const link = document.createElement('a'); | |
| 563 link.href = url; | |
| 564 link.textContent = selectedText; | |
| 565 link.target = '_blank'; | |
| 566 | |
| 567 if (selection.rangeCount > 0) { | |
| 568 const range = selection.getRangeAt(0); | |
| 569 range.deleteContents(); | |
| 570 range.insertNode(link); | |
| 571 range.setStartAfter(link); | |
| 572 range.collapse(true); | |
| 573 selection.removeAllRanges(); | |
| 574 selection.addRange(range); | |
| 575 } | |
| 576 } | |
| 577 | |
| 578 insertNoteLink() { | |
| 579 const noteId = prompt('Enter note ID (e.g., my-ideas):'); | |
| 580 if (!noteId) return; | |
| 581 | |
| 582 const sanitizedId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); | |
| 583 const selection = window.getSelection(); | |
| 584 const selectedText = selection.toString() || sanitizedId; | |
| 585 | |
| 586 const link = document.createElement('a'); | |
| 587 link.href = '/notes/' + encodeURIComponent(sanitizedId); | |
| 588 link.textContent = selectedText; | |
| 589 link.className = 'note-link'; | |
| 590 | |
| 591 if (selection.rangeCount > 0) { | |
| 592 const range = selection.getRangeAt(0); | |
| 593 range.deleteContents(); | |
| 594 range.insertNode(link); | |
| 595 range.setStartAfter(link); | |
| 596 range.collapse(true); | |
| 597 selection.removeAllRanges(); | |
| 598 selection.addRange(range); | |
| 599 } | |
| 600 } | |
| 601 | |
| 602 setStatus(text) { | |
| 603 this.status.textContent = text; | |
| 604 } | |
| 605 | |
| 606 async save() { | |
| 607 if (!this.options.saveCallback) return; | |
| 608 | |
| 609 this.setStatus('Saving...'); | |
| 610 | |
| 611 try { | |
| 612 await this.options.saveCallback(this.getContent()); | |
| 613 this.setStatus('Saved'); | |
| 614 } catch (error) { | |
| 615 console.error('Save error:', error); | |
| 616 this.setStatus('Save failed'); | |
| 617 } | |
| 618 } | |
| 619 | |
| 620 getContent() { | |
| 621 return this.content.innerHTML; | |
| 622 } | |
| 623 | |
| 624 setContent(html) { | |
| 625 this.content.innerHTML = html; | |
| 626 } | |
| 627 | |
| 628 getText() { | |
| 629 return this.content.textContent; | |
| 630 } | |
| 631 | |
| 632 clear() { | |
| 633 this.content.innerHTML = ''; | |
| 634 } | |
| 635 | |
| 636 focus() { | |
| 637 this.content.focus(); | |
| 638 } | |
| 639 } | |
| 640 | |
| 641 return { | |
| 642 init: function(elementId, options) { | |
| 643 return new Editor(elementId, options); | |
| 644 } | |
| 645 }; | |
| 646 })(); | |
| 647 | |
| 648 // Export for module systems | |
| 649 if (typeof module !== 'undefined' && module.exports) { | |
| 650 module.exports = RichEditor; | |
| 651 } |