added basic toolbar

This commit is contained in:
Gamosoft 2025-12-18 14:16:04 +01:00
parent 4499d6ff09
commit 4b65ebc58c
8 changed files with 285 additions and 0 deletions

View File

@ -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;

View File

@ -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%;'"
>
<!-- Markdown Formatting Toolbar -->
<div class="flex items-center gap-1 px-2 py-1.5 border-b flex-shrink-0" style="background-color: var(--bg-secondary); border-color: var(--border-primary);">
<button @click="formatText('bold')" class="format-btn" :title="t('format.bold') + ' (Ctrl+B)'">
<span class="font-bold">B</span>
</button>
<button @click="formatText('italic')" class="format-btn" :title="t('format.italic') + ' (Ctrl+I)'">
<span class="italic">I</span>
</button>
<button @click="formatText('strikethrough')" class="format-btn" :title="t('format.strikethrough')">
<span class="line-through">S</span>
</button>
<div class="w-px h-4 mx-1" style="background-color: var(--border-primary);"></div>
<button @click="formatText('heading')" class="format-btn" :title="t('format.heading')">
<span class="font-bold text-xs">H</span>
</button>
<button @click="formatText('link')" class="format-btn" :title="t('format.link') + ' (Ctrl+K)'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
</button>
<button @click="formatText('image')" class="format-btn" :title="t('format.image')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
</button>
<div class="w-px h-4 mx-1" style="background-color: var(--border-primary);"></div>
<button @click="formatText('code')" class="format-btn" :title="t('format.code')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
</button>
<button @click="formatText('codeblock')" class="format-btn" :title="t('format.codeblock')">
<span class="text-xs font-mono">{}</span>
</button>
<button @click="formatText('quote')" class="format-btn" :title="t('format.quote')">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/></svg>
</button>
<div class="w-px h-4 mx-1" style="background-color: var(--border-primary);"></div>
<button @click="formatText('bullet')" class="format-btn" :title="t('format.bullet_list')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
<button @click="formatText('numbered')" class="format-btn" :title="t('format.numbered_list')">
<span class="text-xs font-mono">1.</span>
</button>
<button @click="formatText('checkbox')" class="format-btn" :title="t('format.checkbox')">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</button>
<button @click="formatText('table')" class="format-btn" :title="t('format.table') + ' (Ctrl+Alt+T)'">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
</button>
</div>
<div
class="editor-wrapper"
:class="{'syntax-highlight': syntaxHighlightEnabled}"

View File

@ -224,6 +224,22 @@
"folder_plural": "Ordner"
},
"format": {
"bold": "Fett",
"italic": "Kursiv",
"strikethrough": "Durchgestrichen",
"heading": "Überschrift",
"link": "Link",
"image": "Bild",
"code": "Inline-Code",
"codeblock": "Codeblock",
"quote": "Zitat",
"bullet_list": "Aufzählungsliste",
"numbered_list": "Nummerierte Liste",
"checkbox": "Kontrollkästchen",
"table": "Tabelle"
},
"validation": {
"forbidden_chars": "Der Name enthält unzulässige Zeichen: {{chars}}",
"reserved_name": "Dieser Name ist vom Betriebssystem reserviert.",

View File

@ -224,6 +224,22 @@
"folder_plural": "folders"
},
"format": {
"bold": "Bold",
"italic": "Italic",
"strikethrough": "Strikethrough",
"heading": "Heading",
"link": "Link",
"image": "Image",
"code": "Inline code",
"codeblock": "Code block",
"quote": "Quote",
"bullet_list": "Bullet list",
"numbered_list": "Numbered list",
"checkbox": "Checkbox",
"table": "Table"
},
"validation": {
"forbidden_chars": "Name contains forbidden characters: {{chars}}",
"reserved_name": "This name is reserved by the operating system.",

View File

@ -224,6 +224,22 @@
"folder_plural": "carpetas"
},
"format": {
"bold": "Negrita",
"italic": "Cursiva",
"strikethrough": "Tachado",
"heading": "Encabezado",
"link": "Enlace",
"image": "Imagen",
"code": "Código en línea",
"codeblock": "Bloque de código",
"quote": "Cita",
"bullet_list": "Lista con viñetas",
"numbered_list": "Lista numerada",
"checkbox": "Casilla de verificación",
"table": "Tabla"
},
"validation": {
"forbidden_chars": "El nombre contiene caracteres no permitidos: {{chars}}",
"reserved_name": "Este nombre está reservado por el sistema operativo.",

View File

@ -224,6 +224,22 @@
"folder_plural": "dossiers"
},
"format": {
"bold": "Gras",
"italic": "Italique",
"strikethrough": "Barré",
"heading": "Titre",
"link": "Lien",
"image": "Image",
"code": "Code en ligne",
"codeblock": "Bloc de code",
"quote": "Citation",
"bullet_list": "Liste à puces",
"numbered_list": "Liste numérotée",
"checkbox": "Case à cocher",
"table": "Tableau"
},
"validation": {
"forbidden_chars": "Le nom contient des caractères interdits : {{chars}}",
"reserved_name": "Ce nom est réservé par le système d'exploitation.",

View File

@ -224,6 +224,22 @@
"folder_plural": "mape"
},
"format": {
"bold": "Krepko",
"italic": "Ležeče",
"strikethrough": "Prečrtano",
"heading": "Naslov",
"link": "Povezava",
"image": "Slika",
"code": "Vrstična koda",
"codeblock": "Blok kode",
"quote": "Citat",
"bullet_list": "Seznam z oznakami",
"numbered_list": "Oštevilčen seznam",
"checkbox": "Potrditveno polje",
"table": "Tabela"
},
"validation": {
"forbidden_chars": "Ime vsebuje prepovedane znake: {{chars}}",
"reserved_name": "To ime je rezervirano s strani operacijskega sistema.",

View File

@ -224,6 +224,22 @@
"folder_plural": "文件夹"
},
"format": {
"bold": "粗体",
"italic": "斜体",
"strikethrough": "删除线",
"heading": "标题",
"link": "链接",
"image": "图片",
"code": "行内代码",
"codeblock": "代码块",
"quote": "引用",
"bullet_list": "无序列表",
"numbered_list": "有序列表",
"checkbox": "复选框",
"table": "表格"
},
"validation": {
"forbidden_chars": "名称包含禁止的字符:{{chars}}",
"reserved_name": "此名称被操作系统保留。",