diff --git a/packages/nc-gui/app.vue b/packages/nc-gui/app.vue index bf4941ab0c..13e8934fd4 100644 --- a/packages/nc-gui/app.vue +++ b/packages/nc-gui/app.vue @@ -1,6 +1,5 @@ diff --git a/packages/nc-gui/assets/nc-icons/link.svg b/packages/nc-gui/assets/nc-icons/link.svg index 1207f4b97f..383121b0eb 100644 --- a/packages/nc-gui/assets/nc-icons/link.svg +++ b/packages/nc-gui/assets/nc-icons/link.svg @@ -1,4 +1,8 @@ - - - + + + \ No newline at end of file diff --git a/packages/nc-gui/components/cell/RichText.vue b/packages/nc-gui/components/cell/RichText.vue index b263004f00..bb21d91cc8 100644 --- a/packages/nc-gui/components/cell/RichText.vue +++ b/packages/nc-gui/components/cell/RichText.vue @@ -6,6 +6,7 @@ import TurndownService from 'turndown' import { marked } from 'marked' import { generateJSON } from '@tiptap/html' import Underline from '@tiptap/extension-underline' +import Placeholder from '@tiptap/extension-placeholder' import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item' import { Link } from '@/helpers/dbTiptapExtensions/links' import { IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, RowHeightInj } from '#imports' @@ -16,6 +17,10 @@ const props = defineProps<{ syncValueChange?: boolean showMenu?: boolean fullMode?: boolean + isFormField?: boolean + autofocus?: boolean + placeholder?: string + renderAsText?: boolean }>() const emits = defineEmits(['update:value']) @@ -28,6 +33,8 @@ const readOnlyCell = inject(ReadonlyInj, ref(false)) const isForm = inject(IsFormInj, ref(false)) +const isFocused = ref(false) + const turndownService = new TurndownService({}) turndownService.addRule('lineBreak', { @@ -105,13 +112,19 @@ const editorDom = ref(null) const vModel = useVModel(props, 'value', emits, { defaultValue: '' }) const tiptapExtensions = [ - StarterKit, + StarterKit.configure({ + heading: props.isFormField ? false : undefined, + }), TaskList, TaskItem.configure({ nested: true, }), Underline, Link, + Placeholder.configure({ + emptyEditorClass: 'is-editor-empty', + placeholder: props.placeholder, + }), ] const editor = useEditor({ @@ -121,9 +134,18 @@ const editor = useEditor({ .turndown(editor.getHTML().replaceAll(/

<\/p>/g, '
')) .replaceAll(/\n\n
\n\n/g, '
\n\n') - vModel.value = markdown + vModel.value = props.isFormField && markdown === '
' ? '' : markdown }, editable: !props.readOnly, + autofocus: props.autofocus, + onFocus: () => { + isFocused.value = true + }, + onBlur: (e) => { + if (!(e?.event?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) { + isFocused.value = false + } + }, }) const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => { @@ -153,21 +175,43 @@ const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => { } if (props.syncValueChange) { - watch(vModel, () => { + watch([vModel, editor], () => { setEditorContent(vModel.value) }) } +if (props.isFormField) { + watch([props, editor], () => { + if (props.readOnly) { + editor.value?.setEditable(false) + } else { + editor.value?.setEditable(true) + } + }) +} + watch(editorDom, () => { if (!editorDom.value) return setEditorContent(vModel.value, true) + if (props.isFormField) return // Focus editor after editor is mounted setTimeout(() => { editor.value?.chain().focus().run() }, 50) }) + +useEventListener( + editorDom, + 'focusout', + (e: FocusEvent) => { + if (!(e?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) { + isFocused.value = false + } + }, + true, +) @@ -223,7 +284,28 @@ watch(editorDom, () => { .nc-rich-text-embed { .ProseMirror { @apply !border-transparent max-h-full; - min-height: 8rem; + } + &:not(.nc-form-rich-text-field) { + .ProseMirror { + min-height: 8rem; + } + } + &.nc-form-rich-text-field { + .ProseMirror { + padding: 0; + } + &.readonly { + ul[data-type='taskList'] li input[type='checkbox'] { + background-color: #d5d5d9 !important; + &:not(:checked) { + @apply !border-gray-400; + } + &:focus { + box-shadow: none !important; + background-color: #d5d5d9 !important; + } + } + } } &.readonly { .nc-textarea-rich-editor { @@ -256,6 +338,13 @@ watch(editorDom, () => { } .nc-textarea-rich-editor { + .tiptap p.is-editor-empty:first-child::before { + color: #6a7184; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } .ProseMirror { @apply flex-grow pt-1 border-1 border-gray-200 rounded-lg; diff --git a/packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue b/packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue index 51b939b264..ed5048d9fd 100644 --- a/packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue +++ b/packages/nc-gui/components/cell/RichText/SelectedBubbleMenu.vue @@ -14,13 +14,12 @@ import MsFormatQuote from '~icons/material-symbols/format-quote' interface Props { editor: Editor embedMode?: boolean + isFormField?: boolean } const props = defineProps() -const editor = computed(() => props.editor) - -const embedMode = computed(() => props.embedMode) +const { editor, embedMode } = toRefs(props) const cmdOrCtrlKey = computed(() => { return isMac() ? '⌘' : 'CTRL' @@ -79,13 +78,15 @@ const onToggleLink = () => {