Browse Source

Nc Feat: Allow inline edit rich text field in form view (#7974)

* feat(nc-gui): allow inline edit rich text field in form view setup

* fix(nc-gui): rich text link options width issue

* fix(nc-gui): form view title, description on focus bg color issue

* fix(nc-gui): form view rich text field shift tab focus out issue

* fix(nc-gui): set max height of rich text field in form view to 240px

* fix(nc-gui): rich text full mode options visibility issue

* chore(nc-gui): lint
pull/7976/head
Ramesh Mane 8 months ago committed by GitHub
parent
commit
779db0104b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 47
      packages/nc-gui/components/cell/RichText.vue
  2. 22
      packages/nc-gui/components/cell/RichText/LinkOptions.vue
  3. 10
      packages/nc-gui/components/cell/TextArea.vue
  4. 18
      packages/nc-gui/components/smartsheet/Form.vue
  5. 18
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

47
packages/nc-gui/components/cell/RichText.vue

@ -10,7 +10,7 @@ import Placeholder from '@tiptap/extension-placeholder'
import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item' import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links' import { Link } from '@/helpers/dbTiptapExtensions/links'
import type { RichTextBubbleMenuOptions } from '#imports' import type { RichTextBubbleMenuOptions } from '#imports'
import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, ReadonlyInj, RowHeightInj } from '#imports' import { IsExpandedFormOpenInj, IsFormInj, IsGridInj, IsSurveyFormInj, ReadonlyInj, RowHeightInj } from '#imports'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -26,13 +26,14 @@ const props = withDefaults(
hiddenBubbleMenuOptions?: RichTextBubbleMenuOptions[] hiddenBubbleMenuOptions?: RichTextBubbleMenuOptions[]
}>(), }>(),
{ {
isFormField: false,
hiddenBubbleMenuOptions: () => [], hiddenBubbleMenuOptions: () => [],
}, },
) )
const emits = defineEmits(['update:value']) const emits = defineEmits(['update:value', 'focus', 'blur'])
const { hiddenBubbleMenuOptions } = toRefs(props) const { isFormField, hiddenBubbleMenuOptions } = toRefs(props)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))! const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
@ -44,8 +45,12 @@ const isForm = inject(IsFormInj, ref(false))
const isGrid = inject(IsGridInj, ref(false)) const isGrid = inject(IsGridInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isFocused = ref(false) const isFocused = ref(false)
const keys = useMagicKeys()
const turndownService = new TurndownService({}) const turndownService = new TurndownService({})
turndownService.addRule('lineBreak', { turndownService.addRule('lineBreak', {
@ -124,7 +129,7 @@ const vModel = useVModel(props, 'value', emits, { defaultValue: '' })
const tiptapExtensions = [ const tiptapExtensions = [
StarterKit.configure({ StarterKit.configure({
heading: props.isFormField ? false : undefined, heading: isFormField.value ? false : undefined,
}), }),
TaskList, TaskList,
TaskItem.configure({ TaskItem.configure({
@ -145,16 +150,18 @@ const editor = useEditor({
.turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />')) .turndown(editor.getHTML().replaceAll(/<p><\/p>/g, '<br />'))
.replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n') .replaceAll(/\n\n<br \/>\n\n/g, '<br>\n\n')
vModel.value = props.isFormField && markdown === '<br />' ? '' : markdown vModel.value = isFormField.value && markdown === '<br />' ? '' : markdown
}, },
editable: !props.readOnly, editable: !props.readOnly,
autofocus: props.autofocus, autofocus: props.autofocus,
onFocus: () => { onFocus: () => {
isFocused.value = true isFocused.value = true
emits('focus')
}, },
onBlur: (e) => { onBlur: (e) => {
if (!(e?.event?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) { if (!(e?.event?.relatedTarget as HTMLElement)?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false isFocused.value = false
emits('blur')
} }
}, },
}) })
@ -185,13 +192,19 @@ const setEditorContent = (contentMd: any, focusEndOfDoc?: boolean) => {
}, 100) }, 100)
} }
const onFocusWrapper = () => {
if (isForm.value && !isFormField.value && !props.readOnly && !keys.shift.value) {
editor.value?.chain().focus().run()
}
}
if (props.syncValueChange) { if (props.syncValueChange) {
watch([vModel, editor], () => { watch([vModel, editor], () => {
setEditorContent(vModel.value) setEditorContent(vModel.value)
}) })
} }
if (props.isFormField) { if (isFormField.value) {
watch([props, editor], () => { watch([props, editor], () => {
if (props.readOnly) { if (props.readOnly) {
editor.value?.setEditable(false) editor.value?.setEditable(false)
@ -206,7 +219,7 @@ watch(editorDom, () => {
setEditorContent(vModel.value, true) setEditorContent(vModel.value, true)
if (props.isFormField) return if ((isForm.value && !isSurveyForm.value) || isFormField.value) return
// Focus editor after editor is mounted // Focus editor after editor is mounted
setTimeout(() => { setTimeout(() => {
editor.value?.chain().focus().run() editor.value?.chain().focus().run()
@ -220,6 +233,7 @@ useEventListener(
const targetEl = e?.relatedTarget as HTMLElement const targetEl = e?.relatedTarget as HTMLElement
if (targetEl?.classList?.contains('tiptap') || !targetEl?.closest('.bubble-menu, .nc-textarea-rich-editor')) { if (targetEl?.classList?.contains('tiptap') || !targetEl?.closest('.bubble-menu, .nc-textarea-rich-editor')) {
isFocused.value = false isFocused.value = false
emits('blur')
} }
}, },
true, true,
@ -228,7 +242,7 @@ useEventListener(
<template> <template>
<div <div
class="h-full focus:outline-none" class="nc-rich-text h-full focus:outline-none"
:class="{ :class="{
'flex flex-col flex-grow nc-rich-text-full': fullMode, 'flex flex-col flex-grow nc-rich-text-full': fullMode,
'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode, 'nc-rich-text-embed flex flex-col pl-1 w-full': !fullMode,
@ -237,6 +251,7 @@ useEventListener(
'nc-rich-text-grid': isGrid, 'nc-rich-text-grid': isGrid,
}" }"
:tabindex="readOnlyCell || isFormField ? -1 : 0" :tabindex="readOnlyCell || isFormField ? -1 : 0"
@focus="onFocusWrapper"
> >
<div v-if="renderAsText" class="truncate"> <div v-if="renderAsText" class="truncate">
<span v-if="editor"> {{ editor?.getText() ?? '' }}</span> <span v-if="editor"> {{ editor?.getText() ?? '' }}</span>
@ -244,17 +259,22 @@ useEventListener(
<template v-else> <template v-else>
<div <div
v-if="showMenu && !readOnly && !isFormField" v-if="showMenu && !readOnly && !isFormField"
class="absolute top-0 right-0.5 xs:hidden" class="absolute top-0 right-0.5"
:class="{ :class="{
'max-w-[calc(100%_-_198px)] flex justify-end rounded-tr-2xl overflow-hidden': fullMode, 'flex rounded-tr-2xl overflow-hidden w-full': fullMode || isForm,
'max-w-[calc(100%_-_198px)]': fullMode,
'justify-start left-0.5': isForm,
'justify-end xs:hidden': !isForm,
}" }"
> >
<div class="nc-longtext-scrollbar"> <div class="scrollbar-thin scrollbar-thumb-gray-200 hover:scrollbar-thumb-gray-300 scrollbar-track-transparent">
<CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode :is-form-field="isFormField" /> <CellRichTextSelectedBubbleMenu v-if="editor" :editor="editor" embed-mode :is-form-field="isFormField" />
</div> </div>
</div> </div>
<CellRichTextSelectedBubbleMenuPopup v-if="editor && !isFormField" :editor="editor" /> <CellRichTextSelectedBubbleMenuPopup v-if="editor && !isFormField && !isForm" :editor="editor" />
<CellRichTextLinkOptions v-if="editor" :editor="editor" /> <CellRichTextLinkOptions v-if="editor" :editor="editor" />
<EditorContent <EditorContent
ref="editorDom" ref="editorDom"
:editor="editor" :editor="editor"
@ -444,18 +464,21 @@ useEventListener(
font-weight: 700; font-weight: 700;
font-size: 1.85rem; font-size: 1.85rem;
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
line-height: 36px;
} }
h2 { h2 {
font-weight: 600; font-weight: 600;
font-size: 1.55rem; font-size: 1.55rem;
margin-bottom: 0.1em; margin-bottom: 0.1em;
line-height: 30px;
} }
h3 { h3 {
font-weight: 600; font-weight: 600;
font-size: 1.15rem; font-size: 1.15rem;
margin-bottom: 0.1em; margin-bottom: 0.1em;
line-height: 24px;
} }
blockquote { blockquote {

22
packages/nc-gui/components/cell/RichText/LinkOptions.vue

@ -158,23 +158,37 @@ const openLink = () => {
window.open(href.value, '_blank', 'noopener,noreferrer') window.open(href.value, '_blank', 'noopener,noreferrer')
} }
} }
const onMountLinkOptions = (e) => {
if (e?.popper?.style) {
e.popper.style.width = '95%'
}
}
</script> </script>
<template> <template>
<BubbleMenu :editor="editor" :tippy-options="{ duration: 100, maxWidth: 600 }" :should-show="(checkLinkMark as any)"> <BubbleMenu
:editor="editor"
:tippy-options="{
duration: 100,
maxWidth: 600,
onMount: onMountLinkOptions,
}"
:should-show="(checkLinkMark as any)"
>
<div <div
v-if="!justDeleted" v-if="!justDeleted"
ref="wrapperRef" ref="wrapperRef"
class="relative bubble-menu nc-text-area-rich-link-options flex flex-col bg-gray-50 py-1 px-1 rounded-lg" class="relative bubble-menu nc-text-area-rich-link-options flex flex-col bg-gray-50 py-1 px-1 rounded-lg w-full"
data-testid="nc-text-area-rich-link-options" data-testid="nc-text-area-rich-link-options"
@keydown.stop="handleKeyDown" @keydown.stop="handleKeyDown"
> >
<div class="flex items-center gap-x-1"> <div class="flex items-center gap-x-1">
<div class="!border-1 !border-gray-200 !py-0.5 bg-gray-100 rounded-md !z-10"> <div class="!border-1 !border-gray-200 !py-0.5 bg-gray-100 rounded-md !z-10 flex-1">
<a-input <a-input
ref="inputRef" ref="inputRef"
v-model:value="href" v-model:value="href"
class="nc-text-area-rich-link-option-input flex-1 !w-96 !mx-0.5 !px-1.5 !py-0.5 !rounded-md z-10" class="nc-text-area-rich-link-option-input flex-1 !mx-0.5 !px-1.5 !py-0.5 !rounded-md z-10"
:bordered="false" :bordered="false"
placeholder="Enter a link" placeholder="Enter a link"
@change="onChange" @change="onChange"

10
packages/nc-gui/components/cell/TextArea.vue

@ -210,8 +210,14 @@ watch(inputWrapperRef, () => {
'h-full w-full': isForm, 'h-full w-full': isForm,
}" }"
> >
<div v-if="isForm && isRichMode" class="w-full">
<div class="w-full relative pt-11 w-full px-0 pb-1">
<LazyCellRichText v-model:value="vModel" class="border-t-1 border-gray-100 !max-h-50" :autofocus="false" show-menu />
</div>
</div>
<div <div
v-if="isRichMode" v-else-if="isRichMode"
class="w-full cursor-pointer nc-readonly-rich-text-wrapper" class="w-full cursor-pointer nc-readonly-rich-text-wrapper"
:class="{ :class="{
'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm, 'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm,
@ -271,7 +277,7 @@ watch(inputWrapperRef, () => {
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
<NcTooltip <NcTooltip
v-if="!isVisible" v-if="!isVisible && !isForm"
placement="bottom" placement="bottom"
class="!absolute top-1 hidden nc-text-area-expand-btn group-hover:block z-3" class="!absolute top-1 hidden nc-text-area-expand-btn group-hover:block z-3"
:class="isForm ? 'right-1' : 'right-0'" :class="isForm ? 'right-1' : 'right-0'"

18
packages/nc-gui/components/smartsheet/Form.vue

@ -80,7 +80,7 @@ const enum NcForm {
subheading = 'nc-form-sub-heading', subheading = 'nc-form-sub-heading',
} }
const { user } = useGlobal() const { isMobileMode, user } = useGlobal()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -1026,7 +1026,8 @@ useEventListener(
class="border-transparent px-4 lg:px-6" class="border-transparent px-4 lg:px-6"
:class="[ :class="[
{ {
'rounded-2xl overflow-hidden border-2 cursor-pointer mb-1 py-4 lg:py-6': isEditable, 'rounded-2xl overflow-hidden border-2 cursor-pointer mb-1 py-4 lg:py-6 focus-within:bg-gray-50':
isEditable,
}, },
{ {
'mb-4 py-0 lg:py-0': !isEditable, 'mb-4 py-0 lg:py-0': !isEditable,
@ -1062,6 +1063,8 @@ useEventListener(
:data-testid="NcForm.heading" :data-testid="NcForm.heading"
:data-title="NcForm.heading" :data-title="NcForm.heading"
@update:value="updateView" @update:value="updateView"
@focus="activeRow = NcForm.heading"
@blur="activeRow = ''"
/> />
</a-form-item> </a-form-item>
@ -1075,7 +1078,7 @@ useEventListener(
class="border-transparent px-4 lg:px-6" class="border-transparent px-4 lg:px-6"
:class="[ :class="[
{ {
'rounded-2xl border-2 cursor-pointer mb-1 py-4 lg:py-6': isEditable, 'rounded-2xl border-2 cursor-pointer mb-1 py-4 lg:py-6 focus-within:bg-gray-50': isEditable,
}, },
{ {
'mb-4 py-0 lg:py-0': !isEditable, 'mb-4 py-0 lg:py-0': !isEditable,
@ -1102,6 +1105,8 @@ useEventListener(
:data-testid="NcForm.subheading" :data-testid="NcForm.subheading"
:data-title="NcForm.subheading" :data-title="NcForm.subheading"
@update:value="updateView" @update:value="updateView"
@focus="activeRow = NcForm.subheading"
@blur="activeRow = ''"
/> />
<LazyCellRichText <LazyCellRichText
v-else-if="formViewData.subheading" v-else-if="formViewData.subheading"
@ -1984,13 +1989,6 @@ useEventListener(
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff; box-shadow: 0 0 0 2px #fff, 0 0 0 4px #3366ff;
} }
} }
.nc-form-right-panel {
:deep(.nc-form-rich-text-field) {
.nc-text-area-rich-link-option-input {
@apply !w-250px;
}
}
}
</style> </style>
<style lang="scss"> <style lang="scss">

18
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -121,10 +121,17 @@ p {
} }
&:not(.readonly) { &:not(.readonly) {
input, &:not(.nc-cell-longtext) {
textarea, input,
&.nc-virtual-cell { textarea,
@apply bg-white !disabled:bg-transparent; &.nc-virtual-cell {
@apply bg-white !disabled:bg-transparent;
}
}
&.nc-cell-longtext {
textarea {
@apply bg-white !disabled:bg-transparent;
}
} }
} }
@ -188,6 +195,9 @@ p {
@apply !bg-gray-100; @apply !bg-gray-100;
} }
} }
&.nc-cell-attachment {
@apply h-auto;
}
} }
} }

Loading…
Cancel
Save