added basic toolbar
This commit is contained in:
parent
4499d6ff09
commit
4b65ebc58c
121
frontend/app.js
121
frontend/app.js
|
|
@ -3307,6 +3307,127 @@ function noteApp() {
|
||||||
this.autoSave();
|
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 ? `` : '';
|
||||||
|
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
|
// Save current note
|
||||||
async saveNote() {
|
async saveNote() {
|
||||||
if (!this.currentNote) return;
|
if (!this.currentNote) return;
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,29 @@
|
||||||
height: 20px;
|
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 {
|
.icon-rail-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -2208,6 +2231,51 @@
|
||||||
style="min-height: 0;"
|
style="min-height: 0;"
|
||||||
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
|
: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
|
<div
|
||||||
class="editor-wrapper"
|
class="editor-wrapper"
|
||||||
:class="{'syntax-highlight': syntaxHighlightEnabled}"
|
:class="{'syntax-highlight': syntaxHighlightEnabled}"
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,22 @@
|
||||||
"folder_plural": "Ordner"
|
"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": {
|
"validation": {
|
||||||
"forbidden_chars": "Der Name enthält unzulässige Zeichen: {{chars}}",
|
"forbidden_chars": "Der Name enthält unzulässige Zeichen: {{chars}}",
|
||||||
"reserved_name": "Dieser Name ist vom Betriebssystem reserviert.",
|
"reserved_name": "Dieser Name ist vom Betriebssystem reserviert.",
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,22 @@
|
||||||
"folder_plural": "folders"
|
"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": {
|
"validation": {
|
||||||
"forbidden_chars": "Name contains forbidden characters: {{chars}}",
|
"forbidden_chars": "Name contains forbidden characters: {{chars}}",
|
||||||
"reserved_name": "This name is reserved by the operating system.",
|
"reserved_name": "This name is reserved by the operating system.",
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,22 @@
|
||||||
"folder_plural": "carpetas"
|
"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": {
|
"validation": {
|
||||||
"forbidden_chars": "El nombre contiene caracteres no permitidos: {{chars}}",
|
"forbidden_chars": "El nombre contiene caracteres no permitidos: {{chars}}",
|
||||||
"reserved_name": "Este nombre está reservado por el sistema operativo.",
|
"reserved_name": "Este nombre está reservado por el sistema operativo.",
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,22 @@
|
||||||
"folder_plural": "dossiers"
|
"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": {
|
"validation": {
|
||||||
"forbidden_chars": "Le nom contient des caractères interdits : {{chars}}",
|
"forbidden_chars": "Le nom contient des caractères interdits : {{chars}}",
|
||||||
"reserved_name": "Ce nom est réservé par le système d'exploitation.",
|
"reserved_name": "Ce nom est réservé par le système d'exploitation.",
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,22 @@
|
||||||
"folder_plural": "mape"
|
"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": {
|
"validation": {
|
||||||
"forbidden_chars": "Ime vsebuje prepovedane znake: {{chars}}",
|
"forbidden_chars": "Ime vsebuje prepovedane znake: {{chars}}",
|
||||||
"reserved_name": "To ime je rezervirano s strani operacijskega sistema.",
|
"reserved_name": "To ime je rezervirano s strani operacijskega sistema.",
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,22 @@
|
||||||
"folder_plural": "文件夹"
|
"folder_plural": "文件夹"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"format": {
|
||||||
|
"bold": "粗体",
|
||||||
|
"italic": "斜体",
|
||||||
|
"strikethrough": "删除线",
|
||||||
|
"heading": "标题",
|
||||||
|
"link": "链接",
|
||||||
|
"image": "图片",
|
||||||
|
"code": "行内代码",
|
||||||
|
"codeblock": "代码块",
|
||||||
|
"quote": "引用",
|
||||||
|
"bullet_list": "无序列表",
|
||||||
|
"numbered_list": "有序列表",
|
||||||
|
"checkbox": "复选框",
|
||||||
|
"table": "表格"
|
||||||
|
},
|
||||||
|
|
||||||
"validation": {
|
"validation": {
|
||||||
"forbidden_chars": "名称包含禁止的字符:{{chars}}",
|
"forbidden_chars": "名称包含禁止的字符:{{chars}}",
|
||||||
"reserved_name": "此名称被操作系统保留。",
|
"reserved_name": "此名称被操作系统保留。",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue