new title structure

This commit is contained in:
nicwands
2026-03-31 13:39:08 -04:00
parent a3f82fb74c
commit 555cfd4a93
9 changed files with 132 additions and 65 deletions

View File

@@ -21,10 +21,9 @@
<script setup> <script setup>
import CategoryRow from '@/components/CategoryRow.vue' import CategoryRow from '@/components/CategoryRow.vue'
import { computed, onBeforeUnmount, watch } from 'vue' import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import useNotes from '@/composables/useNotes' import useNotes from '@/composables/useNotes'
import useState from '@/composables/useState'
import _omit from 'lodash/omit' import _omit from 'lodash/omit'
const { categories, updateNote } = useNotes() const { categories, updateNote } = useNotes()

View File

@@ -60,6 +60,10 @@ const moveActive = computed(() => route.query.move === props.note.id)
align-items: flex-start; align-items: flex-start;
gap: 2px; gap: 2px;
.title {
white-space: break-spaces;
word-break: break-word;
}
.action { .action {
opacity: 0; opacity: 0;

View File

@@ -1,38 +1,27 @@
<template> <template>
<div class="note-download"> <div class="note-download">
<button @click="download">{{ noteTitle }}.md </button> <button @click="download">
<span class="title-text">{{ noteTitle }}</span>
<span class="extension">.md </span>
</button>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
import _kebabCase from 'lodash/kebabCase' import _kebabCase from 'lodash/kebabCase'
import { computed } from 'vue'
const DEFAULT_TITLE = 'Untitled' const DEFAULT_TITLE = 'Untitled'
const props = defineProps({ const props = defineProps({
editor: { title: String,
type: Object, editor: Object,
required: true,
},
}) })
const noteTitle = computed(() => { const noteTitle = computed(() => _kebabCase(props.title || DEFAULT_TITLE))
if (!props.editor) return DEFAULT_TITLE
let title
const doc = props.editor.state.doc
const firstNode = doc.firstChild
if (!firstNode || firstNode.type.name !== 'heading') title = DEFAULT_TITLE
title = firstNode.textContent.trim() || DEFAULT_TITLE
return _kebabCase(title)
})
const download = () => { const download = () => {
if (!props.editor) return if (!noteTitle.value || !props.editor) return
const content = props.editor.getMarkdown() const content = props.editor.getMarkdown()
const blob = new Blob([content], { type: 'text/plain' }) const blob = new Blob([content], { type: 'text/plain' })
@@ -53,6 +42,24 @@ const download = () => {
justify-content: center; justify-content: center;
button { button {
display: inline-flex;
margin: auto;
justify-content: center;
.title-text {
display: block;
max-width: min(
300px,
calc(100vw - var(--layout-margin) * 2) - 36px
);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.extension {
display: block;
flex-shrink: 0;
}
&:hover { &:hover {
color: var(--theme-accent); color: var(--theme-accent);
} }

View File

@@ -1,5 +1,15 @@
<template> <template>
<div v-if="editor" class="note-editor"> <div v-if="editor" class="note-editor">
<div class="title">
<div
:class="['title-input p', { placeholder: !title }]"
contenteditable="true"
ref="titleInput"
@input="onTitleChange"
@keydown="onTitleKeydown"
/>
</div>
<editor-content :editor="editor" class="editor-wrap" /> <editor-content :editor="editor" class="editor-wrap" />
<editor-menu :editor="editor" /> <editor-menu :editor="editor" />
@@ -9,13 +19,22 @@
</template> </template>
<script setup> <script setup>
import { onBeforeUnmount, onMounted, shallowRef } from 'vue' import {
onBeforeUnmount,
onMounted,
shallowRef,
ref,
watch,
nextTick,
} from 'vue'
import PageLoading from '@/components/PageLoading.vue' import PageLoading from '@/components/PageLoading.vue'
import { Editor, EditorContent } from '@tiptap/vue-3' import { Editor, EditorContent } from '@tiptap/vue-3'
import { extensions } from '@/libs/editorExtensions'
import useNotes from '@/composables/useNotes' import useNotes from '@/composables/useNotes'
import _debounce from 'lodash/debounce' import _debounce from 'lodash/debounce'
import EditorMenu from './Menu.vue' import EditorMenu from './Menu.vue'
import { extensions } from '@/libs/editorExtensions'
const TITLE_CHAR_LIMIT = 100
const props = defineProps({ const props = defineProps({
id: { id: {
@@ -25,6 +44,8 @@ const props = defineProps({
}) })
const editor = shallowRef() const editor = shallowRef()
const title = ref('')
const titleInput = ref()
const { loadNote, updateNote } = useNotes() const { loadNote, updateNote } = useNotes()
@@ -32,25 +53,17 @@ const onUpdate = _debounce(async ({ editor }) => {
const json = editor.getJSON() const json = editor.getJSON()
const text = editor.getText() const text = editor.getText()
// Get doc title
let title
const doc = editor.state.doc
const firstNode = doc.firstChild
if (!firstNode || firstNode.type.name !== 'heading') title = 'Untitled'
title = firstNode.textContent.trim() || 'Untitled'
await updateNote(props.id, { await updateNote(props.id, {
title,
content: json, content: json,
plainText: text, plainText: text,
}) })
}, 300) }, 500)
onMounted(async () => { onMounted(async () => {
const note = await loadNote(props.id) const note = await loadNote(props.id)
title.value = note.title || ''
editor.value = new Editor({ editor.value = new Editor({
extensions, extensions,
content: note.content || [], content: note.content || [],
@@ -59,31 +72,69 @@ onMounted(async () => {
// Clear any highlights from find feature // Clear any highlights from find feature
editor.value.chain().selectAll().unsetHighlight().run() editor.value.chain().selectAll().unsetHighlight().run()
await nextTick()
if (titleInput.value) {
titleInput.value.textContent = title.value
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.value?.destroy?.() editor.value?.destroy?.()
}) })
// Make sure title input is synced
watch(title, async (newVal) => {
if (!titleInput.value) return
if (titleInput.value.textContent !== newVal) {
titleInput.value.textContent = newVal
}
})
const updateTitle = _debounce((title) => {
updateNote(props.id, {
title: title === '' ? 'Untitled' : title,
})
}, 500)
const onTitleChange = (e) => {
title.value = e.target.textContent
updateTitle(title.value)
}
const onTitleKeydown = (e) => {
if (title.value.length >= TITLE_CHAR_LIMIT && e.key.length === 1) {
e.preventDefault()
}
}
defineExpose({ defineExpose({
title,
editor, editor,
}) })
</script> </script>
<style lang="scss"> <style lang="scss">
.note-editor { .note-editor {
.title {
width: 100%;
margin-bottom: 40px;
padding-bottom: 0.5em;
border-bottom: 1px dashed currentColor;
.title-input {
white-space: break-spaces;
word-break: break-word;
@include drop-cap;
&.placeholder:before {
content: 'Title';
color: var(--grey-100);
}
}
}
h1 { h1 {
font-weight: 700 !important; font-weight: 700 !important;
@include p; @include p;
&:first-child {
@include drop-cap;
}
}
h1.is-editor-empty:first-child::before {
color: var(--grey-100);
content: attr(data-placeholder);
pointer-events: none;
@include drop-cap;
} }
p strong { p strong {
font-weight: 700; font-weight: 700;
@@ -155,6 +206,9 @@ defineExpose({
background: currentColor; background: currentColor;
} }
} }
u {
text-decoration: underline;
}
.highlighted { .highlighted {
background: var(--theme-accent); background: var(--theme-accent);
color: var(--theme-bg); color: var(--theme-bg);

View File

@@ -3,34 +3,18 @@ import StarterKit from '@tiptap/starter-kit'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { TaskList, TaskItem } from '@tiptap/extension-list' import { TaskList, TaskItem } from '@tiptap/extension-list'
import { Highlight } from '@tiptap/extension-highlight' import { Highlight } from '@tiptap/extension-highlight'
import Document from '@tiptap/extension-document'
import { all, createLowlight } from 'lowlight' import { all, createLowlight } from 'lowlight'
import { Placeholder } from '@tiptap/extensions'
// Lowlight setup // Lowlight setup
const lowlight = createLowlight(all) const lowlight = createLowlight(all)
// Force note format
const CustomDocument = Document.extend({
content: 'heading block*',
})
export const extensions = [ export const extensions = [
CustomDocument,
StarterKit.configure({ StarterKit.configure({
document: false,
heading: { levels: [1] }, heading: { levels: [1] },
trailingNode: { trailingNode: {
node: 'paragraph', node: 'paragraph',
}, },
}), }),
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === 'heading') {
return 'Title'
}
},
}),
TaskList, TaskList,
TaskItem, TaskItem,
Highlight.configure({ Highlight.configure({

View File

@@ -57,5 +57,6 @@
&:first-child::first-letter { &:first-child::first-letter {
font-family: var(--font-display); font-family: var(--font-display);
font-size: 42px; font-size: 42px;
line-height: 0.7;
} }
} }

View File

@@ -27,8 +27,13 @@ const route = useRoute()
const id = route.params?.id const id = route.params?.id
const router = useRouter() const router = useRouter()
const { categories, loadCategoryNotes, updateCategory, notesChangeCount } = const {
useNotes() categories,
loadCategoryNotes,
updateCategory,
notesChangeCount,
loadCategories,
} = useNotes()
const notes = ref() const notes = ref()
@@ -40,6 +45,11 @@ async function refreshNotes() {
onMounted(async () => { onMounted(async () => {
await refreshNotes() await refreshNotes()
if (!categories.value?.length) {
await loadCategories()
console.log(categories.value)
}
}) })
watch(notesChangeCount, async () => { watch(notesChangeCount, async () => {
@@ -53,7 +63,8 @@ const onCategoryEdited = async (editedCategory) => {
} }
const categoryIndex = computed(() => { const categoryIndex = computed(() => {
return categories.value?.findIndex((category) => category === id) || 1 if (!categories.value) return 0
return categories.value?.findIndex((category) => category === id)
}) })
</script> </script>

View File

@@ -12,6 +12,8 @@
<div class="notes"> <div class="notes">
<note-row v-for="note in notes" :note="note" :key="note.id" /> <note-row v-for="note in notes" :note="note" :key="note.id" />
</div> </div>
<new-note />
</main> </main>
<page-loading v-else /> <page-loading v-else />
@@ -23,6 +25,7 @@ import { onMounted, ref, watch } from 'vue'
import CategoryRow from '@/components/CategoryRow.vue' import CategoryRow from '@/components/CategoryRow.vue'
import NoteRow from '@/components/NoteRow.vue' import NoteRow from '@/components/NoteRow.vue'
import PageLoading from '@/components/PageLoading.vue' import PageLoading from '@/components/PageLoading.vue'
import NewNote from '@/components/NewNote.vue'
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } = const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
useNotes() useNotes()
@@ -61,5 +64,9 @@ main.directory {
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
} }
.new-note {
display: block;
margin: 50px auto 0;
}
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<main class="note layout-block"> <main class="note layout-block">
<note-download :editor="editorRef?.editor" /> <note-download :title="editorRef?.title" :editor="editorRef?.editor" />
<note-find <note-find
:editor="editorRef?.editor" :editor="editorRef?.editor"