upd md editor
This commit is contained in:
parent
684519384b
commit
d0cf5c8505
|
|
@ -0,0 +1,132 @@
|
|||
# Feature: Markdown-Editor Verbesserungen
|
||||
|
||||
## Motivation
|
||||
|
||||
Der Wiki-Editor in Ka-Note basiert auf **Tiptap v3** (ProseMirror). Für Nutzer die primär mit Tastatur und Maus arbeiten fehlten:
|
||||
- Ein schneller Weg in den Edit-Modus (ohne Button-Klick)
|
||||
- Kontextuelle Formatierungshilfe beim Schreiben (Slash-Menü)
|
||||
- Schnell-Formatierung bei Textauswahl (Bubble Menu)
|
||||
|
||||
Referenz-UI: UpNote (Slash-Menü, mobiler Toolbar-Bereich).
|
||||
|
||||
---
|
||||
|
||||
## Umgesetzte Features
|
||||
|
||||
### 1. Doppelklick → Edit-Modus
|
||||
|
||||
**Datei:** `ka-note/client/src/routes/wiki/[id]/+page.svelte`
|
||||
|
||||
Im Read-Modus aktiviert ein Doppelklick auf den Inhaltsbereich oder den Seitentitel den Editor.
|
||||
|
||||
- Titel (`<span>`, Zeile ~118): `ondblclick={() => editing = true}`
|
||||
- Content-Container (`<div>`, Zeile ~202): `ondblclick={() => editing = true}` + `cursor-text`
|
||||
- Einfacher Klick auf `[[Wikilinks]]` → Navigation (unverändert)
|
||||
- Doppelklick auf Linktext → öffnet Editor (kein Konflikt, da Browser-Doppelklick = Textauswahl, nicht Click-Event auf Anchor)
|
||||
|
||||
---
|
||||
|
||||
### 2. Slash-Command-Menü (`/`)
|
||||
|
||||
**Datei:** `ka-note/client/src/lib/editor/tiptapSlashCommand.ts`
|
||||
|
||||
Tippt man `/` am Zeilenanfang oder nach einem Leerzeichen, erscheint ein Kontext-Menü mit Block-Befehlen.
|
||||
|
||||
#### Verfügbare Befehle
|
||||
|
||||
| Icon | Label | Tiptap-Command |
|
||||
|------|-------|----------------|
|
||||
| `H1` | Heading 1 | `toggleHeading({ level: 1 })` |
|
||||
| `H2` | Heading 2 | `toggleHeading({ level: 2 })` |
|
||||
| `H3` | Heading 3 | `toggleHeading({ level: 3 })` |
|
||||
| `•–` | Bullet List | `toggleBulletList()` |
|
||||
| `1.` | Ordered List | `toggleOrderedList()` |
|
||||
| `` `x` `` | Code (inline) | `toggleCode()` |
|
||||
| ` ``` ` | Code Block | `toggleCodeBlock()` |
|
||||
| `❝` | Blockquote | `toggleBlockquote()` |
|
||||
| `⊞` | Table (3×3) | `insertTable({ rows: 3, cols: 3, withHeaderRow: true })` |
|
||||
| `—` | Divider | `setHorizontalRule()` |
|
||||
|
||||
#### Verhalten
|
||||
|
||||
- Trigger: `/` nach Whitespace oder am Zeilenanfang
|
||||
- Typing filtert die Liste (Label + Keywords, deutsch + englisch)
|
||||
- Arrow Up/Down → navigieren, Enter → ausführen, Escape → schließen
|
||||
- Mausklick auf Eintrag → ausführen
|
||||
- Vor Ausführung: `/` + getippter Query-Text werden gelöscht (`deleteRange`)
|
||||
- URLs wie `https://example.com` triggern das Menü **nicht** (`:` vor `/` bricht den Scan ab)
|
||||
|
||||
#### Architektur
|
||||
|
||||
Exakt das gleiche ProseMirror Plugin-Pattern wie `tiptapMention.ts` und `tiptapWikiLink.ts`:
|
||||
- `Extension.create()` mit `addProseMirrorPlugins()`
|
||||
- Rückwärts-Scan im `update(view)`-Hook
|
||||
- DOM-Dropdown in `.ka-editor-wrapper` (renutzt `.mention-dropdown`/`.mention-item`/`.mention-item-active` aus `mention.css`)
|
||||
- `pointerdown` + `preventDefault()` für Klick-Handling ohne Fokusverlust
|
||||
|
||||
---
|
||||
|
||||
### 3. Bubble Menu (Textauswahl-Toolbar)
|
||||
|
||||
**Extension:** `@tiptap/extension-bubble-menu` (Tiptap v3, floating-ui-basiert)
|
||||
|
||||
Bei Textauswahl erscheint eine Mini-Toolbar mit:
|
||||
|
||||
| Button | Funktion |
|
||||
|--------|----------|
|
||||
| **B** | Bold toggle |
|
||||
| *I* | Italic toggle |
|
||||
| H2 | Heading 2 toggle |
|
||||
| ↗ | Link setzen/entfernen (`window.prompt`) |
|
||||
|
||||
- Buttons werden aktiv (blau) wenn der Cursor im entsprechenden Mark/Node sitzt
|
||||
- Verschwindet automatisch wenn Auswahl aufgehoben wird
|
||||
- Nicht aktiv bei Bild-Auswahl (`!e.isActive('image')`)
|
||||
- `onpointerdown` + `preventDefault()` verhindert Fokusverlust im Editor
|
||||
|
||||
#### Tiptap v3 BubbleMenu API
|
||||
|
||||
v3 nutzt `@floating-ui/dom` für Positionierung (kein tippy.js mehr).
|
||||
Visibility wird über `element.style.visibility` gesteuert (nicht `display`).
|
||||
Der `element`-Prop erwartet einen DOM-Node — in Svelte via `bind:this={bubbleMenuEl}`.
|
||||
|
||||
---
|
||||
|
||||
## Neue Tiptap-Extensions (Table)
|
||||
|
||||
Für das Table-Slash-Command wurden hinzugefügt:
|
||||
- `@tiptap/extension-table`
|
||||
- `@tiptap/extension-table-row`
|
||||
- `@tiptap/extension-table-cell`
|
||||
- `@tiptap/extension-table-header`
|
||||
|
||||
Konfiguration: `Table.configure({ resizable: false })`
|
||||
|
||||
**Hinweis:** `tiptap-markdown` v0.9 serialisiert Tabellen als GFM (`| col | col |`). Beim ersten produktiven Einsatz verifizieren dass Round-Trip (Edit → Save → reOpen) korrekt funktioniert.
|
||||
|
||||
---
|
||||
|
||||
## Geänderte / neue Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `client/src/routes/wiki/[id]/+page.svelte` | `ondblclick` auf Titel-Span + Content-Container |
|
||||
| `client/src/lib/components/MarkdownEditor.svelte` | 7 neue Imports, `bubbleMenuEl`-Variable, 6 neue Extensions, Bubble-Menu-Template |
|
||||
| `client/src/lib/editor/tiptapSlashCommand.ts` | Neu — Slash-Command Extension |
|
||||
| `client/src/lib/editor/editor.css` | Neu — Slash-Icon + Bubble-Menu-Styles |
|
||||
|
||||
---
|
||||
|
||||
## Nicht umgesetzt (bewusste Entscheidung)
|
||||
|
||||
- **Feste Toolbar**: Passt nicht zum minimalistischen UI-Stil; Bubble Menu ist ausreichend für Desktop
|
||||
- **Mobile Keyboard-Toolbar**: Separates Thema — erfordert `visualViewport`-Handling für iOS; bei Bedarf als eigenes Feature
|
||||
- **Link-inline-Dialog**: `window.prompt` als MVP; langfristig ein kleines Svelte-Popover
|
||||
- **Tabellen-Resize**: `resizable: false` — die Resize-Handles brauchen zusätzliches CSS-Styling
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
- `/code` (inline) — nach dem Löschen des Slash-Texts gibt es keine Auswahl; das Code-Mark wird als "stored mark" für den nächsten Tipp-Vorgang aktiviert (Tiptap-Standardverhalten)
|
||||
- Bubble Menu Link-Button nutzt `window.prompt` — blockiert kurz den Browser
|
||||
|
|
@ -1 +1 @@
|
|||
1.1.77
|
||||
1.1.78
|
||||
|
|
@ -12,9 +12,14 @@
|
|||
"@azure/msal-browser": "^5.2.0",
|
||||
"@ka-note/shared": "*",
|
||||
"@tiptap/core": "^3.20.0",
|
||||
"@tiptap/extension-bubble-menu": "^3.20.0",
|
||||
"@tiptap/extension-image": "^3.20.0",
|
||||
"@tiptap/extension-link": "^3.20.0",
|
||||
"@tiptap/extension-placeholder": "^3.20.0",
|
||||
"@tiptap/extension-table": "^3.20.0",
|
||||
"@tiptap/extension-table-cell": "^3.20.0",
|
||||
"@tiptap/extension-table-header": "^3.20.0",
|
||||
"@tiptap/extension-table-row": "^3.20.0",
|
||||
"@tiptap/pm": "^3.20.0",
|
||||
"@tiptap/starter-kit": "^3.20.0",
|
||||
"dexie": "^4.0.11",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
import { Markdown } from 'tiptap-markdown';
|
||||
import { TiptapMention } from '$lib/editor/tiptapMention';
|
||||
import { createTiptapWikiLink } from '$lib/editor/tiptapWikiLink';
|
||||
import { TiptapSlashCommand } from '$lib/editor/tiptapSlashCommand';
|
||||
import { storeImage, getImageUrl, resolveImageUrls, objectUrlToKaImg } from '$lib/db/imageStore';
|
||||
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
||||
import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table';
|
||||
import '$lib/editor/editor.css';
|
||||
|
||||
interface Props {
|
||||
content?: string;
|
||||
|
|
@ -23,6 +27,7 @@
|
|||
let { content = '', placeholder = '', minHeight = '80px', wikiScope = null, onchange, onsave, onsavearchive }: Props = $props();
|
||||
|
||||
let element: HTMLDivElement;
|
||||
let bubbleMenuEl: HTMLDivElement;
|
||||
let editor: Editor | null = null;
|
||||
let skipNextUpdate = false;
|
||||
let lastExternalContent = $state(content);
|
||||
|
|
@ -85,7 +90,16 @@
|
|||
Placeholder.configure({ placeholder }),
|
||||
Markdown.configure({ html: false, transformCopiedText: true, transformPastedText: true }),
|
||||
TiptapMention,
|
||||
createTiptapWikiLink(wikiScope)
|
||||
createTiptapWikiLink(wikiScope),
|
||||
TiptapSlashCommand,
|
||||
Table.configure({ resizable: false }),
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
BubbleMenu.configure({
|
||||
element: bubbleMenuEl,
|
||||
shouldShow: ({ editor: e, from, to }) => from !== to && !e.isActive('image'),
|
||||
}),
|
||||
],
|
||||
content: initialContent,
|
||||
editorProps: {
|
||||
|
|
@ -142,4 +156,32 @@
|
|||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div class="ka-editor-wrapper" onclick={(e) => e.stopPropagation()}>
|
||||
<div bind:this={element}></div>
|
||||
<div bind:this={bubbleMenuEl} class="ka-bubble-menu">
|
||||
<button class="bubble-btn"
|
||||
class:bubble-btn-active={editor?.isActive('bold')}
|
||||
onpointerdown={(e) => { e.preventDefault(); editor?.chain().focus().toggleBold().run(); }}
|
||||
title="Bold"
|
||||
>B</button>
|
||||
<button class="bubble-btn bubble-btn-italic"
|
||||
class:bubble-btn-active={editor?.isActive('italic')}
|
||||
onpointerdown={(e) => { e.preventDefault(); editor?.chain().focus().toggleItalic().run(); }}
|
||||
title="Italic"
|
||||
>I</button>
|
||||
<button class="bubble-btn"
|
||||
class:bubble-btn-active={editor?.isActive('heading', { level: 2 })}
|
||||
onpointerdown={(e) => { e.preventDefault(); editor?.chain().focus().toggleHeading({ level: 2 }).run(); }}
|
||||
title="Heading 2"
|
||||
>H2</button>
|
||||
<button class="bubble-btn"
|
||||
class:bubble-btn-active={editor?.isActive('link')}
|
||||
onpointerdown={(e) => {
|
||||
e.preventDefault();
|
||||
const href = editor?.getAttributes('link').href ?? '';
|
||||
const url = window.prompt('URL:', href);
|
||||
if (url) editor?.chain().focus().setLink({ href: url }).run();
|
||||
else if (url === '') editor?.chain().focus().unsetLink().run();
|
||||
}}
|
||||
title="Link"
|
||||
>\u2197</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
/* Slash command: Icon-Badge links neben Label */
|
||||
.slash-command-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.slash-command-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 20px;
|
||||
background: #3a3a3a;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: #aaa;
|
||||
flex-shrink: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Bubble Menu Toolbar */
|
||||
.ka-bubble-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
padding: 3px 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 60;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.bubble-btn {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #e0e0e0;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bubble-btn:hover {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
.bubble-btn-active {
|
||||
background: #007acc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bubble-btn-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import { Extension } from '@tiptap/core';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
import type { EditorView } from '@tiptap/pm/view';
|
||||
import '$lib/actions/mention.css';
|
||||
|
||||
const slashPluginKey = new PluginKey('slashCommand');
|
||||
|
||||
interface SlashCommand {
|
||||
label: string;
|
||||
icon: string;
|
||||
keywords: string[];
|
||||
execute: (editor: Editor) => void;
|
||||
}
|
||||
|
||||
const ALL_COMMANDS: SlashCommand[] = [
|
||||
{
|
||||
label: 'Heading 1',
|
||||
icon: 'H1',
|
||||
keywords: ['h1', 'heading'],
|
||||
execute: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
},
|
||||
{
|
||||
label: 'Heading 2',
|
||||
icon: 'H2',
|
||||
keywords: ['h2', 'heading'],
|
||||
execute: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
},
|
||||
{
|
||||
label: 'Heading 3',
|
||||
icon: 'H3',
|
||||
keywords: ['h3', 'heading'],
|
||||
execute: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
},
|
||||
{
|
||||
label: 'Bullet List',
|
||||
icon: '•–',
|
||||
keywords: ['bullet', 'ul', 'list'],
|
||||
execute: (e) => e.chain().focus().toggleBulletList().run(),
|
||||
},
|
||||
{
|
||||
label: 'Ordered List',
|
||||
icon: '1.',
|
||||
keywords: ['ordered', 'ol', 'numbered', 'list'],
|
||||
execute: (e) => e.chain().focus().toggleOrderedList().run(),
|
||||
},
|
||||
{
|
||||
label: 'Code',
|
||||
icon: '`x`',
|
||||
keywords: ['code', 'inline'],
|
||||
execute: (e) => e.chain().focus().toggleCode().run(),
|
||||
},
|
||||
{
|
||||
label: 'Code Block',
|
||||
icon: '```',
|
||||
keywords: ['code', 'block', 'fenced', 'codeblock'],
|
||||
execute: (e) => e.chain().focus().toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
label: 'Blockquote',
|
||||
icon: '❝',
|
||||
keywords: ['quote', 'blockquote', 'zitat'],
|
||||
execute: (e) => e.chain().focus().toggleBlockquote().run(),
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
icon: '⊞',
|
||||
keywords: ['table', 'grid', 'tabelle'],
|
||||
execute: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||
},
|
||||
{
|
||||
label: 'Divider',
|
||||
icon: '—',
|
||||
keywords: ['hr', 'rule', 'divider', 'horizontal', 'trennlinie'],
|
||||
execute: (e) => e.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
];
|
||||
|
||||
export const TiptapSlashCommand = Extension.create({
|
||||
name: 'slashCommand',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
let dropdown: HTMLDivElement | null = null;
|
||||
let filtered: SlashCommand[] = [];
|
||||
let selectedIndex = 0;
|
||||
let triggerFrom = -1;
|
||||
let active = false;
|
||||
|
||||
function hide() {
|
||||
dropdown?.remove();
|
||||
dropdown = null;
|
||||
active = false;
|
||||
filtered = [];
|
||||
triggerFrom = -1;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!dropdown) return;
|
||||
if (filtered.length === 0) { hide(); return; }
|
||||
dropdown.innerHTML = '';
|
||||
filtered.forEach((cmd, i) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'mention-item slash-command-item'
|
||||
+ (i === selectedIndex ? ' mention-item-active' : '');
|
||||
|
||||
const iconSpan = document.createElement('span');
|
||||
iconSpan.className = 'slash-command-icon';
|
||||
iconSpan.textContent = cmd.icon;
|
||||
|
||||
const labelSpan = document.createElement('span');
|
||||
labelSpan.textContent = cmd.label;
|
||||
|
||||
row.appendChild(iconSpan);
|
||||
row.appendChild(labelSpan);
|
||||
row.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectItem(i);
|
||||
});
|
||||
row.addEventListener('mouseenter', () => { selectedIndex = i; render(); });
|
||||
dropdown!.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function show(view: EditorView) {
|
||||
if (!dropdown) {
|
||||
dropdown = document.createElement('div');
|
||||
dropdown.className = 'mention-dropdown';
|
||||
|
||||
const coords = view.coordsAtPos(view.state.selection.from);
|
||||
const editorRect = view.dom.closest('.ka-editor-wrapper')?.getBoundingClientRect()
|
||||
?? view.dom.getBoundingClientRect();
|
||||
|
||||
dropdown.style.position = 'absolute';
|
||||
dropdown.style.left = `${coords.left - editorRect.left}px`;
|
||||
dropdown.style.top = `${coords.bottom - editorRect.top + 4}px`;
|
||||
|
||||
const wrapper = view.dom.closest('.ka-editor-wrapper') as HTMLElement;
|
||||
if (wrapper) {
|
||||
wrapper.style.position = 'relative';
|
||||
wrapper.appendChild(dropdown);
|
||||
}
|
||||
}
|
||||
active = true;
|
||||
render();
|
||||
}
|
||||
|
||||
function selectItem(index: number) {
|
||||
const cmd = filtered[index];
|
||||
if (!cmd) return;
|
||||
const savedFrom = triggerFrom;
|
||||
const savedTo = editor.state.selection.from;
|
||||
editor.chain().focus().deleteRange({ from: savedFrom, to: savedTo }).run();
|
||||
cmd.execute(editor);
|
||||
hide();
|
||||
}
|
||||
|
||||
function getSlashState(view: EditorView): { query: string; from: number } | null {
|
||||
const { state } = view;
|
||||
const { from } = state.selection;
|
||||
if (!state.selection.empty) return null;
|
||||
|
||||
// Resolve current block start to limit scan to current paragraph
|
||||
const resolved = state.doc.resolve(from);
|
||||
const blockStart = resolved.start(); // start of current text block (after the node opening token)
|
||||
const scanFrom = Math.max(blockStart, from - 50);
|
||||
const textBefore = state.doc.textBetween(scanFrom, from, undefined, '\ufffc');
|
||||
|
||||
// Scan backwards for '/'
|
||||
for (let i = textBefore.length - 1; i >= 0; i--) {
|
||||
const ch = textBefore[i];
|
||||
if (ch === '/') {
|
||||
// Must be at block start or after whitespace
|
||||
if (i === 0 || /[\s\ufffc]/.test(textBefore[i - 1])) {
|
||||
const query = textBefore.slice(i + 1);
|
||||
// If query contains whitespace, no longer active
|
||||
if (/[\s\ufffc]/.test(query)) return null;
|
||||
const docFrom = scanFrom + i;
|
||||
return { query, from: docFrom };
|
||||
}
|
||||
// '/' after non-whitespace (e.g. URL) — don't trigger
|
||||
return null;
|
||||
}
|
||||
// Stop scanning on whitespace or block boundary
|
||||
if (/[\s\ufffc]/.test(ch)) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function update(view: EditorView) {
|
||||
const slashState = getSlashState(view);
|
||||
if (!slashState) {
|
||||
if (active) hide();
|
||||
return;
|
||||
}
|
||||
|
||||
triggerFrom = slashState.from;
|
||||
const q = slashState.query.toLowerCase();
|
||||
|
||||
filtered = q === ''
|
||||
? ALL_COMMANDS
|
||||
: ALL_COMMANDS.filter(cmd =>
|
||||
cmd.label.toLowerCase().includes(q) ||
|
||||
cmd.keywords.some(k => k.includes(q))
|
||||
);
|
||||
|
||||
selectedIndex = 0;
|
||||
if (filtered.length > 0) {
|
||||
show(view);
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: slashPluginKey,
|
||||
props: {
|
||||
handleKeyDown(view, event) {
|
||||
if (!active) return false;
|
||||
const count = filtered.length;
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
selectedIndex = (selectedIndex + 1) % count;
|
||||
render();
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
selectedIndex = (selectedIndex - 1 + count) % count;
|
||||
render();
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
selectItem(selectedIndex);
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
hide();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
view() {
|
||||
return {
|
||||
update(view) { update(view); },
|
||||
destroy() { hide(); }
|
||||
};
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
|
@ -114,7 +114,8 @@
|
|||
placeholder="Seitentitel..."
|
||||
/>
|
||||
{:else}
|
||||
<span class="flex-1 text-2xl font-bold">{currentPage.title}</span>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span class="flex-1 text-2xl font-bold cursor-text" ondblclick={() => editing = true}>{currentPage.title}</span>
|
||||
{/if}
|
||||
|
||||
<!-- Privat/Firma toggle -->
|
||||
|
|
@ -198,7 +199,8 @@
|
|||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded border border-border p-4 min-h-[100px]">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="rounded border border-border p-4 min-h-[100px] cursor-text" ondblclick={() => editing = true}>
|
||||
{#if currentPage.body}
|
||||
<RenderedMarkdown text={currentPage.body} class="prose-wiki" />
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,14 @@
|
|||
"@azure/msal-browser": "^5.2.0",
|
||||
"@ka-note/shared": "*",
|
||||
"@tiptap/core": "^3.20.0",
|
||||
"@tiptap/extension-bubble-menu": "^3.20.0",
|
||||
"@tiptap/extension-image": "^3.20.0",
|
||||
"@tiptap/extension-link": "^3.20.0",
|
||||
"@tiptap/extension-placeholder": "^3.20.0",
|
||||
"@tiptap/extension-table": "^3.20.0",
|
||||
"@tiptap/extension-table-cell": "^3.20.0",
|
||||
"@tiptap/extension-table-header": "^3.20.0",
|
||||
"@tiptap/extension-table-row": "^3.20.0",
|
||||
"@tiptap/pm": "^3.20.0",
|
||||
"@tiptap/starter-kit": "^3.20.0",
|
||||
"dexie": "^4.0.11",
|
||||
|
|
@ -2579,6 +2584,31 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
|
||||
"integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
|
||||
"integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.4",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
|
|
@ -3476,6 +3506,23 @@
|
|||
"@tiptap/core": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.0.tgz",
|
||||
"integrity": "sha512-MDosUfs8Tj+nwg8RC+wTMWGkLJORXmbR6YZgbiX4hrc7G90Gopdd6kj6ht5/T8t7dLLaX7N0+DEHdUEPGED7dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.20.0",
|
||||
"@tiptap/pm": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz",
|
||||
|
|
@ -3730,6 +3777,59 @@
|
|||
"@tiptap/core": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.20.0.tgz",
|
||||
"integrity": "sha512-vaaMtQ2KnSSr8WVwgWf7NYNzPwrHx/6T0ekA5CxV8qNUEpXIaLXa5+tE7tJHWEdNR2KY3gUJ46D3lfOkxyFrBQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.20.0",
|
||||
"@tiptap/pm": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-cell": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.20.0.tgz",
|
||||
"integrity": "sha512-9Dg4zda3UWwtpBwSG7b9BeQy5oT27a/yEIBeARuxe19bloMLZgqpPRtnSrOK0OAITtVnjA+NZdKPcVLRMS2E8A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-header": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.20.0.tgz",
|
||||
"integrity": "sha512-2tVHHlihpeHO/gh2uU46gAX3NTGdKR+yDmfLlO2l0QAvx2TXNfNzX2pOM4MmyostW5Ko9TCWV4x0D9h3IQDhPw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-row": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.20.0.tgz",
|
||||
"integrity": "sha512-clkfQahkYW/U48QBh1rPZv3AWWSC9AqGKp2DLTH/SGIorM/NwI0jpPtBETMlvowyQu0ivlH9B896smEph+Do2A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.0.tgz",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue