Refactor Journals UI and enhance CodeMirror editor
- Added formatting toolbar to journal entry editor with support for bold, italic, headers, and lists. - Standardized iconography to Lucide across Journals module, removing legacy FontAwesome. - Improved responsiveness and dark mode compatibility in layout and list views. - Refactored CodeMirror component to support external control via editorView binding. - Hardened security by removing unnecessary @html tags in journal names.
This commit is contained in:
60
src/lib/ae_journals/ae_journals_editor_helpers.ts
Normal file
60
src/lib/ae_journals/ae_journals_editor_helpers.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the current selection in CodeMirror with the given prefix and suffix.
|
||||||
|
*/
|
||||||
|
export function wrapSelection(view: EditorView, prefix: string, suffix: string = prefix) {
|
||||||
|
if (!view) return;
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const changes = state.changeByRange((range) => {
|
||||||
|
const selectedText = state.doc.sliceString(range.from, range.to);
|
||||||
|
return {
|
||||||
|
changes: [
|
||||||
|
{ from: range.from, insert: prefix },
|
||||||
|
{ from: range.to, insert: suffix }
|
||||||
|
],
|
||||||
|
range: {
|
||||||
|
from: range.from + prefix.length,
|
||||||
|
to: range.to + prefix.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: 'input' }));
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a prefix at the start of each line in the selection (e.g., for lists or blockquotes).
|
||||||
|
*/
|
||||||
|
export function toggleLinePrefix(view: EditorView, prefix: string) {
|
||||||
|
if (!view) return;
|
||||||
|
const { state, dispatch } = view;
|
||||||
|
const changes = state.changeByRange((range) => {
|
||||||
|
const lines = [];
|
||||||
|
for (let pos = range.from; pos <= range.to; ) {
|
||||||
|
const line = state.doc.lineAt(pos);
|
||||||
|
lines.push(line);
|
||||||
|
pos = line.to + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlreadyPrefixed = lines.every(l => l.text.startsWith(prefix));
|
||||||
|
|
||||||
|
const lineChanges = lines.map(l => {
|
||||||
|
if (isAlreadyPrefixed) {
|
||||||
|
return { from: l.from, to: l.from + prefix.length, insert: '' };
|
||||||
|
} else {
|
||||||
|
return { from: l.from, insert: prefix };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
changes: lineChanges,
|
||||||
|
range: {
|
||||||
|
from: range.from + (isAlreadyPrefixed ? -prefix.length : prefix.length),
|
||||||
|
to: range.to + (isAlreadyPrefixed ? -prefix.length * lines.length : prefix.length * lines.length)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: 'input' }));
|
||||||
|
view.focus();
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
let {
|
let {
|
||||||
content = 'test test test test',
|
content = 'test test test test',
|
||||||
new_content = $bindable(''),
|
new_content = $bindable(''),
|
||||||
|
editorView = $bindable(), // Exposed for external control
|
||||||
theme_mode = 'light',
|
theme_mode = 'light',
|
||||||
extensions = [],
|
extensions = [],
|
||||||
editable = true,
|
editable = true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
// *** Import other supporting libraries
|
// *** Import other supporting libraries
|
||||||
import { House, RefreshCw, Satellite } from '@lucide/svelte';
|
import { ArrowDownUp, House, RefreshCw, Satellite } from '@lucide/svelte';
|
||||||
|
|
||||||
// *** Import Aether specific variables and functions
|
// *** Import Aether specific variables and functions
|
||||||
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
|
import { ae_loc, ae_sess, ae_api, slct } from '$lib/stores/ae_stores';
|
||||||
@@ -271,10 +271,6 @@
|
|||||||
`Scroll to top button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`,
|
`Scroll to top button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`,
|
||||||
scroll_container()
|
scroll_container()
|
||||||
);
|
);
|
||||||
// document.getElementById('ae_idaa')?.scrollTo(0, 0);
|
|
||||||
// document.documentElement?.scrollTo(0, 0);
|
|
||||||
// document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
|
||||||
// document.body.scrollTop = 0; // For Safari
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -284,11 +280,11 @@
|
|||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
|
|
||||||
window.parent.postMessage({ scroll_to: 0 }, '*'); // This should be
|
window.parent.postMessage({ scroll_to: 0 }, '*');
|
||||||
}}
|
}}
|
||||||
title="Scroll to top"
|
title="Scroll to top"
|
||||||
>
|
>
|
||||||
<span class="fas fa-arrow-up"></span>
|
<ArrowDownUp class="rotate-180" />
|
||||||
Scroll to Top
|
Scroll to Top
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -345,10 +341,6 @@
|
|||||||
`Scroll to bottom button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`,
|
`Scroll to bottom button clicked. yScroll: ${yScroll} scrollTop: ${scroll_container().scrollTop}`,
|
||||||
scroll_container()
|
scroll_container()
|
||||||
);
|
);
|
||||||
// document.getElementById('ae_idaa')?.scrollTo(0, 0);
|
|
||||||
// document.documentElement?.scrollTo(0, 0);
|
|
||||||
// document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
|
||||||
// document.body.scrollTop = 0; // For Safari
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,13 +350,12 @@
|
|||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
|
|
||||||
window.parent.postMessage({ scroll_to: yScroll }, '*'); // This should be
|
window.parent.postMessage({ scroll_to: yScroll }, '*');
|
||||||
}}
|
}}
|
||||||
title="Scroll to bottom"
|
title="Scroll to bottom"
|
||||||
>
|
>
|
||||||
<span class="fas fa-arrow-down"></span>
|
<ArrowDownUp />
|
||||||
Scroll to Bottom
|
Scroll to Bottom
|
||||||
<!-- yTop={yTop} yScroll={yScroll} yHeight={yHeight} yScroll={yScroll} scrollTop={scroll_container().scrollTop} total={scroll_container().scrollTop + yHeight} -->
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
// *** Import other supporting libraries
|
// *** Import other supporting libraries
|
||||||
import { BookPlus, FolderPlus, Library, SquareLibrary, Wrench } from '@lucide/svelte';
|
import { BookPlus, FolderPlus, Library, Loader, SquareLibrary, Wrench } from '@lucide/svelte';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import { Modal } from 'flowbite-svelte';
|
import { Modal } from 'flowbite-svelte';
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
|
|
||||||
{#await ae_acct.slct.journal_obj_li}
|
{#await ae_acct.slct.journal_obj_li}
|
||||||
<div class="flex flex-col items-center justify-center p-8">
|
<div class="flex flex-col items-center justify-center p-8">
|
||||||
<span class="fas fa-spinner fa-spin text-4xl text-primary-500 mb-4"></span>
|
<Loader size="2em" class="animate-spin text-primary-500 mb-4" />
|
||||||
<span class="text-lg text-gray-600 dark:text-gray-400">Loading journals...</span>
|
<span class="text-lg text-gray-600 dark:text-gray-400">Loading journals...</span>
|
||||||
</div>
|
</div>
|
||||||
{:then}
|
{:then}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
ArrowDown01,
|
ArrowDown01,
|
||||||
ArrowDown10,
|
ArrowDown10,
|
||||||
ArrowDownUp,
|
ArrowDownUp,
|
||||||
|
Bold,
|
||||||
BookHeart,
|
BookHeart,
|
||||||
Bot,
|
Bot,
|
||||||
BotMessageSquare,
|
BotMessageSquare,
|
||||||
@@ -31,6 +32,10 @@
|
|||||||
Group,
|
Group,
|
||||||
Hash,
|
Hash,
|
||||||
History,
|
History,
|
||||||
|
Italic,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
Loader,
|
Loader,
|
||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
LockKeyholeOpen,
|
LockKeyholeOpen,
|
||||||
@@ -43,6 +48,7 @@
|
|||||||
Pencil,
|
Pencil,
|
||||||
PenLine,
|
PenLine,
|
||||||
Plus,
|
Plus,
|
||||||
|
Quote,
|
||||||
RemoveFormatting,
|
RemoveFormatting,
|
||||||
Save,
|
Save,
|
||||||
Search,
|
Search,
|
||||||
@@ -54,6 +60,7 @@
|
|||||||
Siren,
|
Siren,
|
||||||
Skull,
|
Skull,
|
||||||
SquareLibrary,
|
SquareLibrary,
|
||||||
|
Strikethrough,
|
||||||
Tags,
|
Tags,
|
||||||
Trash2,
|
Trash2,
|
||||||
TypeOutline,
|
TypeOutline,
|
||||||
@@ -64,6 +71,8 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
// import { Configuration, OpenAIApi } from 'openai';
|
// import { Configuration, OpenAIApi } from 'openai';
|
||||||
|
|
||||||
|
import { wrapSelection, toggleLinePrefix } from '$lib/ae_journals/ae_journals_editor_helpers';
|
||||||
|
|
||||||
let llm_api_token =
|
let llm_api_token =
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVhYjI2MzdlLThiMjktNGM2Zi05MzVhLWFkYjU1MDkwMGU5MCJ9.4y5AStXZJAVnWRlgG3lVV0-xKIfMzqdNRuInGwT0ThQ';
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVhYjI2MzdlLThiMjktNGM2Zi05MzVhLWFkYjU1MDkwMGU5MCJ9.4y5AStXZJAVnWRlgG3lVV0-xKIfMzqdNRuInGwT0ThQ';
|
||||||
|
|
||||||
@@ -128,6 +137,8 @@
|
|||||||
lq__journal_entry_obj
|
lq__journal_entry_obj
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let editorView: any = $state();
|
||||||
|
|
||||||
if (log_lvl) {
|
if (log_lvl) {
|
||||||
console.log(
|
console.log(
|
||||||
`ae_comp__journal_entry_obj_id_view.svelte`,
|
`ae_comp__journal_entry_obj_id_view.svelte`,
|
||||||
@@ -2651,19 +2662,56 @@ tabindex={$ae_loc.edit_mode ? 0 : -1} -->
|
|||||||
<!-- disabled={tmp_entry_obj?.private && !$journals_loc?.entry?.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id]} -->
|
<!-- disabled={tmp_entry_obj?.private && !$journals_loc?.entry?.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id]} -->
|
||||||
|
|
||||||
{#if $lq__journal_obj?.cfg_json?.pref_editor == 'codemirror'}
|
{#if $lq__journal_obj?.cfg_json?.pref_editor == 'codemirror'}
|
||||||
<!-- bg-slate-100 text-gray-900
|
<!-- Toolbar for CodeMirror -->
|
||||||
dark:bg-slate-900 dark:text-gray-100
|
<div class="flex flex-row flex-wrap gap-1 p-1 bg-surface-100-900 border-x border-t border-orange-300 dark:border-orange-700 rounded-t-lg w-full max-w-6xl">
|
||||||
border border-orange-200 dark:border-orange-700
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Bold" onclick={() => wrapSelection(editorView, '**')}>
|
||||||
hover:border-orange-500 dark:hover:border-orange-500 -->
|
<Bold size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Italic" onclick={() => wrapSelection(editorView, '*')}>
|
||||||
|
<Italic size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Strikethrough" onclick={() => wrapSelection(editorView, '~~')}>
|
||||||
|
<Strikethrough size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<span class="border-r border-surface-500 mx-1"></span>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Header 1" onclick={() => toggleLinePrefix(editorView, '# ')}>
|
||||||
|
<span class="font-bold">H1</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Header 2" onclick={() => toggleLinePrefix(editorView, '## ')}>
|
||||||
|
<span class="font-bold">H2</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Header 3" onclick={() => toggleLinePrefix(editorView, '### ')}>
|
||||||
|
<span class="font-bold">H3</span>
|
||||||
|
</button>
|
||||||
|
<span class="border-r border-surface-500 mx-1"></span>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Bullet List" onclick={() => toggleLinePrefix(editorView, '- ')}>
|
||||||
|
<List size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Ordered List" onclick={() => toggleLinePrefix(editorView, '1. ')}>
|
||||||
|
<ListOrdered size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Blockquote" onclick={() => toggleLinePrefix(editorView, '> ')}>
|
||||||
|
<Quote size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<span class="border-r border-surface-500 mx-1"></span>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Link" onclick={() => wrapSelection(editorView, '[', '](https://)')}>
|
||||||
|
<Link size="1.25em" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Code" onclick={() => wrapSelection(editorView, '`')}>
|
||||||
|
<CodeXml size="1.25em" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<E_app_codemirror_v5
|
<E_app_codemirror_v5
|
||||||
content={tmp_entry_obj?.content ?? ''}
|
content={tmp_entry_obj?.content ?? ''}
|
||||||
bind:new_content={tmp_entry_obj.content}
|
bind:new_content={tmp_entry_obj.content}
|
||||||
|
bind:editorView={editorView}
|
||||||
bind:theme_mode={$ae_loc.theme_mode}
|
bind:theme_mode={$ae_loc.theme_mode}
|
||||||
placeholder="Write using Markdown here..."
|
placeholder="Write using Markdown here..."
|
||||||
class="
|
class="
|
||||||
p-2
|
p-2
|
||||||
preset-outlined-warning-300-700
|
preset-outlined-warning-300-700
|
||||||
shadow-lg rounded-lg
|
shadow-lg rounded-b-lg rounded-t-none
|
||||||
bg-gray-100 text-gray-950
|
bg-gray-100 text-gray-950
|
||||||
dark:bg-gray-800 dark:text-gray-100
|
dark:bg-gray-800 dark:text-gray-100
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
>
|
>
|
||||||
<h3 class="journal__name h3">
|
<h3 class="journal__name h3">
|
||||||
<BookType class="m-1 inline-block" />
|
<BookType class="m-1 inline-block" />
|
||||||
<span class="journal__name">{@html journals_journal_obj.name}</span>
|
<span class="journal__name">{journals_journal_obj.name}</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Show a label if the type code is set -->
|
<!-- Show a label if the type code is set -->
|
||||||
|
|||||||
Reference in New Issue
Block a user