From 42ad089b04e6e853ce81ea77f9611bd086d45ed8 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Sun, 12 Apr 2026 18:46:35 +0200 Subject: [PATCH] added continuation features --- documentation/FEATURES.md | 1 + frontend/app.js | 25 ++++++ frontend/editor-markdown-continue.js | 124 +++++++++++++++++++++++++++ frontend/index.html | 2 + 4 files changed, 152 insertions(+) create mode 100644 frontend/editor-markdown-continue.js diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md index e453aa1..7e53e1e 100644 --- a/documentation/FEATURES.md +++ b/documentation/FEATURES.md @@ -32,6 +32,7 @@ - **Visual tree view** - Expandable/collapsible navigation - **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar - **Tab inserts tab** - Toggle to make Tab key insert a tab character in the editor instead of changing focus +- **List & quote continuation (Enter)** - At the end of a line, Enter continues blockquotes (`>`), bullets (`-` / `*` / `+`), and task lists (`- [ ]` / `- [x]`); pressing Enter again on an empty continued line exits the list or quote. Disabled inside fenced code blocks and when using Shift+Enter (plain newline) ### Export & Print - **HTML Export** - Download notes as standalone HTML files with all styling, images, diagrams, and math embedded diff --git a/frontend/app.js b/frontend/app.js index 7fced06..ae22d18 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -857,6 +857,31 @@ function noteApp() { this.autoSave(); }, + /** + * Enter: continue blockquote / bullet / task list; second Enter on empty item exits (see editor-markdown-continue.js). + */ + handleEditorEnterKey(event) { + if (typeof EditorMarkdownContinue === 'undefined') return; + const textarea = event.target; + if (!textarea || textarea.id !== 'note-editor') return; + + const result = EditorMarkdownContinue.tryEnter( + this.noteContent, + textarea.selectionStart, + textarea.selectionEnd, + event + ); + if (!result.handled) return; + + event.preventDefault(); + this.noteContent = result.text; + this.$nextTick(() => { + textarea.selectionStart = textarea.selectionEnd = result.cursor; + }); + this.autoSave(); + this.updateSyntaxHighlight(); + }, + // Sort mode configuration sortModes: ['a-z', 'z-a', 'newest', 'oldest', 'largest', 'smallest'], sortModeIcons: { diff --git a/frontend/editor-markdown-continue.js b/frontend/editor-markdown-continue.js new file mode 100644 index 0000000..eac57c5 --- /dev/null +++ b/frontend/editor-markdown-continue.js @@ -0,0 +1,124 @@ +/** + * Markdown editor helpers: Enter continues blockquotes, bullets, and task lists. + * Plain functions only; no DOM. Used by app.js handleEditorEnterKey. + */ +(function (global) { + 'use strict'; + + /** + * True if cursor position lies inside a ``` fenced code block (toggle per line). + */ + function cursorInMarkdownCodeFence(text, pos) { + const before = text.slice(0, pos); + let lineStart = 0; + let inFence = false; + for (;;) { + const nl = before.indexOf('\n', lineStart); + const end = nl === -1 ? before.length : nl; + const line = before.slice(lineStart, end); + const trimmed = line.trimStart(); + if (trimmed.startsWith('```')) { + inFence = !inFence; + } + if (nl === -1) break; + lineStart = nl + 1; + } + return inFence; + } + + function getLineBounds(text, pos) { + const lineStart = text.lastIndexOf('\n', Math.max(0, pos - 1)) + 1; + let lineEnd = text.indexOf('\n', pos); + if (lineEnd === -1) lineEnd = text.length; + return { + lineStart, + lineEnd, + line: text.slice(lineStart, lineEnd), + }; + } + + /** + * @returns {{ continuePrefix: string, isEmpty: boolean } | null} + */ + function parseContinuationContext(line) { + const quoteMatch = line.match(/^(\s*)((?:>\s*)+)/); + let quoteFull = ''; + let rest = line; + if (quoteMatch) { + quoteFull = quoteMatch[1] + quoteMatch[2]; + rest = line.slice(quoteMatch[0].length); + } + + const taskMatch = rest.match(/^(\s*)([-*+])\s+\[([xX ])\]\s*(.*)$/); + if (taskMatch) { + const inner = taskMatch[1]; + const ch = taskMatch[2]; + const mark = taskMatch[3]; + const cont = taskMatch[4]; + const marker = ch + ' [' + mark + '] '; + const isEmpty = cont.trim() === ''; + return { continuePrefix: quoteFull + inner + marker, isEmpty }; + } + + const bulletMatch = rest.match(/^(\s*)([-*+])\s+(.*)$/); + if (bulletMatch) { + const inner = bulletMatch[1]; + const ch = bulletMatch[2]; + const cont = bulletMatch[3]; + const marker = ch + ' '; + const isEmpty = cont.trim() === ''; + return { continuePrefix: quoteFull + inner + marker, isEmpty }; + } + + if (quoteFull) { + const isEmpty = rest.trim() === ''; + return { continuePrefix: quoteFull, isEmpty }; + } + + return null; + } + + /** + * @param {string} text + * @param {number} selStart + * @param {number} selEnd + * @param {KeyboardEvent} event + * @returns {{ handled: false } | { handled: true, text: string, cursor: number }} + */ + function tryEnter(text, selStart, selEnd, event) { + if (event.key !== 'Enter') return { handled: false }; + if (event.defaultPrevented) return { handled: false }; + if (event.isComposing) return { handled: false }; + if (event.shiftKey) return { handled: false }; + if (event.ctrlKey || event.metaKey || event.altKey) return { handled: false }; + if (selStart !== selEnd) return { handled: false }; + if (cursorInMarkdownCodeFence(text, selStart)) return { handled: false }; + + const { lineStart, lineEnd, line } = getLineBounds(text, selStart); + if (selStart !== lineEnd) return { handled: false }; + + const lineForParse = line.replace(/\r/g, ''); + const ctx = parseContinuationContext(lineForParse); + if (!ctx) return { handled: false }; + + if (ctx.isEmpty) { + let delEnd = lineEnd; + if (delEnd < text.length && text.charAt(delEnd) === '\n') { + delEnd += 1; + } + const newText = text.slice(0, lineStart) + text.slice(delEnd); + return { handled: true, text: newText, cursor: lineStart }; + } + + const insert = '\n' + ctx.continuePrefix; + const newText = text.slice(0, lineEnd) + insert + text.slice(lineEnd); + return { handled: true, text: newText, cursor: lineEnd + insert.length }; + } + + global.EditorMarkdownContinue = { + tryEnter, + parseContinuationContext, + getLineBounds, + cursorInMarkdownCodeFence, + }; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/frontend/index.html b/frontend/index.html index 1e7aa1f..609f662 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2682,6 +2682,7 @@ x-model="noteContent" @input="autoSave(); updateSyntaxHighlight()" @scroll="syncOverlayScroll()" + @keydown.enter="handleEditorEnterKey($event)" @keydown.tab="handleTabKey($event)" @drop="onEditorDrop($event)" @dragover.prevent="onEditorDragOver($event)" @@ -3433,6 +3434,7 @@ +