Replace entry dialog with full-page edit view
- Remove Entry_Dialog class; edit/new now navigate to #edit/<id> and #new/... - render_edit_view() builds a full-page layout: header with title input, Write/Preview tabs, and a metadata sidebar - Tab key inserts a literal tab character in the editor - Ctrl+S saves from both title input and textarea - Body/main element uses flex column layout so edit view fills viewport Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
478
public/app.mjs
478
public/app.mjs
@@ -10,8 +10,9 @@ class App_State {
|
|||||||
this.entry_types = [];
|
this.entry_types = [];
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
this.active_type = null; // null = all types
|
this.active_type = null; // null = all types
|
||||||
this.active_view = 'entries'; // 'entries' | 'entry' | 'manage-types'
|
this.active_view = 'entries'; // 'entries' | 'entry' | 'edit' | 'manage-types'
|
||||||
this.active_entry_id = null;
|
this.active_entry_id = null;
|
||||||
|
this.active_edit = null; // { mode: 'new'|'edit', id?, type?, parent_id? }
|
||||||
this.filter_status = 'open';
|
this.filter_status = 'open';
|
||||||
this.filter_priority = '';
|
this.filter_priority = '';
|
||||||
this.filter_tag = '';
|
this.filter_tag = '';
|
||||||
@@ -160,141 +161,6 @@ class Entry_Picker_Dialog {
|
|||||||
|
|
||||||
let entry_picker;
|
let entry_picker;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Entry dialog
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class Entry_Dialog {
|
|
||||||
constructor() {
|
|
||||||
const el = clone('t-entry-dialog');
|
|
||||||
document.body.appendChild(el);
|
|
||||||
this.dialog = el;
|
|
||||||
this.form = el.querySelector('form');
|
|
||||||
this.on_save = null;
|
|
||||||
this._editing_id = null;
|
|
||||||
|
|
||||||
// Tab switching
|
|
||||||
const tab_btns = el.querySelectorAll('.tab-btn');
|
|
||||||
const tab_panes = el.querySelectorAll('.tab-pane');
|
|
||||||
for (const btn of tab_btns) {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const target = btn.dataset.tab;
|
|
||||||
for (const b of tab_btns) { b.classList.toggle('active', b.dataset.tab === target); }
|
|
||||||
for (const p of tab_panes) { p.hidden = p.dataset.tab !== target; }
|
|
||||||
if (target === 'preview') { this._load_preview(); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent controls
|
|
||||||
el.querySelector('.btn-set-parent').addEventListener('click', () => {
|
|
||||||
const current_id = Number(this.form.elements['parent_id'].value) || null;
|
|
||||||
const self_id = this._editing_id;
|
|
||||||
const excluded = self_id ? new Set([self_id, ...get_descendants(self_id)]) : new Set();
|
|
||||||
if (current_id) { excluded.add(current_id); }
|
|
||||||
entry_picker.open(excluded, (picked_id) => { this._set_parent(picked_id); });
|
|
||||||
});
|
|
||||||
|
|
||||||
el.querySelector('.btn-clear-parent').addEventListener('click', () => { this._set_parent(null); });
|
|
||||||
el.querySelector('.btn-cancel').addEventListener('click', () => el.close());
|
|
||||||
this.form.addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.on_save) { this.on_save(this._read_form()); }
|
|
||||||
el.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_set_parent(parent_id) {
|
|
||||||
const hidden = this.form.elements['parent_id'];
|
|
||||||
const display = this.dialog.querySelector('.parent-display');
|
|
||||||
const clear_btn = this.dialog.querySelector('.btn-clear-parent');
|
|
||||||
if (parent_id) {
|
|
||||||
const parent = state.entries.find(e => e.id === parent_id);
|
|
||||||
hidden.value = parent_id;
|
|
||||||
display.textContent = parent ? `#${parent.id} ${parent.title}` : `#${parent_id}`;
|
|
||||||
show(clear_btn);
|
|
||||||
} else {
|
|
||||||
hidden.value = '';
|
|
||||||
display.textContent = '—';
|
|
||||||
hide(clear_btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_load_preview() {
|
|
||||||
const text = this.form.elements['body'].value;
|
|
||||||
const preview = this.dialog.querySelector('.preview-content');
|
|
||||||
if (!text.trim()) { preview.innerHTML = '<em style="color:#555">Nothing to preview.</em>'; return; }
|
|
||||||
preview.classList.add('loading');
|
|
||||||
preview.textContent = 'Loading…';
|
|
||||||
render_markdown(text).then(html => {
|
|
||||||
preview.classList.remove('loading');
|
|
||||||
preview.innerHTML = html;
|
|
||||||
}).catch(err => {
|
|
||||||
preview.classList.remove('loading');
|
|
||||||
preview.textContent = 'Error: ' + err.message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_read_form() {
|
|
||||||
const fd = new FormData(this.form);
|
|
||||||
const tags_raw = fd.get('tags').trim();
|
|
||||||
const parent_id_raw = fd.get('parent_id');
|
|
||||||
return {
|
|
||||||
type: fd.get('type'),
|
|
||||||
title: fd.get('title').trim(),
|
|
||||||
body: fd.get('body').trim(),
|
|
||||||
status: fd.get('status'),
|
|
||||||
priority: fd.get('priority'),
|
|
||||||
tags: tags_raw ? tags_raw.split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
||||||
parent_id: parent_id_raw ? Number(parent_id_raw) : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixed_type: string — type is pre-set and shown as label; null — show select
|
|
||||||
open(title_text, initial = {}, fixed_type, on_save) {
|
|
||||||
this._editing_id = initial.id ?? null;
|
|
||||||
set_text(this.dialog, '.dialog-title', title_text);
|
|
||||||
|
|
||||||
const type_display = this.dialog.querySelector('.type-display');
|
|
||||||
const type_select = this.dialog.querySelector('.type-select');
|
|
||||||
|
|
||||||
// Always populate select options
|
|
||||||
type_select.innerHTML = '';
|
|
||||||
for (const et of state.entry_types) {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = et.id;
|
|
||||||
opt.textContent = et.title;
|
|
||||||
type_select.appendChild(opt);
|
|
||||||
}
|
|
||||||
type_select.value = fixed_type ?? initial.type ?? (state.entry_types[0]?.id ?? '');
|
|
||||||
|
|
||||||
if (fixed_type) {
|
|
||||||
const et = state.entry_types.find(t => t.id === fixed_type);
|
|
||||||
type_display.textContent = et ? et.title : fixed_type;
|
|
||||||
show(type_display);
|
|
||||||
hide(type_select);
|
|
||||||
} else {
|
|
||||||
hide(type_display);
|
|
||||||
show(type_select);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.form.elements['title'].value = initial.title ?? '';
|
|
||||||
this.form.elements['body'].value = initial.body ?? '';
|
|
||||||
this.form.elements['status'].value = initial.status ?? 'open';
|
|
||||||
this.form.elements['priority'].value = initial.priority ?? 'normal';
|
|
||||||
this.form.elements['tags'].value = (initial.tags ?? []).join(', ');
|
|
||||||
this._set_parent(initial.parent_id ?? null);
|
|
||||||
|
|
||||||
// Always open on edit tab
|
|
||||||
for (const b of this.dialog.querySelectorAll('.tab-btn')) { b.classList.toggle('active', b.dataset.tab === 'edit'); }
|
|
||||||
for (const p of this.dialog.querySelectorAll('.tab-pane')) { p.hidden = p.dataset.tab !== 'edit'; }
|
|
||||||
|
|
||||||
this.on_save = on_save;
|
|
||||||
this.dialog.showModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry_dialog;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Entry type dialog
|
// Entry type dialog
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -707,20 +573,43 @@ function apply_hash(hash) {
|
|||||||
state.active_view = 'manage-types';
|
state.active_view = 'manage-types';
|
||||||
state.active_type = null;
|
state.active_type = null;
|
||||||
state.active_entry_id = null;
|
state.active_entry_id = null;
|
||||||
|
state.active_edit = null;
|
||||||
} else if (hash.startsWith('type/')) {
|
} else if (hash.startsWith('type/')) {
|
||||||
state.active_view = 'entries';
|
state.active_view = 'entries';
|
||||||
state.active_type = hash.slice(5);
|
state.active_type = hash.slice(5);
|
||||||
state.active_entry_id = null;
|
state.active_entry_id = null;
|
||||||
|
state.active_edit = null;
|
||||||
} else if (/^\d+$/.test(hash)) {
|
} else if (/^\d+$/.test(hash)) {
|
||||||
state.active_view = 'entry';
|
state.active_view = 'entry';
|
||||||
state.active_entry_id = Number(hash);
|
state.active_entry_id = Number(hash);
|
||||||
|
state.active_edit = null;
|
||||||
|
} else if (hash.startsWith('edit/')) {
|
||||||
|
state.active_view = 'edit';
|
||||||
|
state.active_edit = { mode: 'edit', id: Number(hash.slice(5)) };
|
||||||
|
} else if (hash === 'new' || hash.startsWith('new/')) {
|
||||||
|
state.active_view = 'edit';
|
||||||
|
const rest = hash.length > 4 ? hash.slice(4) : '';
|
||||||
|
const [type_part = '', parent_part = ''] = rest.split('/');
|
||||||
|
state.active_edit = {
|
||||||
|
mode: 'new',
|
||||||
|
type: (type_part && type_part !== '_') ? type_part : null,
|
||||||
|
parent_id: parent_part ? Number(parent_part) : null,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
state.active_view = 'entries';
|
state.active_view = 'entries';
|
||||||
state.active_type = null;
|
state.active_type = null;
|
||||||
state.active_entry_id = null;
|
state.active_entry_id = null;
|
||||||
|
state.active_edit = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navigate_new(type, parent_id) {
|
||||||
|
let hash = 'new';
|
||||||
|
if (type || parent_id) { hash += '/' + (type || '_'); }
|
||||||
|
if (parent_id) { hash += '/' + parent_id; }
|
||||||
|
navigate(hash);
|
||||||
|
}
|
||||||
|
|
||||||
function navigate(hash) {
|
function navigate(hash) {
|
||||||
history.pushState(null, '', '#' + hash);
|
history.pushState(null, '', '#' + hash);
|
||||||
apply_hash(hash);
|
apply_hash(hash);
|
||||||
@@ -859,6 +748,289 @@ function render_entry_detail(container) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Edit view
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function render_edit_view(container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
const ae = state.active_edit;
|
||||||
|
const entry = ae.mode === 'edit' ? state.entries.find(e => e.id === ae.id) : null;
|
||||||
|
if (ae.mode === 'edit' && !entry) {
|
||||||
|
container.textContent = `Entry #${ae.id} not found.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const back_hash = ae.mode === 'edit' ? String(ae.id) : (ae.type ? 'type/' + ae.type : 'all');
|
||||||
|
let current_parent_id = ae.parent_id ?? entry?.parent_id ?? null;
|
||||||
|
|
||||||
|
// View wrapper
|
||||||
|
const view = document.createElement('div');
|
||||||
|
view.className = 'edit-view';
|
||||||
|
|
||||||
|
// --- Header ---
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'edit-header';
|
||||||
|
|
||||||
|
const back_btn = document.createElement('a');
|
||||||
|
back_btn.className = 'detail-back';
|
||||||
|
back_btn.href = '#' + back_hash;
|
||||||
|
back_btn.textContent = '← Back';
|
||||||
|
back_btn.addEventListener('click', e => { e.preventDefault(); navigate(back_hash); });
|
||||||
|
header.appendChild(back_btn);
|
||||||
|
|
||||||
|
const title_input = document.createElement('input');
|
||||||
|
title_input.type = 'text';
|
||||||
|
title_input.className = 'edit-title-input';
|
||||||
|
title_input.placeholder = 'Title';
|
||||||
|
title_input.value = entry?.title ?? '';
|
||||||
|
header.appendChild(title_input);
|
||||||
|
|
||||||
|
const save_btn = document.createElement('button');
|
||||||
|
save_btn.className = 'btn-primary';
|
||||||
|
save_btn.textContent = 'Save';
|
||||||
|
const cancel_btn = document.createElement('button');
|
||||||
|
cancel_btn.textContent = 'Cancel';
|
||||||
|
cancel_btn.addEventListener('click', () => navigate(back_hash));
|
||||||
|
header.appendChild(save_btn);
|
||||||
|
header.appendChild(cancel_btn);
|
||||||
|
view.appendChild(header);
|
||||||
|
|
||||||
|
// --- Body area: (tabs + textarea/preview) | sidebar ---
|
||||||
|
const body_area = document.createElement('div');
|
||||||
|
body_area.className = 'edit-body-area';
|
||||||
|
|
||||||
|
// Tab bar
|
||||||
|
const tab_bar = document.createElement('div');
|
||||||
|
tab_bar.className = 'edit-tab-bar';
|
||||||
|
|
||||||
|
const write_tab = document.createElement('button');
|
||||||
|
write_tab.type = 'button';
|
||||||
|
write_tab.textContent = 'Write';
|
||||||
|
write_tab.className = 'edit-tab active';
|
||||||
|
|
||||||
|
const preview_tab = document.createElement('button');
|
||||||
|
preview_tab.type = 'button';
|
||||||
|
preview_tab.textContent = 'Preview';
|
||||||
|
preview_tab.className = 'edit-tab';
|
||||||
|
|
||||||
|
tab_bar.appendChild(write_tab);
|
||||||
|
tab_bar.appendChild(preview_tab);
|
||||||
|
|
||||||
|
// Textarea
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.className = 'edit-textarea';
|
||||||
|
textarea.value = entry?.body ?? '';
|
||||||
|
textarea.placeholder = 'Body (markdown)';
|
||||||
|
|
||||||
|
// Preview pane
|
||||||
|
const preview_el = document.createElement('div');
|
||||||
|
preview_el.className = 'edit-preview markup';
|
||||||
|
preview_el.hidden = true;
|
||||||
|
|
||||||
|
write_tab.addEventListener('click', () => {
|
||||||
|
write_tab.classList.add('active');
|
||||||
|
preview_tab.classList.remove('active');
|
||||||
|
textarea.hidden = false;
|
||||||
|
preview_el.hidden = true;
|
||||||
|
textarea.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
preview_tab.addEventListener('click', () => {
|
||||||
|
preview_tab.classList.add('active');
|
||||||
|
write_tab.classList.remove('active');
|
||||||
|
textarea.hidden = true;
|
||||||
|
preview_el.hidden = false;
|
||||||
|
const text = textarea.value;
|
||||||
|
if (!text.trim()) {
|
||||||
|
preview_el.innerHTML = '<em style="color:#555">Nothing to preview.</em>';
|
||||||
|
} else {
|
||||||
|
preview_el.textContent = '…';
|
||||||
|
render_markdown(text).then(html => { preview_el.innerHTML = html; });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ctrl+S to save
|
||||||
|
title_input.addEventListener('keydown', e => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); save_btn.click(); }
|
||||||
|
});
|
||||||
|
textarea.addEventListener('keydown', e => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); save_btn.click(); }
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
textarea.value = textarea.value.slice(0, start) + '\t' + textarea.value.slice(end);
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Sidebar ---
|
||||||
|
const sidebar = document.createElement('div');
|
||||||
|
sidebar.className = 'edit-sidebar';
|
||||||
|
|
||||||
|
function sidebar_group(label) {
|
||||||
|
const g = document.createElement('div');
|
||||||
|
g.className = 'edit-sidebar-group';
|
||||||
|
const l = document.createElement('div');
|
||||||
|
l.className = 'edit-sidebar-label';
|
||||||
|
l.textContent = label;
|
||||||
|
g.appendChild(l);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type
|
||||||
|
const type_group = sidebar_group('Type');
|
||||||
|
let type_el;
|
||||||
|
if (ae.mode === 'edit') {
|
||||||
|
const et = state.entry_types.find(t => t.id === entry.type);
|
||||||
|
type_el = document.createElement('span');
|
||||||
|
type_el.className = 'edit-sidebar-value';
|
||||||
|
type_el.textContent = et ? et.title : entry.type;
|
||||||
|
} else {
|
||||||
|
type_el = document.createElement('select');
|
||||||
|
type_el.className = 'edit-sidebar-select';
|
||||||
|
for (const et of state.entry_types) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = et.id; opt.textContent = et.title;
|
||||||
|
if (et.id === (ae.type ?? state.entry_types[0]?.id)) { opt.selected = true; }
|
||||||
|
type_el.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type_group.appendChild(type_el);
|
||||||
|
sidebar.appendChild(type_group);
|
||||||
|
|
||||||
|
// Status
|
||||||
|
const status_group = sidebar_group('Status');
|
||||||
|
const status_sel = document.createElement('select');
|
||||||
|
status_sel.className = 'edit-sidebar-select';
|
||||||
|
for (const v of ['open', 'deferred', 'done', 'cancelled']) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = v; opt.textContent = v;
|
||||||
|
if (v === (entry?.status ?? 'open')) { opt.selected = true; }
|
||||||
|
status_sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
status_group.appendChild(status_sel);
|
||||||
|
sidebar.appendChild(status_group);
|
||||||
|
|
||||||
|
// Priority
|
||||||
|
const priority_group = sidebar_group('Priority');
|
||||||
|
const priority_sel = document.createElement('select');
|
||||||
|
priority_sel.className = 'edit-sidebar-select';
|
||||||
|
for (const v of ['high', 'normal', 'low']) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = v; opt.textContent = v;
|
||||||
|
if (v === (entry?.priority ?? 'normal')) { opt.selected = true; }
|
||||||
|
priority_sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
priority_group.appendChild(priority_sel);
|
||||||
|
sidebar.appendChild(priority_group);
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
const tags_group = sidebar_group('Tags');
|
||||||
|
const tags_input = document.createElement('input');
|
||||||
|
tags_input.type = 'text';
|
||||||
|
tags_input.className = 'edit-sidebar-input';
|
||||||
|
tags_input.placeholder = 'comma-separated';
|
||||||
|
tags_input.value = (entry?.tags ?? []).join(', ');
|
||||||
|
tags_group.appendChild(tags_input);
|
||||||
|
sidebar.appendChild(tags_group);
|
||||||
|
|
||||||
|
// Parent
|
||||||
|
const parent_group = sidebar_group('Parent');
|
||||||
|
const parent_display = document.createElement('span');
|
||||||
|
parent_display.className = 'edit-sidebar-value';
|
||||||
|
|
||||||
|
function refresh_parent_display() {
|
||||||
|
if (current_parent_id) {
|
||||||
|
const p = state.entries.find(e => e.id === current_parent_id);
|
||||||
|
parent_display.textContent = p ? `#${p.id} ${p.title}` : `#${current_parent_id}`;
|
||||||
|
} else {
|
||||||
|
parent_display.textContent = '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh_parent_display();
|
||||||
|
|
||||||
|
const parent_btns = document.createElement('div');
|
||||||
|
parent_btns.className = 'edit-parent-btns';
|
||||||
|
|
||||||
|
const set_parent_btn = document.createElement('button');
|
||||||
|
set_parent_btn.textContent = 'Set…';
|
||||||
|
set_parent_btn.addEventListener('click', () => {
|
||||||
|
const excluded = entry ? new Set([entry.id, ...get_descendants(entry.id)]) : new Set();
|
||||||
|
if (current_parent_id) { excluded.add(current_parent_id); }
|
||||||
|
entry_picker.open(excluded, id => {
|
||||||
|
current_parent_id = id;
|
||||||
|
refresh_parent_display();
|
||||||
|
show(clear_parent_btn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const clear_parent_btn = document.createElement('button');
|
||||||
|
clear_parent_btn.textContent = '× Clear';
|
||||||
|
if (!current_parent_id) { hide(clear_parent_btn); }
|
||||||
|
clear_parent_btn.addEventListener('click', () => {
|
||||||
|
current_parent_id = null;
|
||||||
|
refresh_parent_display();
|
||||||
|
hide(clear_parent_btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
parent_btns.appendChild(set_parent_btn);
|
||||||
|
parent_btns.appendChild(clear_parent_btn);
|
||||||
|
parent_group.appendChild(parent_display);
|
||||||
|
parent_group.appendChild(parent_btns);
|
||||||
|
sidebar.appendChild(parent_group);
|
||||||
|
|
||||||
|
const editor_pane = document.createElement('div');
|
||||||
|
editor_pane.className = 'edit-editor-pane';
|
||||||
|
editor_pane.appendChild(tab_bar);
|
||||||
|
editor_pane.appendChild(textarea);
|
||||||
|
editor_pane.appendChild(preview_el);
|
||||||
|
|
||||||
|
body_area.appendChild(editor_pane);
|
||||||
|
body_area.appendChild(sidebar);
|
||||||
|
view.appendChild(body_area);
|
||||||
|
container.appendChild(view);
|
||||||
|
|
||||||
|
if (!title_input.value) { title_input.focus(); }
|
||||||
|
else { textarea.focus(); }
|
||||||
|
|
||||||
|
// Save handler
|
||||||
|
save_btn.addEventListener('click', async () => {
|
||||||
|
const data = {
|
||||||
|
type: ae.mode === 'new' ? (type_el.value ?? ae.type) : entry.type,
|
||||||
|
title: title_input.value.trim(),
|
||||||
|
body: textarea.value.trim(),
|
||||||
|
status: status_sel.value,
|
||||||
|
priority: priority_sel.value,
|
||||||
|
tags: tags_input.value.split(',').map(s => s.trim()).filter(Boolean),
|
||||||
|
parent_id: current_parent_id,
|
||||||
|
};
|
||||||
|
if (!data.title) { title_input.focus(); return; }
|
||||||
|
try {
|
||||||
|
save_btn.disabled = true;
|
||||||
|
save_btn.textContent = 'Saving…';
|
||||||
|
if (ae.mode === 'edit') {
|
||||||
|
const { entry: updated } = await api.update_entry(ae.id, data);
|
||||||
|
const idx = state.entries.findIndex(e => e.id === ae.id);
|
||||||
|
if (idx !== -1) { state.entries[idx] = updated; }
|
||||||
|
md_cache.delete(entry.title);
|
||||||
|
md_cache.delete(entry.body);
|
||||||
|
navigate(String(ae.id));
|
||||||
|
} else {
|
||||||
|
const { entry: created } = await api.create_entry(data);
|
||||||
|
state.entries.unshift(created);
|
||||||
|
navigate(String(created.id));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
save_btn.disabled = false;
|
||||||
|
save_btn.textContent = 'Save';
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Main render
|
// Main render
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -866,10 +1038,13 @@ function render_entry_detail(container) {
|
|||||||
function render() {
|
function render() {
|
||||||
render_nav();
|
render_nav();
|
||||||
const main = document.getElementById('main');
|
const main = document.getElementById('main');
|
||||||
|
main.classList.toggle('edit-mode', state.active_view === 'edit');
|
||||||
if (state.active_view === 'manage-types') {
|
if (state.active_view === 'manage-types') {
|
||||||
render_manage_types(main);
|
render_manage_types(main);
|
||||||
} else if (state.active_view === 'entry') {
|
} else if (state.active_view === 'entry') {
|
||||||
render_entry_detail(main);
|
render_entry_detail(main);
|
||||||
|
} else if (state.active_view === 'edit') {
|
||||||
|
render_edit_view(main);
|
||||||
} else {
|
} else {
|
||||||
render_entries(main);
|
render_entries(main);
|
||||||
}
|
}
|
||||||
@@ -880,35 +1055,11 @@ function render() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function open_add_dialog(parent_id, type_id) {
|
function open_add_dialog(parent_id, type_id) {
|
||||||
// If no type given and only one type exists, auto-select it
|
navigate_new(type_id, parent_id);
|
||||||
const resolved = type_id ?? (state.entry_types.length === 1 ? state.entry_types[0].id : null);
|
|
||||||
entry_dialog.open('New entry', { parent_id }, resolved, async (data) => {
|
|
||||||
try {
|
|
||||||
const { entry } = await api.create_entry(data);
|
|
||||||
state.entries.unshift(entry);
|
|
||||||
if (parent_id) { state.expanded.add(parent_id); }
|
|
||||||
render();
|
|
||||||
} catch (err) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function open_edit_dialog(entry) {
|
function open_edit_dialog(entry) {
|
||||||
entry_dialog.open('Edit entry', entry, entry.type, async (data) => {
|
navigate('edit/' + entry.id);
|
||||||
try {
|
|
||||||
const { entry: updated } = await api.update_entry(entry.id, data);
|
|
||||||
const idx = state.entries.findIndex(e => e.id === entry.id);
|
|
||||||
if (idx !== -1) { state.entries[idx] = updated; }
|
|
||||||
md_cache.delete(entry.title);
|
|
||||||
md_cache.delete(entry.body);
|
|
||||||
md_cache.delete(updated.title);
|
|
||||||
md_cache.delete(updated.body);
|
|
||||||
render();
|
|
||||||
} catch (err) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirm_delete(entry) {
|
function confirm_delete(entry) {
|
||||||
@@ -938,8 +1089,7 @@ async function init() {
|
|||||||
document.body.appendChild(tmpl);
|
document.body.appendChild(tmpl);
|
||||||
}
|
}
|
||||||
|
|
||||||
entry_picker = new Entry_Picker_Dialog();
|
entry_picker = new Entry_Picker_Dialog();
|
||||||
entry_dialog = new Entry_Dialog();
|
|
||||||
entry_type_dialog = new Entry_Type_Dialog();
|
entry_type_dialog = new Entry_Type_Dialog();
|
||||||
|
|
||||||
const [types_res, entries_res] = await Promise.all([
|
const [types_res, entries_res] = await Promise.all([
|
||||||
|
|||||||
176
public/style.css
176
public/style.css
@@ -6,7 +6,10 @@ body {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
@@ -608,3 +611,174 @@ dialog input:focus, dialog textarea:focus, dialog select:focus {
|
|||||||
.detail-children .task-list {
|
.detail-children .task-list {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Full-page edit mode */
|
||||||
|
header { flex-shrink: 0; }
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.edit-mode {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-title-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-title-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #5588e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-body-area {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 240px;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-editor-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid #2a2a2a;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-tab-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #161616;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-tab {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-tab:hover { color: #aaa; background: transparent; }
|
||||||
|
.edit-tab.active { color: #e0e0e0; border-bottom-color: #5588e0; background: transparent; }
|
||||||
|
|
||||||
|
.edit-textarea {
|
||||||
|
flex: 1;
|
||||||
|
resize: none;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #111;
|
||||||
|
border: none;
|
||||||
|
color: #d0d0d0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
background: #141414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-preview {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #111;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.edit-sidebar {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #1a1a1a;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-sidebar-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-sidebar-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-sidebar-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 0.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-sidebar-select,
|
||||||
|
.edit-sidebar-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
background: #242424;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-sidebar-select:focus,
|
||||||
|
.edit-sidebar-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #5588e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-parent-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user