Compare commits
2 Commits
86146f1ecf
...
555cfd4a93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555cfd4a93 | ||
|
|
a3f82fb74c |
@@ -2,14 +2,18 @@
|
|||||||
<div v-if="open" class="move-menu layout-block">
|
<div v-if="open" class="move-menu layout-block">
|
||||||
<button class="cancel-button" @click="close">Cancel</button>
|
<button class="cancel-button" @click="close">Cancel</button>
|
||||||
|
|
||||||
|
<button class="summarium-button" @click="onCategoryClick(null)">
|
||||||
|
SUMMARIUM
|
||||||
|
</button>
|
||||||
|
|
||||||
<template v-for="(category, i) in categories">
|
<template v-for="(category, i) in categories">
|
||||||
<category-row
|
<category-row
|
||||||
v-if="category !== fromCategory"
|
v-if="category !== fromCategory"
|
||||||
:category="category"
|
:category="category"
|
||||||
:index="i"
|
:index="i"
|
||||||
wrapper="button"
|
wrapper="button"
|
||||||
@click="onCategoryClick(category)"
|
|
||||||
:key="category"
|
:key="category"
|
||||||
|
@click="onCategoryClick(category)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,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()
|
||||||
@@ -39,7 +42,7 @@ const close = async () => {
|
|||||||
await window.api.moveClosed()
|
await window.api.moveClosed()
|
||||||
}
|
}
|
||||||
const onCategoryClick = async (category) => {
|
const onCategoryClick = async (category) => {
|
||||||
if (!category || !noteId.value) return
|
if (!noteId.value) return
|
||||||
|
|
||||||
await updateNote(noteId.value, { category: category })
|
await updateNote(noteId.value, { category: category })
|
||||||
|
|
||||||
@@ -55,10 +58,20 @@ watch(open, async () => {
|
|||||||
width: 50vw;
|
width: 50vw;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-left: 1px solid var(--grey-100);
|
border-left: 1px solid var(--grey-100);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.cancel-button {
|
.cancel-button {
|
||||||
color: var(--grey-100);
|
color: var(--grey-100);
|
||||||
padding: 9px 0 16px;
|
padding: 9px 0 16px;
|
||||||
}
|
}
|
||||||
|
.summarium-button {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user