Frontend: Description Edit Hotkeys #2265
|
@ -67,6 +67,7 @@
|
|||
class="tiptap__editor"
|
||||
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
|
||||
:editor="editor"
|
||||
@dblclick="setEditIfApplicable()"
|
||||
Elscrux marked this conversation as resolved
Outdated
|
||||
@click="focusIfEditing()"
|
||||
/>
|
||||
|
||||
|
@ -171,7 +172,7 @@ import {OrderedList} from '@tiptap/extension-ordered-list'
|
|||
import {Paragraph} from '@tiptap/extension-paragraph'
|
||||
import {Strike} from '@tiptap/extension-strike'
|
||||
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 Commands from './commands'
|
||||
|
@ -189,7 +190,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
import XButton from '@/components/input/button.vue'
|
||||
import {Placeholder} from '@tiptap/extension-placeholder'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {mergeAttributes} from '@tiptap/core'
|
||||
import {Extension, mergeAttributes} from '@tiptap/core'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
@ -202,6 +203,7 @@ const {
|
|||
showSave = false,
|
||||
placeholder = '',
|
||||
editShortcut = '',
|
||||
discardShortcutEnabled = false,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
uploadCallback?: UploadCallback,
|
||||
|
@ -210,6 +212,7 @@ const {
|
|||
showSave?: boolean,
|
||||
placeholder?: string,
|
||||
editShortcut?: string,
|
||||
discardShortcutEnabled?: boolean,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
@ -311,6 +314,8 @@ const internalMode = ref<Mode>('preview')
|
|||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
const contentHasChanged = ref<boolean>(false)
|
||||
|
||||
let lastSavedState = modelValue
|
||||
|
||||
watch(
|
||||
() => internalMode.value,
|
||||
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 (discardShortcutEnabled) {
|
||||
extensions.push(Extension.create({
|
||||
name: 'escapeKey',
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
konrad
commented
Isn't the extra mention of Isn't the extra mention of `discardShortcutEnabled` in the line before redundant?
Elscrux
commented
Yes Yes `discardShortcutEnabled ` in line 433 is needed, this is part of the ternary operator to include this Extension optionally. Not super fancy but I didn't see a much better option. Let me know if you know a better way.
konrad
commented
I feel like this is kinda unreadable. Can you rewrite it so that it uses a regular I feel like this is kinda unreadable. Can you rewrite it so that it uses a regular `if`?
Elscrux
commented
There was no pretty way to do it, but I've changed it now, I hope it works for you! There was no pretty way to do it, but I've changed it now, I hope it works for you!
konrad
commented
Looks good, thanks! Looks good, thanks!
|
||||
return {
|
||||
'Escape': () => {
|
||||
exitEditMode()
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
// eslint-disable-next-line vue/no-ref-object-destructure
|
||||
editable: isEditing.value,
|
||||
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,
|
||||
],
|
||||
extensions: extensions,
|
||||
onUpdate: () => {
|
||||
bubbleNow()
|
||||
},
|
||||
|
@ -461,12 +484,27 @@ function bubbleNow() {
|
|||
|
||||
function bubbleSave() {
|
||||
bubbleNow()
|
||||
emit('save', editor.value?.getHTML())
|
||||
lastSavedState = editor.value?.getHTML() ?? ''
|
||||
konrad
commented
How does this behave when editing a project description? There the editor is not used as a rendering engine, so it should not change the mode to preview. How does this behave when editing a project description? There the editor is not used as a rendering engine, so it should not change the mode to preview.
Elscrux
commented
Currently it changes it back to the preview mode, which is not what it should be. Currently it changes it back to the preview mode, which is not what it should be.
I'll add an opt-in feature for this, or what do you think should be done for this?
konrad
commented
Iirc there are props already controlling if it will allow rendering or editing, maybe use those? (Not on my pc right now, so I'm unable to check this in detail) Iirc there are props already controlling if it will allow rendering or editing, maybe use those? (Not on my pc right now, so I'm unable to check this in detail)
Elscrux
commented
There is There is `isEditEnabled`, but it is enabled by default for something like editing the project description as well.
I see that `showSave` is only used by the two editors, for comments and task description which should get this feature. So functionally the discard feature could be baked into this, but I'm not sure if that makes a lot of sense.
konrad
commented
Good point. Let's add a new prop Good point. Let's add a new prop `previewAfterExit` or something similar and then refactor it later.
|
||||
emit('save', lastSavedState)
|
||||
if (isEditing.value) {
|
||||
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) {
|
||||
internalMode.value = 'edit'
|
||||
if (focus) {
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
:upload-enabled="true"
|
||||
:bottom-actions="actions[c.id]"
|
||||
:show-save="true"
|
||||
:discard-shortcut-enabled="true"
|
||||
initial-mode="preview"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
:placeholder="$t('task.description.placeholder')"
|
||||
:show-save="true"
|
||||
edit-shortcut="e"
|
||||
:discard-shortcut-enabled="true"
|
||||
@update:modelValue="saveWithDelay"
|
||||
@save="save"
|
||||
/>
|
||||
|
|
Where is this event defined?
It seems to be a native event, just like @click