feat: full Internal Link Picker with search and type filter
Replace broken ObjectPickerModal and manual modal with proper InternalLinkPicker component: - Search field with debounced SearchNodes API calls - Type tabs: Дело, Заметка, Файл, Секрет (disabled) - Results list showing title + path, keyboard navigation - Inserts [Title](verstak://type/id) at cursor position - No layout breakage — picker is a normal modal via position:fixed - Escape/Cancel close picker cleanly - bind:this on NoteEditorPanel → MarkdownEditor.insertText() Also: - MarkdownEditor: added public insertText() method + bind:this - NoteEditorPanel: added bind:this on MarkdownEditor + public insertText() - Removed manual modal, insertInternalLinkMarkdown(), document.querySelector
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -19,8 +19,8 @@
|
|||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-6mFhgd0M.js"></script>
|
<script type="module" crossorigin src="/assets/main-C5mYAmha.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-bmXj_j_Z.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
+24
-64
@@ -17,6 +17,7 @@
|
|||||||
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
import { canPreviewFile, needsBase64Preview, needsTextPreview } from './lib/fileUtils.js'
|
||||||
import { t } from './lib/i18n'
|
import { t } from './lib/i18n'
|
||||||
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
||||||
|
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
|
||||||
|
|
||||||
// ===== Wails v2 API call helper =====
|
// ===== Wails v2 API call helper =====
|
||||||
function wailsCall(method, ...args) {
|
function wailsCall(method, ...args) {
|
||||||
@@ -77,14 +78,12 @@
|
|||||||
let activeTab = 'overview'
|
let activeTab = 'overview'
|
||||||
let notes = []
|
let notes = []
|
||||||
let noteEditor = null
|
let noteEditor = null
|
||||||
|
let noteEditorPanel = undefined; // bind:this ref for NoteEditorPanel
|
||||||
let noteViewMode = 'edit'
|
let noteViewMode = 'edit'
|
||||||
let showLinkModal = false
|
let showLinkModal = false
|
||||||
let linkModalLabel = ''
|
let linkModalLabel = ''
|
||||||
let linkModalUrl = ''
|
let linkModalUrl = ''
|
||||||
// Simple internal link modal state (replaces broken ObjectPickerModal)
|
let showInternalLinkPicker = false
|
||||||
let showInternalLinkModal = false
|
|
||||||
let internalLinkLabel = ''
|
|
||||||
let internalLinkUrl = 'verstak://secret/sec_example'
|
|
||||||
let showVerstakToast = false
|
let showVerstakToast = false
|
||||||
let verstakToastMessage = ''
|
let verstakToastMessage = ''
|
||||||
// Note rename state
|
// Note rename state
|
||||||
@@ -1355,21 +1354,21 @@
|
|||||||
showLinkModal = false
|
showLinkModal = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Simple internal link modal =====
|
// ===== Internal link picker =====
|
||||||
function openInternalLinkModal() {
|
function openInternalLinkPicker() {
|
||||||
internalLinkLabel = ''
|
showInternalLinkPicker = true;
|
||||||
internalLinkUrl = 'verstak://secret/sec_example'
|
|
||||||
showInternalLinkModal = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitInternalLink() {
|
function handleInternalLinkPickerInsert(e) {
|
||||||
insertInternalLinkMarkdown()
|
const md = e.detail.markdown;
|
||||||
|
if (noteEditorPanel && typeof noteEditorPanel.insertText === 'function') {
|
||||||
|
noteEditorPanel.insertText(md);
|
||||||
|
}
|
||||||
|
showInternalLinkPicker = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelInternalLink() {
|
function handleInternalLinkPickerClose() {
|
||||||
showInternalLinkModal = false
|
showInternalLinkPicker = false;
|
||||||
internalLinkLabel = ''
|
|
||||||
internalLinkUrl = 'verstak://secret/sec_example'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Note rename =====
|
// ===== Note rename =====
|
||||||
@@ -1459,38 +1458,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNoteInsertInternalLink() {
|
function handleNoteInsertInternalLink() {
|
||||||
openInternalLinkModal()
|
openInternalLinkPicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert markdown link at cursor position in the editor textarea.
|
// ===== Verstak link handler =====
|
||||||
function insertInternalLinkMarkdown() {
|
|
||||||
if (!noteEditor) { showInternalLinkModal = false; return }
|
|
||||||
const label = internalLinkLabel.trim() || internalLinkUrl
|
|
||||||
const href = internalLinkUrl.trim() || 'verstak://secret/sec_example'
|
|
||||||
const md = `[${label}](${href})`
|
|
||||||
// Try to insert at cursor via the hidden textarea used by MarkdownEditor.
|
|
||||||
// If textarea not available, append to content.
|
|
||||||
const ta = document.querySelector('.md-textarea')
|
|
||||||
if (ta) {
|
|
||||||
const start = ta.selectionStart
|
|
||||||
const end = ta.selectionEnd
|
|
||||||
const before = noteEditor.content.substring(0, start)
|
|
||||||
const after = noteEditor.content.substring(end)
|
|
||||||
noteEditor.content = before + md + after
|
|
||||||
noteEditor.dirty = true
|
|
||||||
// Restore cursor after inserted text
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
ta.focus()
|
|
||||||
ta.selectionStart = ta.selectionEnd = start + md.length
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
noteEditor.content = noteEditor.content + (noteEditor.content.endsWith('\n') ? '' : '\n') + md + '\n'
|
|
||||||
noteEditor.dirty = true
|
|
||||||
}
|
|
||||||
showInternalLinkModal = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Worklog =====
|
|
||||||
function openWorklogModal(entry = null) {
|
function openWorklogModal(entry = null) {
|
||||||
editingWorklogEntry = entry
|
editingWorklogEntry = entry
|
||||||
acceptingSuggestion = null
|
acceptingSuggestion = null
|
||||||
@@ -2919,6 +2890,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoteEditorPanel
|
<NoteEditorPanel
|
||||||
|
bind:this={noteEditorPanel}
|
||||||
content={noteEditor.content}
|
content={noteEditor.content}
|
||||||
viewMode={noteViewMode}
|
viewMode={noteViewMode}
|
||||||
placeholder={t('note.placeholder')}
|
placeholder={t('note.placeholder')}
|
||||||
@@ -4698,25 +4670,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Simple internal link modal (replaces broken ObjectPickerModal) -->
|
<!-- Internal link picker -->
|
||||||
{#if showInternalLinkModal}
|
{#if showInternalLinkPicker}
|
||||||
<div class="modal-overlay" role="dialog" aria-modal="true" aria-label={t('note.internalLink.title')} on:click|self={cancelInternalLink} on:keydown={(e) => e.key === 'Escape' && cancelInternalLink()}>
|
<InternalLinkPicker
|
||||||
<div class="modal" style="width: 420px;">
|
visible={true}
|
||||||
<h3>{t('note.internalLink.title')}</h3>
|
on:insert={handleInternalLinkPickerInsert}
|
||||||
<div class="form-group">
|
on:close={handleInternalLinkPickerClose}
|
||||||
<label for="il-label">{t('note.internalLink.label')}</label>
|
/>
|
||||||
<input id="il-label" type="text" bind:value={internalLinkLabel} placeholder={t('note.internalLink.labelPlaceholder')} on:keydown={(e) => e.key === 'Enter' && submitInternalLink()} />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="il-url">{t('note.internalLink.url')}</label>
|
|
||||||
<input id="il-url" type="text" bind:value={internalLinkUrl} placeholder={t('note.internalLink.urlPlaceholder')} on:keydown={(e) => e.key === 'Enter' && submitInternalLink()} />
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn" on:click={cancelInternalLink}>{t('common.cancel')}</button>
|
|
||||||
<button class="btn btn-primary" on:click={submitInternalLink}>{t('common.save')}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Note rename modal (for rename from notes list) -->
|
<!-- Note rename modal (for rename from notes list) -->
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let visible = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// Filter types
|
||||||
|
const types = [
|
||||||
|
{ id: 'case', label: 'Дело' },
|
||||||
|
{ id: 'note', label: 'Заметка' },
|
||||||
|
{ id: 'file', label: 'Файл' },
|
||||||
|
{ id: 'secret', label: 'Секрет' },
|
||||||
|
];
|
||||||
|
|
||||||
|
let activeType = 'note';
|
||||||
|
let query = '';
|
||||||
|
let results = [];
|
||||||
|
let selectedIndex = 0;
|
||||||
|
let loading = false;
|
||||||
|
let error = '';
|
||||||
|
let inputRef;
|
||||||
|
|
||||||
|
// Map node types from backend to our filter types
|
||||||
|
function nodeTypeToFilter(type) {
|
||||||
|
// case, project, client, document, recipe, space → case
|
||||||
|
// note → note
|
||||||
|
// file → file
|
||||||
|
// secret → secret
|
||||||
|
if (type === 'note') return 'note';
|
||||||
|
if (type === 'file') return 'file';
|
||||||
|
if (type === 'secret') return 'secret';
|
||||||
|
// Everything else goes to "case"
|
||||||
|
return 'case';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
if (!query.trim() || query.trim().length < 2) {
|
||||||
|
results = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
try {
|
||||||
|
const res = await window['go']['main']['App']['SearchNodes'](query.trim()) || [];
|
||||||
|
// Filter by active type
|
||||||
|
results = res.filter(n => nodeTypeToFilter(n.type) === activeType);
|
||||||
|
selectedIndex = 0;
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
results = [];
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput() {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(search, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e) {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (results[selectedIndex]) {
|
||||||
|
selectResult(results[selectedIndex]);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
dispatch('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectResult(item) {
|
||||||
|
const md = `[${item.title}](verstak://${nodeTypeToFilter(item.type)}/${item.id})`;
|
||||||
|
dispatch('insert', { markdown: md });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTypeChange(typeId) {
|
||||||
|
activeType = typeId;
|
||||||
|
query = '';
|
||||||
|
results = [];
|
||||||
|
error = '';
|
||||||
|
if (inputRef) inputRef.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchTimer;
|
||||||
|
$: if (visible && inputRef) {
|
||||||
|
setTimeout(() => inputRef.focus(), 50);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class="picker-modal-overlay" on:click|self={() => dispatch('close')} on:keydown={(e) => e.key === 'Escape' && dispatch('close')}>
|
||||||
|
<div class="picker-modal" role="dialog" aria-modal="true" aria-label="Выбрать объект">
|
||||||
|
<div class="picker-header">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:this={inputRef}
|
||||||
|
bind:value={query}
|
||||||
|
on:input={handleInput}
|
||||||
|
on:keydown={handleKeydown}
|
||||||
|
placeholder="Поиск..."
|
||||||
|
class="picker-search-input"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="picker-types" role="tablist" aria-label="Тип объекта">
|
||||||
|
{#each types as type}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="picker-type-btn"
|
||||||
|
class:active={activeType === type.id}
|
||||||
|
class:disabled={type.id === 'secret'}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeType === type.id}
|
||||||
|
on:click={() => type.id !== 'secret' && handleTypeChange(type.id)}
|
||||||
|
disabled={type.id === 'secret'}
|
||||||
|
>
|
||||||
|
{type.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if activeType === 'secret'}
|
||||||
|
<div class="picker-secret-placeholder">
|
||||||
|
<p>🔒 Сейф доступов ещё не реализован</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="picker-results" role="listbox">
|
||||||
|
{#if loading}
|
||||||
|
<div class="picker-loading">Загрузка...</div>
|
||||||
|
{:else if error}
|
||||||
|
<div class="picker-error">{error}</div>
|
||||||
|
{:else if query.trim().length >= 2 && results.length === 0}
|
||||||
|
<div class="picker-empty">Ничего не найдено</div>
|
||||||
|
{:else}
|
||||||
|
{#each results as item, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="picker-result"
|
||||||
|
class:selected={i === selectedIndex}
|
||||||
|
role="option"
|
||||||
|
aria-selected={i === selectedIndex}
|
||||||
|
on:click={() => selectResult(item)}
|
||||||
|
>
|
||||||
|
<span class="picker-result-title">{item.title}</span>
|
||||||
|
{#if item.path}
|
||||||
|
<span class="picker-result-path">{item.path}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="picker-footer">
|
||||||
|
<button type="button" class="btn" on:click={() => dispatch('close')}>Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.picker-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-modal {
|
||||||
|
background: #1a1a28;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 480px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 70vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #2a2a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #13131f;
|
||||||
|
color: #e4e4ef;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-search-input:focus {
|
||||||
|
border-color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-types {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid #2a2a3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid #2a2a3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #13131f;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-btn:hover:not(:disabled) {
|
||||||
|
background: #1e1e38;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-btn.active {
|
||||||
|
background: #2a2a4a;
|
||||||
|
color: #e4e4ef;
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-type-btn.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-secret-placeholder {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-results {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-loading,
|
||||||
|
.picker-error,
|
||||||
|
.picker-empty {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-error {
|
||||||
|
color: #ff8888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-result {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #1e1e30;
|
||||||
|
background: transparent;
|
||||||
|
color: #e4e4ef;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-result:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-result:hover,
|
||||||
|
.picker-result.selected {
|
||||||
|
background: #1e1e38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-result-title {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-result-path {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #2a2a3c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,6 +9,23 @@
|
|||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let textareaRef;
|
let textareaRef;
|
||||||
|
|
||||||
|
// ─── Public API: insert text at cursor ──────────────────────────
|
||||||
|
export function insertText(text) {
|
||||||
|
const ta = textareaRef;
|
||||||
|
if (!ta) return;
|
||||||
|
const start = ta.selectionStart;
|
||||||
|
const end = ta.selectionEnd;
|
||||||
|
const before = content.substring(0, start);
|
||||||
|
const after = content.substring(end);
|
||||||
|
const newContent = before + text + after;
|
||||||
|
content = newContent;
|
||||||
|
dispatch('content-change', { content: newContent });
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ta.focus();
|
||||||
|
ta.selectionStart = ta.selectionEnd = start + text.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Core insert helper ──────────────────────────────────────────
|
// ─── Core insert helper ──────────────────────────────────────────
|
||||||
function insertAtCursor(before, after = '', placeholder = '') {
|
function insertAtCursor(before, after = '', placeholder = '') {
|
||||||
const ta = textareaRef;
|
const ta = textareaRef;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
let activeEditor = undefined; // bind:this ref for the visible MarkdownEditor
|
||||||
|
|
||||||
function setMode(mode) {
|
function setMode(mode) {
|
||||||
dispatch('mode-change', { mode });
|
dispatch('mode-change', { mode });
|
||||||
@@ -34,6 +35,13 @@
|
|||||||
function handleVerstakLink(e) {
|
function handleVerstakLink(e) {
|
||||||
dispatch('verstak-link', e.detail);
|
dispatch('verstak-link', e.detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Public API ──────────────────────────────────────────────────
|
||||||
|
export function insertText(text) {
|
||||||
|
if (activeEditor && typeof activeEditor.insertText === 'function') {
|
||||||
|
activeEditor.insertText(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="note-editor-panel" class:mode-edit={viewMode === 'edit'} class:mode-preview={viewMode === 'preview'} class:mode-split={viewMode === 'split'}>
|
<div class="note-editor-panel" class:mode-edit={viewMode === 'edit'} class:mode-preview={viewMode === 'preview'} class:mode-split={viewMode === 'split'}>
|
||||||
@@ -54,6 +62,7 @@
|
|||||||
<div class="panel-content">
|
<div class="panel-content">
|
||||||
{#if viewMode === 'edit'}
|
{#if viewMode === 'edit'}
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
|
bind:this={activeEditor}
|
||||||
{content}
|
{content}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
viewMode="edit"
|
viewMode="edit"
|
||||||
@@ -70,6 +79,7 @@
|
|||||||
<div class="split-pane">
|
<div class="split-pane">
|
||||||
<div class="split-editor">
|
<div class="split-editor">
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
|
bind:this={activeEditor}
|
||||||
{content}
|
{content}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
viewMode="split"
|
viewMode="split"
|
||||||
|
|||||||
Reference in New Issue
Block a user