add: journal note tags and directories

This commit is contained in:
trisua 2025-06-21 19:44:28 -04:00
parent a37312fecf
commit af6fbdf04e
16 changed files with 722 additions and 78 deletions

View file

@ -1065,14 +1065,14 @@ details summary::-webkit-details-marker {
display: none;
}
details[open] summary {
details[open] > summary {
position: relative;
color: var(--color-primary);
background: var(--color-super-lowered);
color: var(--color-text-lowered) !important;
background: var(--color-super-lowered) !important;
margin-bottom: var(--pad-1);
}
details[open] summary::after {
details[open] > summary::after {
top: 0;
left: 0;
width: 5px;

View file

@ -2028,6 +2028,22 @@
(str (text "general:action.delete")))))
(text "{%- endif %}"))
(text "{% if selected_note -%}")
; open all details elements above the selected note
(script
("defer" "true")
(text "setTimeout(() => {
let cursor = document.querySelector(\"[ui_ident=active_note]\");
while (cursor) {
if (cursor.nodeName === \"DETAILS\") {
cursor.setAttribute(\"open\", \"true\");
}
cursor = cursor.parentElement;
}
}, 150);"))
(text "{%- endif %}")
(div
("class" "flex flex-col gap-2")
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
@ -2041,36 +2057,155 @@
(text "{%- endif %}")
; note listings
(text "{% for note in notes %} {% if not view_mode or note.title != \"journal.css\" -%}")
(div
("class" "flex flex-row gap-1")
(a
("href" "{% if owner -%} /@{{ owner.username }}/{{ journal.title }}/{{ note.title }} {%- else -%} /journals/{{ journal.id }}/{{ note.id }} {%- endif %}")
("class" "button justify-start w-full {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
(icon (text "file-text"))
(text "{{ note.title }}"))
(text "{% if user and user.id == journal.owner -%}")
(div
("class" "dropdown")
(button
("class" "big_icon {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
("style" "width: 32px")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(button
("onclick" "change_note_title('{{ note.id }}')")
(icon (text "pencil"))
(str (text "chats:action.rename")))
(button
("onclick" "delete_note('{{ note.id }}')")
("class" "red")
(icon (text "trash"))
(str (text "general:action.delete")))))
(text "{%- endif %}"))
(text "{%- endif %} {% endfor %}"))
(text "{{ self::notes_list_dir_listing_inner(dir=[0, 0, \"root\"], dirs=journal.dirs, notes=notes, owner=owner, journal=journal, view_mode=view_mode) }}"))
(text "{%- endif %}")
(text "{%- endmacro %}")
(text "{% macro notes_list_dir_listing(dir, dirs, notes, owner, journal, view_mode=false) -%}")
(details
(summary
("class" "button w-full justify-start raised w-full")
(icon (text "folder"))
(text "{{ dir[2] }}"))
(div
("class" "flex flex-col gap-2")
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
(text "{{ self::notes_list_dir_listing_inner(dir=dir, dirs=dirs, notes=notes, owner=owner, journal=journal, view_mode=view_mode) }}")))
(text "{%- endmacro %}")
(text "{% macro notes_list_dir_listing_inner(dir, dirs, notes, owner, journal, view_mode=false) -%}")
; child dirs
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
(text "{{ self::notes_list_dir_listing(dir=subdir, dirs=dirs, notes=notes, owner=owner, journal=journal) }}")
(text "{%- endif %} {% endfor %}")
; child notes
(text "{% for note in notes %} {% if note.dir == dir[0] -%} {% if not view_mode or note.title != \"journal.css\" -%}")
(text "{{ self::notes_list_note_listing(note=note, owner=owner, journal=journal) }}")
(text "{%- endif %} {%- endif %} {% endfor %}")
(text "{%- endmacro %}")
(text "{% macro notes_list_note_listing(owner, journal, note) -%}")
(div
("class" "flex flex-row gap-1")
("ui_ident" "{% if selected_note == note.id -%} active_note {%- else -%} inactive_note {%- endif %}")
(a
("href" "{% if owner -%} /@{{ owner.username }}/{{ journal.title }}/{{ note.title }} {%- else -%} /journals/{{ journal.id }}/{{ note.id }} {%- endif %}")
("class" "button justify-start w-full {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
(icon (text "file-text"))
(text "{{ note.title }}"))
(text "{% if user and user.id == journal.owner -%}")
(div
("class" "dropdown")
(button
("class" "big_icon {% if selected_note == note.id -%} lowered {%- else -%} raised {%- endif %}")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
("style" "width: 32px")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(button
("onclick" "change_note_title('{{ note.id }}')")
(icon (text "pencil"))
(str (text "chats:action.rename")))
(a
("class" "button")
("href" "/journals/{{ journal.id }}/{{ note.id }}#/tags")
(icon (text "tag"))
(str (text "journals:action.edit_tags")))
(button
("class" "button")
("onclick" "window.NOTE_MOVER_NOTE_ID = '{{ note.id }}'; document.getElementById('note_mover_dialog').showModal()")
(icon (text "brush-cleaning"))
(str (text "journals:action.move")))
(button
("onclick" "delete_note('{{ note.id }}')")
("class" "red")
(icon (text "trash"))
(str (text "general:action.delete")))))
(text "{%- endif %}"))
(text "{%- endmacro %}")
(text "{% macro note_tags(note) -%} {% if note and note.tags|length > 0 -%}")
(div
("class" "flex gap-1 flex-wrap")
(text "{% for tag in note.tags %}")
(a
("href" "{% if view_mode -%} /@{{ owner.username }} {%- else -%} /@{{ user.username }} {%- endif -%} /{{ journal.title }}?tag={{ tag }}")
("class" "notification chip")
(span (text "{{ tag }}")))
(text "{% endfor %}"))
(text "{%- endif %} {%- endmacro %}")
(text "{% macro directories_editor(dirs) -%}")
(button
("onclick" "create_directory('0')")
(icon (text "plus"))
(str (text "journals:action.create_root_dir")))
(text "{% for dir in dirs %} {% if dir[1] == 0 -%}")
(text "{{ self::directories_editor_listing(dir=dir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}")
(text "{%- endmacro %}")
(text "{% macro directories_editor_listing(dir, dirs) -%}")
(div
("class" "flex flex-row gap-1")
(button
("class" "justify-start lowered w-full")
(icon (text "folder-open"))
(text "{{ dir[2] }}"))
(div
("class" "dropdown")
(button
("class" "big_icon lowered")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
("style" "width: 32px")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(button
("onclick" "create_directory('{{ dir[0] }}')")
(icon (text "plus"))
(str (text "journals:action.create_subdir")))
(button
("onclick" "delete_directory('{{ dir[0] }}')")
("class" "red")
(icon (text "trash"))
(str (text "general:action.delete"))))))
(div
("class" "flex flex-col gap-2")
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
; subdir listings
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
(text "{{ self::directories_editor_listing(dir=subdir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}"))
(text "{%- endmacro %}")
(text "{% macro note_mover_dirs(dirs) -%}")
(text "{% for dir in dirs %} {% if dir[1] == 0 -%}")
(text "{{ self::note_mover_dirs_listing(dir=dir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}")
(text "{%- endmacro %}")
(text "{% macro note_mover_dirs_listing(dir, dirs) -%}")
(button
("onclick" "move_note_dir(window.NOTE_MOVER_NOTE_ID, '{{ dir[0] }}'); document.getElementById('note_mover_dialog').close()")
("class" "justify-start lowered w-full")
(icon (text "folder-open"))
(text "{{ dir[2] }}"))
(div
("class" "flex flex-col gap-2")
("style" "margin-left: 10px; padding-left: 5px; border-left: solid 2px var(--color-super-lowered); width: calc(100% - 10px)")
; subdir listings
(text "{% for subdir in dirs %} {% if subdir[1] == dir[0] -%}")
(text "{{ self::note_mover_dirs_listing(dir=subdir, dirs=dirs) }}")
(text "{%- endif %} {% endfor %}"))
(text "{%- endmacro %}")

View file

@ -83,23 +83,25 @@
(div
("class" "mobile_nav w-full flex items-center justify-between gap-2")
(div
("class" "flex gap-2 items-center")
(a
("class" "flex items-center")
("href" "/api/v1/auth/user/find/{{ journal.owner }}")
(text "{{ components::avatar(username=journal.owner, selector_type=\"id\", size=\"18px\") }}"))
("class" "flex flex-col gap-2")
(div
("class" "flex gap-2 items-center")
(a
("class" "flex items-center")
("href" "/api/v1/auth/user/find/{{ journal.owner }}")
(text "{{ components::avatar(username=journal.owner, selector_type=\"id\", size=\"18px\") }}"))
(text "{% if (view_mode and owner) or not view_mode -%}")
(a
("class" "flush")
("href" "{% if view_mode -%} /@{{ owner.username }}/{{ journal.title }} {%- else -%} /journals/{{ journal.id }}/0 {%- endif %}")
(b (text "{{ journal.title }}")))
(text "{%- endif %}")
(text "{% if (view_mode and owner) or not view_mode -%}")
(a
("class" "flush")
("href" "{% if view_mode -%} /@{{ owner.username }}/{{ journal.title }} {%- else -%} /journals/{{ journal.id }}/0 {%- endif %}")
(b (text "{{ journal.title }}")))
(text "{%- endif %}")
(text "{% if note -%}")
(span (text "/"))
(b (text "{{ note.title }}"))
(text "{%- endif %}"))
(text "{% if note -%}")
(span (text "/"))
(b (text "{{ note.title }}"))
(text "{%- endif %}")))
(text "{% if user and user.id == journal.owner and (not note or note.title != \"journal.css\") -%}")
(div
@ -196,10 +198,29 @@
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}")))))))
; users should also be able to manage the journal's sub directories here
(details
("class" "w-full")
(summary
("class" "button lowered w-full justify-start")
(icon (text "folders"))
(str (text "journals:label.directories")))
(div
("class" "card flex flex-col gap-2 lowered")
(text "{{ components::directories_editor(dirs=journal.dirs) }}")))
(text "{% else %}")
; we're in view mode; just show journal listing and notes as journal homepage
(div
("class" "card flex flex-col gap-2")
(text "{% if tag|length > 0 -%}")
(a
("href" "?")
("class" "notification chip w-content")
(text "{{ tag }}"))
(text "{%- endif %}")
(text "{{ components::journal_listing(journal=journal, notes=notes, selected_note=selected_note, selected_journal=selected_journal, view_mode=view_mode, owner=owner) }}"))
(text "{%- endif %}")
(text "{% else %}")
@ -252,15 +273,30 @@
("href" "#/preview")
("data-tab-button" "preview")
("data-turbo" "false")
(str (text "journals:label.preview_pane"))))
(str (text "journals:label.preview_pane")))
(a
("href" "#/tags")
("data-tab-button" "tags")
("data-turbo" "false")
("class" "hidden")
(str (text "journals:action.edit_tags"))))
(text "{%- endif %}")
; tabs
(text "{{ components::note_tags(note=note) }}")
(div
("data-tab" "editor")
("class" "flex flex-col gap-2 card")
("style" "animation: fadein ease-in-out 1 0.5s forwards running")
("id" "editor_tab"))
(div
("class" "flex flex-col gap-2 card")
("style" "animation: fadein ease-in-out 1 0.5s forwards running")
("id" "editor_tab"))
(button
("onclick" "change_note_content('{{ note.id }}')")
(icon (text "check"))
(str (text "general:action.save"))))
(div
("data-tab" "preview")
@ -268,10 +304,51 @@
("style" "animation: fadein ease-in-out 1 0.5s forwards running")
("id" "preview_tab"))
(button
("onclick" "change_note_content('{{ note.id }}')")
(icon (text "check"))
(str (text "general:action.save")))
(div
("data-tab" "tags")
("class" "flex flex-col gap-2 card hidden")
("style" "animation: fadein ease-in-out 1 0.5s forwards running")
(form
("onsubmit" "save_tags(event)")
("class" "flex flex-col gap-1")
(label
("for" "tags")
(str (text "journals:action.tags"))
(textarea
("type" "text")
("name" "tags")
("id" "tags")
("placeholder" "tags")
("required" "")
("minlength" "2")
("maxlength" "128")
(text "{% for tag in note.tags -%} {{ tag }}, {% endfor %}"))
(span ("class" "fade") (text "Tags should be separated by a comma.")))
(button
(icon (text "check"))
(str (text "general:action.save"))))
(script
(text "globalThis.save_tags = (e) => {
event.preventDefault();
fetch(\"/api/v1/notes/{{ selected_note }}/tags\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
tags: e.target.tags.value.split(\",\").map(t => t.trim()).filter(t => t),
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}")))
; init codemirror
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}"))
@ -281,6 +358,7 @@
document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" });
}
document.getElementById(\"editor_tab\").innerHTML = \"\";
globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), {
value: document.getElementById(\"editor_content\").innerHTML,
mode: \"{% if note.title == 'journal.css' -%} css {%- else -%} markdown {%- endif %}\",
@ -349,7 +427,11 @@
(div
("class" "flex w-full justify-between gap-2")
(span (text "Last updated: ") (span ("class" "date") (text "{{ note.edited }}")))
(div
("class" "flex flex-col gap-2")
(span (text "Last updated: ") (span ("class" "date") (text "{{ note.edited }}")))
(text "{{ components::note_tags(note=note) }}"))
(text "{% if user and user.id == owner.id -%}")
(button
("class" "small")
@ -600,6 +682,99 @@
});
}
globalThis.create_directory = async (parent) => {
const name = await trigger(\"atto::prompt\", [\"Directory name:\"]);
if (!name) {
return;
}
fetch(\"/api/v1/journals/{{ selected_journal }}/dirs\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
name,
parent,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}
globalThis.delete_directory = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? This will delete all notes within this directory.\",
]))
) {
return;
}
fetch(\"/api/v1/journals/{{ selected_journal }}/dirs\", {
method: \"DELETE\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
id,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
fetch(`/api/v1/notes/{{ selected_journal }}/dir/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
});
}
globalThis.move_note_dir = async (id, dir) => {
fetch(`/api/v1/notes/${id}/dir`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
dir,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}
// sidebars
window.SIDEBARS_OPEN = false;
if (new URLSearchParams(window.location.search).get(\"nav\") === \"true\") {
@ -642,4 +817,24 @@
notes_list.style.left = \"0\";
}
}")))
(text "{% if journal -%}")
; note mover
(dialog
("id" "note_mover_dialog")
(div
("class" "inner flex flex-col gap-2")
(p (text "Select a directory to move this note into:"))
(text "{{ components::note_mover_dirs(dirs=journal.dirs) }}")
(div
("class" "flex justify-between")
(div)
(div
("class" "flex gap-2")
(button
("class" "bold red lowered")
("onclick" "document.getElementById('note_mover_dialog').close()")
("type" "button")
(text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}"))))))
(text "{%- endif %}")
(text "{% endblock %}")