new title structure
This commit is contained in:
@@ -21,10 +21,9 @@
|
||||
|
||||
<script setup>
|
||||
import CategoryRow from '@/components/CategoryRow.vue'
|
||||
import { computed, onBeforeUnmount, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useNotes from '@/composables/useNotes'
|
||||
import useState from '@/composables/useState'
|
||||
import _omit from 'lodash/omit'
|
||||
|
||||
const { categories, updateNote } = useNotes()
|
||||
|
||||
@@ -60,6 +60,10 @@ const moveActive = computed(() => route.query.move === props.note.id)
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
|
||||
.title {
|
||||
white-space: break-spaces;
|
||||
word-break: break-word;
|
||||
}
|
||||
.action {
|
||||
opacity: 0;
|
||||
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import _kebabCase from 'lodash/kebabCase'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const DEFAULT_TITLE = 'Untitled'
|
||||
|
||||
const props = defineProps({
|
||||
editor: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
title: String,
|
||||
editor: Object,
|
||||
})
|
||||
|
||||
const noteTitle = computed(() => {
|
||||
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 noteTitle = computed(() => _kebabCase(props.title || DEFAULT_TITLE))
|
||||
|
||||
const download = () => {
|
||||
if (!props.editor) return
|
||||
if (!noteTitle.value || !props.editor) return
|
||||
|
||||
const content = props.editor.getMarkdown()
|
||||
const blob = new Blob([content], { type: 'text/plain' })
|
||||
@@ -53,6 +42,24 @@ const download = () => {
|
||||
justify-content: center;
|
||||
|
||||
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 {
|
||||
color: var(--theme-accent);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<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-menu :editor="editor" />
|
||||
@@ -9,13 +19,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
||||
import {
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
shallowRef,
|
||||
ref,
|
||||
watch,
|
||||
nextTick,
|
||||
} from 'vue'
|
||||
import PageLoading from '@/components/PageLoading.vue'
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||
import { extensions } from '@/libs/editorExtensions'
|
||||
import useNotes from '@/composables/useNotes'
|
||||
import _debounce from 'lodash/debounce'
|
||||
import EditorMenu from './Menu.vue'
|
||||
import { extensions } from '@/libs/editorExtensions'
|
||||
|
||||
const TITLE_CHAR_LIMIT = 100
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -25,6 +44,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const editor = shallowRef()
|
||||
const title = ref('')
|
||||
const titleInput = ref()
|
||||
|
||||
const { loadNote, updateNote } = useNotes()
|
||||
|
||||
@@ -32,25 +53,17 @@ const onUpdate = _debounce(async ({ editor }) => {
|
||||
const json = editor.getJSON()
|
||||
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, {
|
||||
title,
|
||||
content: json,
|
||||
plainText: text,
|
||||
})
|
||||
}, 300)
|
||||
}, 500)
|
||||
|
||||
onMounted(async () => {
|
||||
const note = await loadNote(props.id)
|
||||
|
||||
title.value = note.title || ''
|
||||
|
||||
editor.value = new Editor({
|
||||
extensions,
|
||||
content: note.content || [],
|
||||
@@ -59,31 +72,69 @@ onMounted(async () => {
|
||||
|
||||
// Clear any highlights from find feature
|
||||
editor.value.chain().selectAll().unsetHighlight().run()
|
||||
|
||||
await nextTick()
|
||||
if (titleInput.value) {
|
||||
titleInput.value.textContent = title.value
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
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({
|
||||
title,
|
||||
editor,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.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 {
|
||||
font-weight: 700 !important;
|
||||
@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 {
|
||||
font-weight: 700;
|
||||
@@ -155,6 +206,9 @@ defineExpose({
|
||||
background: currentColor;
|
||||
}
|
||||
}
|
||||
u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.highlighted {
|
||||
background: var(--theme-accent);
|
||||
color: var(--theme-bg);
|
||||
|
||||
@@ -3,34 +3,18 @@ import StarterKit from '@tiptap/starter-kit'
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import { TaskList, TaskItem } from '@tiptap/extension-list'
|
||||
import { Highlight } from '@tiptap/extension-highlight'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import { all, createLowlight } from 'lowlight'
|
||||
import { Placeholder } from '@tiptap/extensions'
|
||||
|
||||
// Lowlight setup
|
||||
const lowlight = createLowlight(all)
|
||||
|
||||
// Force note format
|
||||
const CustomDocument = Document.extend({
|
||||
content: 'heading block*',
|
||||
})
|
||||
|
||||
export const extensions = [
|
||||
CustomDocument,
|
||||
StarterKit.configure({
|
||||
document: false,
|
||||
heading: { levels: [1] },
|
||||
trailingNode: {
|
||||
node: 'paragraph',
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === 'heading') {
|
||||
return 'Title'
|
||||
}
|
||||
},
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem,
|
||||
Highlight.configure({
|
||||
|
||||
@@ -57,5 +57,6 @@
|
||||
&:first-child::first-letter {
|
||||
font-family: var(--font-display);
|
||||
font-size: 42px;
|
||||
line-height: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,13 @@ const route = useRoute()
|
||||
const id = route.params?.id
|
||||
const router = useRouter()
|
||||
|
||||
const { categories, loadCategoryNotes, updateCategory, notesChangeCount } =
|
||||
useNotes()
|
||||
const {
|
||||
categories,
|
||||
loadCategoryNotes,
|
||||
updateCategory,
|
||||
notesChangeCount,
|
||||
loadCategories,
|
||||
} = useNotes()
|
||||
|
||||
const notes = ref()
|
||||
|
||||
@@ -40,6 +45,11 @@ async function refreshNotes() {
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshNotes()
|
||||
|
||||
if (!categories.value?.length) {
|
||||
await loadCategories()
|
||||
console.log(categories.value)
|
||||
}
|
||||
})
|
||||
|
||||
watch(notesChangeCount, async () => {
|
||||
@@ -53,7 +63,8 @@ const onCategoryEdited = async (editedCategory) => {
|
||||
}
|
||||
|
||||
const categoryIndex = computed(() => {
|
||||
return categories.value?.findIndex((category) => category === id) || 1
|
||||
if (!categories.value) return 0
|
||||
return categories.value?.findIndex((category) => category === id)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<div class="notes">
|
||||
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
||||
</div>
|
||||
|
||||
<new-note />
|
||||
</main>
|
||||
|
||||
<page-loading v-else />
|
||||
@@ -23,6 +25,7 @@ import { onMounted, ref, watch } from 'vue'
|
||||
import CategoryRow from '@/components/CategoryRow.vue'
|
||||
import NoteRow from '@/components/NoteRow.vue'
|
||||
import PageLoading from '@/components/PageLoading.vue'
|
||||
import NewNote from '@/components/NewNote.vue'
|
||||
|
||||
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
|
||||
useNotes()
|
||||
@@ -61,5 +64,9 @@ main.directory {
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.new-note {
|
||||
display: block;
|
||||
margin: 50px auto 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<main class="note layout-block">
|
||||
<note-download :editor="editorRef?.editor" />
|
||||
<note-download :title="editorRef?.title" :editor="editorRef?.editor" />
|
||||
|
||||
<note-find
|
||||
:editor="editorRef?.editor"
|
||||
|
||||
Reference in New Issue
Block a user