diff --git a/frontend/src/components/NotesRichTextEditor.vue b/frontend/src/components/NotesRichTextEditor.vue index 311e78c..4ae25a9 100644 --- a/frontend/src/components/NotesRichTextEditor.vue +++ b/frontend/src/components/NotesRichTextEditor.vue @@ -54,6 +54,7 @@ const allowedTags = [ 'blockquote', 'br', 'em', + 'h1', 'h2', 'h3', 'li', @@ -93,9 +94,7 @@ const NoteTextSize = Mark.create({ }, renderHTML: (attributes) => { const size = attributes.size as NoteTextSizeStep | undefined - return size && - noteTextSizeSteps.includes(size) && - size !== 'normal' + return size && noteTextSizeSteps.includes(size) && size !== 'normal' ? { 'data-note-size': size } : {} }, @@ -119,7 +118,16 @@ function sanitizeEditorHtml(body: string): string { ) } -let acceptedHtml = sanitizeEditorHtml(props.modelValue) +function withFirstLineHeading(html: string): string { + const match = html.match( + /^(\s*)<(p|h[1-3])(?:\s[^>]*)?>([\s\S]*?)<\/\2>([\s\S]*)$/i, + ) + if (!match) return `

${html}` + + return `${match[1]}

${match[3]}

${match[4]}` +} + +let acceptedHtml = withFirstLineHeading(sanitizeEditorHtml(props.modelValue)) const editor = useEditor({ content: acceptedHtml, @@ -127,7 +135,7 @@ const editor = useEditor({ StarterKit.configure({ code: false, codeBlock: false, - heading: { levels: [2, 3] }, + heading: { levels: [1, 2, 3] }, horizontalRule: false, link: false, strike: {}, @@ -137,6 +145,20 @@ const editor = useEditor({ Placeholder.configure({ placeholder: props.placeholder }), ], injectCSS: false, + editorProps: { + handleKeyDown: (view, event) => { + if (event.key !== 'Backspace' || !view.state.selection.empty) return false + + const { doc, selection } = view.state + const firstLine = doc.firstChild + return ( + firstLine?.type.name === 'heading' && + firstLine.attrs.level === 1 && + firstLine.content.size === 0 && + selection.$from.parent === firstLine + ) + }, + }, onUpdate: ({ editor: currentEditor }) => { const safeHtml = String( DOMPurify.sanitize(currentEditor.getHTML(), { @@ -157,8 +179,9 @@ const editor = useEditor({ function adjustTextSize(direction: -1 | 1): void { if (!editor.value || editor.value.state.selection.empty) return - const currentSize = editor.value.getAttributes('noteTextSize') - .size as NoteTextSizeStep | undefined + const currentSize = editor.value.getAttributes('noteTextSize').size as + | NoteTextSizeStep + | undefined const currentIndex = currentSize ? noteTextSizeSteps.indexOf(currentSize) : normalTextSizeIndex @@ -186,7 +209,8 @@ function toggleSelectionQuote(): void { const { from, to } = editor.value.state.selection const { doc, tr } = editor.value.state const selectedText = doc.textBetween(from, to) - const isQuotedInside = selectedText.startsWith('„') && selectedText.endsWith('“') + const isQuotedInside = + selectedText.startsWith('„') && selectedText.endsWith('“') const isQuotedOutside = from > 1 && doc.textBetween(from - 1, from) === '„' && @@ -211,7 +235,7 @@ watch( () => props.modelValue, (body) => { if (!editor.value) return - const safeHtml = sanitizeEditorHtml(body) + const safeHtml = withFirstLineHeading(sanitizeEditorHtml(body)) if (editor.value.getHTML() === safeHtml) return acceptedHtml = safeHtml editor.value.commands.setContent(safeHtml, { emitUpdate: false }) @@ -421,6 +445,7 @@ onBeforeUnmount(() => editor.value?.destroy()) margin: 0 0 0.55em; } +:deep(.tiptap h1), :deep(.tiptap h2), :deep(.tiptap h3) { margin: 0.75em 0 0.35em; @@ -428,6 +453,10 @@ onBeforeUnmount(() => editor.value?.destroy()) line-height: 1.15; } +:deep(.tiptap h1) { + font-size: 32px; +} + :deep(.tiptap h2) { font-size: 28px; } diff --git a/frontend/src/views/apps/NotesApp.menu.contract.test.ts b/frontend/src/views/apps/NotesApp.menu.contract.test.ts index e347608..d94f1f1 100644 --- a/frontend/src/views/apps/NotesApp.menu.contract.test.ts +++ b/frontend/src/views/apps/NotesApp.menu.contract.test.ts @@ -7,6 +7,26 @@ const menuSource = source.slice( source.indexOf('') + ''.length, ) +const listSource = source.slice( + source.indexOf(' { + it('places the Sky searchbar and create action together at the bottom', () => { + const composerSource = listSource.slice( + listSource.indexOf('') + ''.length, + ) + + expect(composerSource).toContain('') + }) +}) describe('NotesApp more menu', () => { it('uses the shared Feather-style action sheet', () => { diff --git a/frontend/src/views/apps/NotesApp.vue b/frontend/src/views/apps/NotesApp.vue index 4e54d72..20b10a0 100644 --- a/frontend/src/views/apps/NotesApp.vue +++ b/frontend/src/views/apps/NotesApp.vue @@ -5,12 +5,10 @@ import { kLink, kList, kListButton, - kListInput, kListItem, kNavbar, kNavbarBackLink, kPage, - kSearchbar, } from 'konsta/vue' import { Ellipsis, @@ -26,7 +24,7 @@ import NotesRichTextEditor from '@/components/NotesRichTextEditor.vue' import { useNotesStore } from '@/stores/notes' import { useEasyShareStore } from '@/stores/easyshare' import { usePhoneStore } from '@/stores/phone' -import { SkyActionSheet, SkyButton } from '@/ui' +import { SkyActionSheet, SkyButton, SkyFab, SkySearchbar } from '@/ui' import type { Note } from '@/utils/notes' import { noteBodyToPlainText } from '@/utils/noteRichText' @@ -36,7 +34,6 @@ const easyShare = useEasyShareStore() const searchQuery = ref('') const editorId = ref(null) const editorOpened = ref(false) -const draftTitle = ref('') const draftBody = ref('') const menuOpened = ref(false) const currentNote = computed(() => @@ -100,24 +97,18 @@ function noteSubtitle(note: Note): string { return `${noteDate(note)} · ${notePreview(note)}` } -function updateSearch(event: Event): void { - searchQuery.value = (event.target as HTMLInputElement).value -} - -function updateTitle(event: Event): void { - draftTitle.value = (event.target as HTMLInputElement).value +function titleFromDraftBody(body: string): string { + return noteBodyToPlainText(body).split('\n')[0]?.trim() ?? '' } function createNote(): void { editorId.value = null - draftTitle.value = '' draftBody.value = '' editorOpened.value = true } function editNote(note: Note): void { editorId.value = note.id - draftTitle.value = note.title draftBody.value = note.body editorOpened.value = true } @@ -125,7 +116,7 @@ function editNote(note: Note): void { function persistDraft(): Note | undefined { const draft = { body: draftBody.value, - title: draftTitle.value.trim(), + title: titleFromDraftBody(draftBody.value) || currentNote.value?.title.trim() || '', } if (editorId.value) { @@ -183,29 +174,10 @@ function shareNote(): void { + +
+ + + + +
@@ -264,17 +258,6 @@ function shareNote(): void {
- - -