From 4b65ebc58ccb61713adfc4d3b63ca00dfc381bf2 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Thu, 18 Dec 2025 14:16:04 +0100 Subject: [PATCH] added basic toolbar --- frontend/app.js | 121 ++++++++++++++++++++++++++++++++++++++++++++ frontend/index.html | 68 +++++++++++++++++++++++++ locales/de-DE.json | 16 ++++++ locales/en-US.json | 16 ++++++ locales/es-ES.json | 16 ++++++ locales/fr-FR.json | 16 ++++++ locales/sl-SI.json | 16 ++++++ locales/zh-CN.json | 16 ++++++ 8 files changed, 285 insertions(+) diff --git a/frontend/app.js b/frontend/app.js index ce3e3ff..2b1aba1 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -3307,6 +3307,127 @@ function noteApp() { this.autoSave(); }, + // Format selected text or insert formatting at cursor + formatText(type) { + const editor = document.getElementById('note-editor'); + if (!editor) return; + + const start = editor.selectionStart; + const end = editor.selectionEnd; + const selectedText = this.noteContent.substring(start, end); + const beforeText = this.noteContent.substring(0, start); + const afterText = this.noteContent.substring(end); + + let replacement = ''; + let cursorOffset = 0; + let selectLength = 0; + + // Check if at start of line for block-level elements + const atLineStart = beforeText.endsWith('\n') || beforeText === ''; + const needsNewline = !atLineStart && ['heading', 'quote', 'bullet', 'numbered', 'checkbox', 'codeblock'].includes(type); + const prefix = needsNewline ? '\n' : ''; + + switch (type) { + case 'bold': + replacement = selectedText ? `**${selectedText}**` : '**bold**'; + cursorOffset = selectedText ? 0 : 2; + selectLength = selectedText ? 0 : 4; + break; + case 'italic': + replacement = selectedText ? `*${selectedText}*` : '*italic*'; + cursorOffset = selectedText ? 0 : 1; + selectLength = selectedText ? 0 : 6; + break; + case 'strikethrough': + replacement = selectedText ? `~~${selectedText}~~` : '~~strikethrough~~'; + cursorOffset = selectedText ? 0 : 2; + selectLength = selectedText ? 0 : 13; + break; + case 'heading': + replacement = selectedText ? `${prefix}## ${selectedText}` : `${prefix}## Heading`; + cursorOffset = selectedText ? 0 : prefix.length + 3; + selectLength = selectedText ? 0 : 7; + break; + case 'link': + if (selectedText) { + replacement = `[${selectedText}](url)`; + cursorOffset = selectedText.length + 3; + selectLength = 3; + } else { + replacement = '[link text](url)'; + cursorOffset = 1; + selectLength = 9; + } + break; + case 'image': + replacement = selectedText ? `![${selectedText}](image-url)` : '![alt text](image-url)'; + cursorOffset = selectedText ? 0 : 2; + selectLength = selectedText ? 0 : 8; + break; + case 'code': + replacement = selectedText ? `\`${selectedText}\`` : '`code`'; + cursorOffset = selectedText ? 0 : 1; + selectLength = selectedText ? 0 : 4; + break; + case 'codeblock': + replacement = selectedText ? `${prefix}\`\`\`\n${selectedText}\n\`\`\`` : `${prefix}\`\`\`\ncode\n\`\`\``; + cursorOffset = selectedText ? 0 : prefix.length + 4; + selectLength = selectedText ? 0 : 4; + break; + case 'quote': + if (selectedText) { + replacement = prefix + selectedText.split('\n').map(line => `> ${line}`).join('\n'); + } else { + replacement = `${prefix}> quote`; + cursorOffset = prefix.length + 2; + selectLength = 5; + } + break; + case 'bullet': + if (selectedText) { + replacement = prefix + selectedText.split('\n').map(line => `- ${line}`).join('\n'); + } else { + replacement = `${prefix}- item`; + cursorOffset = prefix.length + 2; + selectLength = 4; + } + break; + case 'numbered': + if (selectedText) { + replacement = prefix + selectedText.split('\n').map((line, i) => `${i + 1}. ${line}`).join('\n'); + } else { + replacement = `${prefix}1. item`; + cursorOffset = prefix.length + 3; + selectLength = 4; + } + break; + case 'checkbox': + if (selectedText) { + replacement = prefix + selectedText.split('\n').map(line => `- [ ] ${line}`).join('\n'); + } else { + replacement = `${prefix}- [ ] task`; + cursorOffset = prefix.length + 6; + selectLength = 4; + } + break; + case 'table': + this.insertTable(); + return; + default: + return; + } + + this.noteContent = beforeText + replacement + afterText; + + this.$nextTick(() => { + const newStart = start + cursorOffset; + editor.setSelectionRange(newStart, newStart + selectLength); + editor.focus(); + }); + + this.autoSave(); + }, + // Save current note async saveNote() { if (!this.currentNote) return; diff --git a/frontend/index.html b/frontend/index.html index ddda311..36fc708 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -718,6 +718,29 @@ height: 20px; } + /* Markdown Formatting Toolbar */ + .format-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; + color: var(--text-secondary); + background: transparent; + border: none; + } + .format-btn:hover { + background-color: var(--bg-hover); + color: var(--text-primary); + } + .format-btn:active { + background-color: var(--accent-light); + color: var(--accent-primary); + } + .icon-rail-spacer { flex: 1; } @@ -2208,6 +2231,51 @@ style="min-height: 0;" :style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'" > + +
+ + + +
+ + + +
+ + + +
+ + + + +