1
0

feat(editor): add hotkeys to quickly edit and discard (#2265)

Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2265
Reviewed-by: konrad <k@knt.li>
This commit is contained in:
konrad 2024-05-25 08:13:10 +00:00
commit fd66e6875c
3 changed files with 143 additions and 103 deletions

View File

@ -67,6 +67,7 @@
class="tiptap__editor" class="tiptap__editor"
:class="{'tiptap__editor-is-edit-enabled': isEditing}" :class="{'tiptap__editor-is-edit-enabled': isEditing}"
:editor="editor" :editor="editor"
@dblclick="setEditIfApplicable()"
@click="focusIfEditing()" @click="focusIfEditing()"
/> />
@ -171,7 +172,7 @@ import {OrderedList} from '@tiptap/extension-ordered-list'
import {Paragraph} from '@tiptap/extension-paragraph' import {Paragraph} from '@tiptap/extension-paragraph'
import {Strike} from '@tiptap/extension-strike' import {Strike} from '@tiptap/extension-strike'
import {Text} from '@tiptap/extension-text' import {Text} from '@tiptap/extension-text'
import {BubbleMenu, EditorContent, useEditor} from '@tiptap/vue-3' import {BubbleMenu, EditorContent, type Extensions, useEditor} from '@tiptap/vue-3'
import {Node} from '@tiptap/pm/model' import {Node} from '@tiptap/pm/model'
import Commands from './commands' import Commands from './commands'
@ -189,7 +190,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
import XButton from '@/components/input/button.vue' import XButton from '@/components/input/button.vue'
import {Placeholder} from '@tiptap/extension-placeholder' import {Placeholder} from '@tiptap/extension-placeholder'
import {eventToHotkeyString} from '@github/hotkey' import {eventToHotkeyString} from '@github/hotkey'
import {mergeAttributes} from '@tiptap/core' import {Extension, mergeAttributes} from '@tiptap/core'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty' import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt' import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor' import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
@ -202,6 +203,7 @@ const {
showSave = false, showSave = false,
placeholder = '', placeholder = '',
editShortcut = '', editShortcut = '',
enableDiscardShortcut = false,
} = defineProps<{ } = defineProps<{
modelValue: string, modelValue: string,
uploadCallback?: UploadCallback, uploadCallback?: UploadCallback,
@ -210,6 +212,7 @@ const {
showSave?: boolean, showSave?: boolean,
placeholder?: string, placeholder?: string,
editShortcut?: string, editShortcut?: string,
enableDiscardShortcut?: boolean,
}>() }>()
const emit = defineEmits(['update:modelValue', 'save']) const emit = defineEmits(['update:modelValue', 'save'])
@ -311,6 +314,8 @@ const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled) const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const contentHasChanged = ref<boolean>(false) const contentHasChanged = ref<boolean>(false)
let lastSavedState = modelValue
watch( watch(
() => internalMode.value, () => internalMode.value,
mode => { mode => {
@ -320,109 +325,127 @@ watch(
}, },
) )
const extensions : Extensions = [
// Starterkit:
Blockquote,
Bold,
BulletList,
Code,
CodeBlockLowlight.configure({
lowlight,
}),
Document,
Dropcursor,
Gapcursor,
HardBreak.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.commands.setHardBreak(),
'Mod-Enter': () => {
if (contentHasChanged.value) {
bubbleSave()
}
return true
},
}
},
}),
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
// Custom TableCell with backgroundColor attribute
CustomTableCell,
CustomImage,
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
return false
}
// The following is a workaround for this bug:
// https://github.com/ueberdosis/tiptap/issues/4521
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
})
return true
},
}),
Commands.configure({
suggestion: suggestionSetup(t),
}),
BubbleMenu,
]
// Add a custom extension for the Escape key
if (enableDiscardShortcut) {
extensions.push(Extension.create({
name: 'escapeKey',
addKeyboardShortcuts() {
return {
'Escape': () => {
exitEditMode()
return true
},
}
},
}))
}
const editor = useEditor({ const editor = useEditor({
// eslint-disable-next-line vue/no-ref-object-destructure // eslint-disable-next-line vue/no-ref-object-destructure
editable: isEditing.value, editable: isEditing.value,
extensions: [ extensions: extensions,
// Starterkit:
Blockquote,
Bold,
BulletList,
Code,
CodeBlockLowlight.configure({
lowlight,
}),
Document,
Dropcursor,
Gapcursor,
HardBreak.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.commands.setHardBreak(),
'Mod-Enter': () => {
if (contentHasChanged.value) {
bubbleSave()
}
return true
},
}
},
}),
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
// Custom TableCell with backgroundColor attribute
CustomTableCell,
CustomImage,
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
return false
}
// The following is a workaround for this bug:
// https://github.com/ueberdosis/tiptap/issues/4521
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
})
return true
},
}),
Commands.configure({
suggestion: suggestionSetup(t),
}),
BubbleMenu,
],
onUpdate: () => { onUpdate: () => {
bubbleNow() bubbleNow()
}, },
@ -461,12 +484,27 @@ function bubbleNow() {
function bubbleSave() { function bubbleSave() {
bubbleNow() bubbleNow()
emit('save', editor.value?.getHTML()) lastSavedState = editor.value?.getHTML() ?? ''
emit('save', lastSavedState)
if (isEditing.value) { if (isEditing.value) {
internalMode.value = 'preview' internalMode.value = 'preview'
} }
} }
function exitEditMode() {
editor.value?.commands.setContent(lastSavedState, false)
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEditIfApplicable() {
if (!isEditEnabled) return
if (isEditing.value) return
setEdit()
}
function setEdit(focus: boolean = true) { function setEdit(focus: boolean = true) {
internalMode.value = 'edit' internalMode.value = 'edit'
if (focus) { if (focus) {

View File

@ -85,6 +85,7 @@
:upload-enabled="true" :upload-enabled="true"
:bottom-actions="actions[c.id]" :bottom-actions="actions[c.id]"
:show-save="true" :show-save="true"
:enable-discard-shortcut="true"
initial-mode="preview" initial-mode="preview"
@update:modelValue=" @update:modelValue="
() => { () => {

View File

@ -30,6 +30,7 @@
:placeholder="$t('task.description.placeholder')" :placeholder="$t('task.description.placeholder')"
:show-save="true" :show-save="true"
edit-shortcut="e" edit-shortcut="e"
:enable-discard-shortcut="true"
@update:modelValue="saveWithDelay" @update:modelValue="saveWithDelay"
@save="save" @save="save"
/> />