Refine Journals UI and harden entry toggle logic

- Implemented 3-state (View/Eye/Save) toggle in journal entry header with Lucide icons.
- Hardened change detection logic using Svelte 5 .by for reliable UI updates.
- Improved header responsiveness and scaling across device sizes.
- Commented out experimental CodeMirror toolbar to maintain stability while keeping the helper code for reference.
This commit is contained in:
Scott Idem
2026-01-08 15:38:28 -05:00
parent 4c8f09e588
commit dd0390a5dd
2 changed files with 66 additions and 144 deletions

View File

@@ -1,35 +1,30 @@
import type { EditorView } from '@codemirror/view';
/** /**
* Wraps the current selection in CodeMirror with the given prefix and suffix. * Wraps the current selection in CodeMirror with the given prefix and suffix.
*/ */
export function wrapSelection(view: EditorView, prefix: string, suffix: string = prefix) { export function wrapSelection(view: any, prefix: string, suffix: string = prefix) {
if (!view) return; if (!view || view.updateInProgress) return;
const { state, dispatch } = view;
const changes = state.changeByRange((range) => { const { state } = view;
const selectedText = state.doc.sliceString(range.from, range.to); view.dispatch(state.changeByRange((range: any) => {
return { return {
changes: [ changes: [
{ from: range.from, insert: prefix }, {from: range.from, insert: prefix},
{ from: range.to, insert: suffix } {from: range.to, insert: suffix}
], ],
range: { range: range.constructor.range(range.from + prefix.length, range.to + prefix.length)
from: range.from + prefix.length,
to: range.to + prefix.length
}
}; };
}); }));
dispatch(state.update(changes, { scrollIntoView: true, userEvent: 'input' }));
view.focus(); view.focus();
} }
/** /**
* Inserts a prefix at the start of each line in the selection (e.g., for lists or blockquotes). * 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) { export function toggleLinePrefix(view: any, prefix: string) {
if (!view) return; if (!view || view.updateInProgress) return;
const { state, dispatch } = view;
const changes = state.changeByRange((range) => { const { state } = view;
view.dispatch(state.changeByRange((range: any) => {
const lines = []; const lines = [];
for (let pos = range.from; pos <= range.to; ) { for (let pos = range.from; pos <= range.to; ) {
const line = state.doc.lineAt(pos); const line = state.doc.lineAt(pos);
@@ -38,7 +33,6 @@ export function toggleLinePrefix(view: EditorView, prefix: string) {
} }
const isAlreadyPrefixed = lines.every(l => l.text.startsWith(prefix)); const isAlreadyPrefixed = lines.every(l => l.text.startsWith(prefix));
const lineChanges = lines.map(l => { const lineChanges = lines.map(l => {
if (isAlreadyPrefixed) { if (isAlreadyPrefixed) {
return { from: l.from, to: l.from + prefix.length, insert: '' }; return { from: l.from, to: l.from + prefix.length, insert: '' };
@@ -47,14 +41,13 @@ export function toggleLinePrefix(view: EditorView, prefix: string) {
} }
}); });
const newFrom = range.from + (isAlreadyPrefixed ? -prefix.length : prefix.length);
const newTo = range.to + (isAlreadyPrefixed ? (-prefix.length * lines.length) : (prefix.length * lines.length));
return { return {
changes: lineChanges, changes: lineChanges,
range: { range: range.constructor.range(newFrom, Math.max(newFrom, newTo))
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(); view.focus();
} }

View File

@@ -9,7 +9,6 @@
ArrowDown01, ArrowDown01,
ArrowDown10, ArrowDown10,
ArrowDownUp, ArrowDownUp,
Bold,
BookHeart, BookHeart,
Bot, Bot,
BotMessageSquare, BotMessageSquare,
@@ -32,10 +31,6 @@
Group, Group,
Hash, Hash,
History, History,
Italic,
Link,
List,
ListOrdered,
Loader, Loader,
LockKeyhole, LockKeyhole,
LockKeyholeOpen, LockKeyholeOpen,
@@ -48,7 +43,6 @@
Pencil, Pencil,
PenLine, PenLine,
Plus, Plus,
Quote,
RemoveFormatting, RemoveFormatting,
Save, Save,
Search, Search,
@@ -60,7 +54,6 @@
Siren, Siren,
Skull, Skull,
SquareLibrary, SquareLibrary,
Strikethrough,
Tags, Tags,
Trash2, Trash2,
TypeOutline, TypeOutline,
@@ -157,11 +150,24 @@
// let orig_entry_obj: key_val = $state({}); // let orig_entry_obj: key_val = $state({});
let orig_entry_obj: key_val | null = $state({}); let orig_entry_obj: key_val | null = $state({});
let tmp_entry_obj_changed: boolean = $state(false);
let tmp_entry_obj: key_val = $state({}); let tmp_entry_obj: key_val = $state({});
let updated_obj: boolean = $state(true); // Start with true to force population of orig and tmp values. let updated_obj: boolean = $state(true); // Start with true to force population of orig and tmp values.
let updated_idb: boolean = $state(true); // Updated in a separate browser session let updated_idb: boolean = $state(true); // Updated in a separate browser session
// Idiomatic Svelte 5 change detection
let tmp_entry_obj_changed = $derived.by(() => {
if (!tmp_entry_obj || !orig_entry_obj || not_obj(tmp_entry_obj) || not_obj(orig_entry_obj)) {
return false;
}
return (
tmp_entry_obj.content !== orig_entry_obj.content ||
tmp_entry_obj.name !== orig_entry_obj.name ||
tmp_entry_obj.tags !== orig_entry_obj.tags ||
tmp_entry_obj.category_code !== orig_entry_obj.category_code
);
});
// let tmp_entry_obj: key_val = { ...$lq__journal_entry_obj }; // let tmp_entry_obj: key_val = { ...$lq__journal_entry_obj };
// let tmp_entry_obj = $derived(async () => { // let tmp_entry_obj = $derived(async () => {
@@ -610,51 +616,7 @@
log_lvl: 1 log_lvl: 1
}); });
console.log('Journal entry updated successfully:', response); console.log('Journal entry updated successfully:', response);
// tick();
updated_obj = true; updated_obj = true;
// tick();
// orig_entry_obj = { ...await $lq__journal_entry_obj };
// tmp_entry_obj = { ...await $lq__journal_entry_obj };
// if ($journals_loc?.entry?.decrypt_kv[$lq__journal_entry_obj?.journal_entry_id]) {
// if (!tmp_entry_obj?.content && tmp_entry_obj?.content_encrypted) {
// console.log('TEST: Decrypting the content...');
// tmp_entry_obj.content = await ae_util.decrypt_wrapper(tmp_entry_obj?.content_encrypted, journal_key);
// tmp_entry_obj.content_md_html = handle_marked(tmp_entry_obj?.content);
// orig_entry_obj.content = await ae_util.decrypt_wrapper(orig_entry_obj?.content_encrypted, journal_key);
// orig_entry_obj.content_md_html = handle_marked(orig_entry_obj?.content);
// }
// if (!tmp_entry_obj?.history && tmp_entry_obj?.history_encrypted) {
// console.log('TEST: Decrypting the history...');
// tmp_entry_obj.history = await ae_util.decrypt_wrapper(tmp_entry_obj?.history_encrypted, journal_key);
// tmp_entry_obj.history_md_html = handle_marked(tmp_entry_obj?.history);
// orig_entry_obj.history = await ae_util.decrypt_wrapper(orig_entry_obj?.history_encrypted, journal_key);
// orig_entry_obj.history_md_html = handle_marked(orig_entry_obj?.history);
// }
// }
// console.log('TEST: tmp_entry_obj:', tmp_entry_obj);
// updated_obj = true;
// updated_idb = false;
// journals_func.update_ae_obj__journal_entry({
// api_cfg: $ae_api,
// journal_entry_id: $lq__journal_entry_obj?.journal_entry_id,
// data_kv: data_kv,
// log_lvl: 1,
// })
// .then((response) => {
// console.log('HERE: response', response);
// updated_obj = true;
// updated_idb = false;
// console.log('Journal entry updated successfully!');
// })
// tick();
} catch (error) { } catch (error) {
console.error('Error updating journal entry:', error); console.error('Error updating journal entry:', error);
alert('Failed to update journal entry.'); alert('Failed to update journal entry.');
@@ -954,95 +916,61 @@
items-center justify-between items-center justify-between
w-full w-full
bg-gray-100 dark:bg-gray-800 bg-gray-100 dark:bg-gray-800
p-1 rounded-lg shadow-md p-2 md:p-3 rounded-lg shadow-md
" "
> >
<div class="grow flex flex-row flex-wrap gap-2 items-center justify-start"> <div class="flex-1 flex flex-row flex-wrap gap-2 items-center justify-start min-w-[200px]">
<!-- Toggle edit for journal entry --> <!-- Toggle edit for journal entry -->
<button <button
type="button" type="button"
onclick={() => { onclick={() => {
// Ask if they would like to save changes before toggling edit mode off. const isEditing = $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] === 'current';
if (
$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == if (isEditing) {
'current' if (tmp_entry_obj_changed) {
) { // Action: SAVE
if (
tmp_entry_obj_changed &&
confirm(
'Would you like to save changes to this journal entry before exiting edit mode? Your changes will be lost if the page is refreshed.'
)
) {
update_journal_entry(); update_journal_entry();
} else {
// Action: CANCEL/BACK TO VIEW
$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] = false;
} }
$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] =
false;
} else { } else {
$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] = // Action: ENTER EDIT MODE
'current'; $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] = 'current';
} }
// if ($ae_loc.trusted_access && !$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id]) {
// trigger_decrypt = true;
// }
}} }}
class=" class="
btn btn-sm btn btn-icon btn-sm
preset-tonal-success hover:preset-filled-warning-500 preset-tonal-surface hover:preset-filled-primary-500
transition-all transition-all
" "
title="Toggle edit mode for this journal entry" class:preset-filled-success-500={tmp_entry_obj_changed && $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] === 'current'}
title="Toggle view/edit/save mode"
> >
{#if $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'current'} {#if $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] === 'current'}
<!-- <Pencil strokeWidth="2.5" color="blue" /> --> {#if tmp_entry_obj_changed}
<!-- <Hash strokeWidth="2.5" color="green" /> --> <Save size="1.25em" />
<!-- <Hash strokeWidth="1" color="red" /> -->
<NotebookText strokeWidth="2.5" color="green" />
<!-- <Hash strokeWidth="2.5" color="red" /> -->
<PenLine strokeWidth="2.5" color="red" />
{:else}
<!-- <Pencil strokeWidth="1" color="gray" /> -->
<!-- <PenLine strokeWidth="1" color="green" /> -->
{#if $lq__journal_entry_obj?.name}
<NotebookText
strokeWidth="2.5"
class="text-neutral-800/60 dark:text-neutral-200/60"
/>
{:else} {:else}
<CalendarClock <Eye size="1.25em" />
strokeWidth="2.5"
class="text-neutral-800/60 dark:text-neutral-200/60"
/>
{/if} {/if}
{:else}
<Hash strokeWidth="2.5" color="green" /> <Pencil size="1.25em" />
{/if} {/if}
</button> </button>
<h2 class="journal_entry__name text-md grow"> <h2 class="journal_entry__name text-base md:text-lg font-bold truncate max-w-[200px] md:max-w-md">
<!-- <span class="fas fa-spinner fa-spin"></span> -->
<!-- <span class="journal_entry__name inline-block"> -->
<!-- Allow for toggle between view and edit of journal entry. -->
{#if $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'current'} {#if $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] == 'current'}
<span class="flex flex-row gap-1 items-center justify-start"> <input
<!-- <Hash strokeWidth="2.5" color="red" /> --> type="text"
<input bind:value={tmp_entry_obj.name}
type="text" class="input input-sm md:input-md input-bordered w-full"
bind:value={tmp_entry_obj.name} placeholder="Journal Entry Name"
class="input input-bordered min-w-60 w-full text-gray-800 dark:text-gray-200" />
placeholder="Journal Entry Name"
title="Edit the name of this journal entry"
/>
</span>
{:else} {:else}
<span> <span>
{#if $lq__journal_entry_obj?.name} {#if $lq__journal_entry_obj?.name}
<!-- <NotebookText class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-200/60"/> -->
{@html $lq__journal_entry_obj?.name} {@html $lq__journal_entry_obj?.name}
{:else} {:else}
<!-- <CalendarClock class="mx-1 inline-block text-neutral-800/60 dark:text-neutral-200/60"/> -->
{ae_util.iso_datetime_formatter( {ae_util.iso_datetime_formatter(
$lq__journal_entry_obj?.created_on, $lq__journal_entry_obj?.created_on,
'datetime_iso_12_no_seconds' 'datetime_iso_12_no_seconds'
@@ -1050,7 +978,6 @@
{/if} {/if}
</span> </span>
{/if} {/if}
<!-- </span> -->
</h2> </h2>
</div> </div>
@@ -2662,7 +2589,8 @@ 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'}
<!-- Toolbar for CodeMirror --> <!-- Toolbar for CodeMirror (Temporarily disabled) -->
<!--
<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"> <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">
<button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Bold" onclick={() => wrapSelection(editorView, '**')}> <button type="button" class="btn btn-icon btn-sm preset-tonal-surface hover:preset-filled-secondary-500" title="Bold" onclick={() => wrapSelection(editorView, '**')}>
<Bold size="1.25em" /> <Bold size="1.25em" />
@@ -2701,6 +2629,7 @@ tabindex={$ae_loc.edit_mode ? 0 : -1} -->
<CodeXml size="1.25em" /> <CodeXml size="1.25em" />
</button> </button>
</div> </div>
-->
<E_app_codemirror_v5 <E_app_codemirror_v5
content={tmp_entry_obj?.content ?? ''} content={tmp_entry_obj?.content ?? ''}