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

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

@ -158,23 +158,37 @@ const openLink = () => {
window.open(href.value, '_blank', 'noopener,noreferrer')
}
}
const onMountLinkOptions = (e) => {
if (e?.popper?.style) {
e.popper.style.width = '95%'
}
}
</script>
<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
v-if="!justDeleted"
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"
@keydown.stop="handleKeyDown"
>
<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
ref="inputRef"
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"
placeholder="Enter a link"
@change="onChange"

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

@ -210,8 +210,14 @@ watch(inputWrapperRef, () => {
'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
v-if="isRichMode"
v-else-if="isRichMode"
class="w-full cursor-pointer nc-readonly-rich-text-wrapper"
:class="{
'nc-readonly-rich-text-grid ': !isExpandedFormOpen && !isForm,
@ -271,7 +277,7 @@ watch(inputWrapperRef, () => {
<span v-else>{{ vModel }}</span>
<NcTooltip
v-if="!isVisible"
v-if="!isVisible && !isForm"
placement="bottom"
class="!absolute top-1 hidden nc-text-area-expand-btn group-hover:block z-3"
: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',
}
const { user } = useGlobal()
const { isMobileMode, user } = useGlobal()
const { $api, $e } = useNuxtApp()
@ -1026,7 +1026,8 @@ useEventListener(
class="border-transparent px-4 lg:px-6"
: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,
@ -1062,6 +1063,8 @@ useEventListener(
:data-testid="NcForm.heading"
:data-title="NcForm.heading"
@update:value="updateView"
@focus="activeRow = NcForm.heading"
@blur="activeRow = ''"
/>
</a-form-item>
@ -1075,7 +1078,7 @@ useEventListener(
class="border-transparent px-4 lg:px-6"
: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,
@ -1102,6 +1105,8 @@ useEventListener(
:data-testid="NcForm.subheading"
:data-title="NcForm.subheading"
@update:value="updateView"
@focus="activeRow = NcForm.subheading"
@blur="activeRow = ''"
/>
<LazyCellRichText
v-else-if="formViewData.subheading"
@ -1984,13 +1989,6 @@ useEventListener(
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 lang="scss">

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

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

Loading…
Cancel
Save