mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 05:37:00 +00:00
387 lines
10 KiB
Vue
387 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { defineAsyncComponent } from 'vue'
|
|
import NcModal from '../nc/Modal.vue'
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emits = defineEmits<Emits>()
|
|
|
|
// Define Monaco Editor as an async component
|
|
const MonacoEditor = defineAsyncComponent(() => import('~/components/monaco/Editor.vue'))
|
|
|
|
type ModelValueType = string | Record<string, any> | undefined | null
|
|
|
|
interface Props {
|
|
modelValue: ModelValueType
|
|
}
|
|
|
|
interface Emits {
|
|
(event: 'update:modelValue', model: string | null): void
|
|
}
|
|
|
|
const { showNull } = useGlobal()
|
|
|
|
const editEnabled = inject(EditModeInj, ref(false))
|
|
|
|
const active = inject(ActiveCellInj, ref(false))
|
|
|
|
const cellEventHook = inject(CellEventHookInj, null)
|
|
|
|
const canvasCellEventData = inject(CanvasCellEventDataInj, reactive<CanvasCellEventDataInjType>({}))
|
|
|
|
const canvasSelectCell = inject(CanvasSelectCellInj, null)
|
|
|
|
const isEditColumn = inject(EditColumnInj, ref(false))
|
|
|
|
const isForm = inject(IsFormInj, ref(false))
|
|
|
|
const readOnly = inject(ReadonlyInj, ref(false))
|
|
|
|
const vModel = useVModel(props, 'modelValue', emits)
|
|
|
|
const localValueState = ref<string | undefined | null>()
|
|
|
|
const error = ref<string | undefined>()
|
|
|
|
const _isExpanded = inject(JsonExpandInj, ref(false))
|
|
|
|
const isExpanded = ref(false)
|
|
|
|
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
|
|
|
|
const rowHeight = inject(RowHeightInj, ref(undefined))
|
|
|
|
const formatValue = (val: ModelValueType) => {
|
|
return val ?? null
|
|
}
|
|
|
|
const localValue = computed<ModelValueType>({
|
|
get: () => localValueState.value,
|
|
set: (val: ModelValueType) => {
|
|
localValueState.value = formatValue(val) === null ? null : typeof val === 'object' ? JSON.stringify(val, null, 2) : val
|
|
/** if form and not expanded then sync directly */
|
|
if (isForm.value && !isExpanded.value) {
|
|
vModel.value = formatValue(val) === null ? null : val
|
|
}
|
|
},
|
|
})
|
|
|
|
function openJSONEditor(e?: Event) {
|
|
const target = e?.target as HTMLElement
|
|
if (target?.classList?.contains('default-value-clear')) return
|
|
isExpanded.value = true
|
|
}
|
|
|
|
function closeJSONEditor() {
|
|
isExpanded.value = false
|
|
}
|
|
|
|
const formatJson = (json: string) => {
|
|
try {
|
|
return JSON.stringify(JSON.parse(json))
|
|
} catch (e) {
|
|
console.log(e)
|
|
return json
|
|
}
|
|
}
|
|
|
|
function setLocalValue(val: any) {
|
|
try {
|
|
localValue.value = formatValue(val) === null ? null : typeof val === 'string' ? JSON.stringify(JSON.parse(val), null, 2) : val
|
|
} catch (e) {
|
|
localValue.value = formatValue(val) === null ? null : val
|
|
}
|
|
}
|
|
|
|
const clear = () => {
|
|
error.value = undefined
|
|
|
|
closeJSONEditor()
|
|
|
|
editEnabled.value = false
|
|
|
|
setLocalValue(vModel.value)
|
|
}
|
|
|
|
const onSave = () => {
|
|
closeJSONEditor()
|
|
|
|
editEnabled.value = false
|
|
|
|
// avoid saving if error exists or value is same as previous
|
|
if (error.value || localValue.value === vModel.value) return false
|
|
|
|
vModel.value = formatValue(localValue.value) === null ? null : formatJson(localValue.value as string)
|
|
}
|
|
|
|
watch(
|
|
vModel,
|
|
(val) => {
|
|
setLocalValue(val)
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch([localValue, editEnabled], () => {
|
|
try {
|
|
JSON.parse(localValue.value as string)
|
|
|
|
error.value = undefined
|
|
} catch (e: any) {
|
|
if (localValue.value === undefined || localValue.value === null) return
|
|
|
|
error.value = e
|
|
}
|
|
})
|
|
|
|
watch(editEnabled, () => {
|
|
closeJSONEditor()
|
|
|
|
setLocalValue(vModel.value)
|
|
})
|
|
|
|
useSelectedCellKeydownListener(active, (e) => {
|
|
if (readOnly.value) return
|
|
switch (true) {
|
|
case e.key === 'Enter':
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
if (e.shiftKey) {
|
|
return true
|
|
}
|
|
openJSONEditor()
|
|
break
|
|
case e.metaKey:
|
|
case e.altKey:
|
|
case e.ctrlKey:
|
|
case e.key === 'Backspace':
|
|
case e.key === 'Spacebar' || e.key === ' ':
|
|
case [...e.key].length > 1:
|
|
// The string iterator that is used here iterates over characters, not mere code units
|
|
// If a key is a modifier key or navigation key or function key or any of the
|
|
// non-printing keys, ignore them
|
|
break
|
|
default:
|
|
// Otherwise it's a printing character, append it and open the JSON modal for editing
|
|
if (typeof localValue.value === 'string') {
|
|
localValue.value += e.key
|
|
} else if (!localValue.value) {
|
|
localValue.value = e.key
|
|
}
|
|
openJSONEditor()
|
|
}
|
|
})
|
|
|
|
const inputWrapperRef = ref<HTMLElement | null>(null)
|
|
|
|
onClickOutside(inputWrapperRef, (e) => {
|
|
if ((e.target as HTMLElement)?.closest('.nc-json-action')) return
|
|
editEnabled.value = false
|
|
})
|
|
|
|
watch(isExpanded, (newVal, oldVal) => {
|
|
_isExpanded.value = isExpanded.value
|
|
|
|
if (oldVal && !newVal) canvasSelectCell?.trigger()
|
|
})
|
|
|
|
const stopPropagation = (event: MouseEvent) => {
|
|
event.stopPropagation()
|
|
}
|
|
|
|
const listners: Array<'click' | 'mousedown' | 'mouseup'> = ['click', 'mousedown', 'mouseup']
|
|
|
|
const addListeners = (element: HTMLDivElement) => {
|
|
listners.forEach((listener) => {
|
|
element.addEventListener(listener, stopPropagation)
|
|
})
|
|
}
|
|
|
|
const removeListeners = (element: HTMLDivElement) => {
|
|
listners.forEach((listener) => {
|
|
element.removeEventListener(listener, stopPropagation)
|
|
})
|
|
}
|
|
|
|
watch(inputWrapperRef, () => {
|
|
if (!isEditColumn.value) return
|
|
|
|
// stop event propogation in edit to prevent close edit modal on clicking expanded modal overlay
|
|
const modal = document.querySelector('.nc-json-expanded-modal') as HTMLElement
|
|
|
|
if (!modal?.parentElement) return
|
|
|
|
removeListeners(modal.parentElement as HTMLDivElement)
|
|
|
|
if (isExpanded.value) {
|
|
addListeners(modal.parentElement as HTMLDivElement)
|
|
}
|
|
})
|
|
|
|
const onCellEvent = (event?: Event) => {
|
|
if (!(event instanceof KeyboardEvent) || !event.target || isActiveInputElementExist(event)) return
|
|
|
|
if (isExpandCellKey(event)) {
|
|
if (isExpanded.value) {
|
|
closeJSONEditor()
|
|
} else {
|
|
openJSONEditor()
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
const el = useCurrentElement()
|
|
const isCanvasInjected = inject(IsCanvasInjectionInj, false)
|
|
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
|
|
|
|
onMounted(() => {
|
|
cellEventHook?.on(onCellEvent)
|
|
|
|
if (
|
|
!isUnderLookup.value &&
|
|
isCanvasInjected &&
|
|
!isExpanded.value &&
|
|
!isEditColumn.value &&
|
|
!isForm.value &&
|
|
!isExpandedFormOpen.value
|
|
) {
|
|
forcedNextTick(() => {
|
|
if (onCellEvent(canvasCellEventData.event)) return
|
|
|
|
openJSONEditor()
|
|
})
|
|
}
|
|
|
|
const gridCell = el.value?.closest('td')
|
|
if (gridCell && !readOnly.value) {
|
|
gridCell.addEventListener('dblclick', openJSONEditor)
|
|
return
|
|
}
|
|
const container = el.value?.closest('.nc-data-cell, .nc-default-value-wrapper')
|
|
if (container) container.addEventListener('click', openJSONEditor)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
cellEventHook?.off(onCellEvent)
|
|
|
|
const gridCell = el.value?.closest?.('td')
|
|
if (gridCell && !readOnly.value) {
|
|
gridCell.removeEventListener('dblclick', openJSONEditor)
|
|
return
|
|
}
|
|
const container = el.value?.closest?.('.nc-data-cell, .nc-default-value-wrapper')
|
|
if (container) container.removeEventListener('click', openJSONEditor)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<component
|
|
:is="isExpanded ? NcModal : 'div'"
|
|
v-model:visible="isExpanded"
|
|
width="auto"
|
|
:closable="false"
|
|
centered
|
|
:footer="null"
|
|
:wrap-class-name="isExpanded ? '!z-1051 nc-json-expanded-modal' : null"
|
|
class="relative"
|
|
:class="{ 'json-modal min-w-80': isExpanded, 'min-h-6 flex items-center': !isExpanded }"
|
|
>
|
|
<div v-if="isExpanded" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
|
|
<div class="flex flex-row justify-between items-center -mt-2 pb-3 nc-json-action" @mousedown.stop>
|
|
<NcButton type="secondary" size="xsmall" class="!w-7 !h-7 !min-w-[fit-content]" @click.stop="closeJSONEditor">
|
|
<component :is="iconMap.minimize" class="w-4 h-4" />
|
|
</NcButton>
|
|
|
|
<div v-if="!readOnly" class="flex gap-2">
|
|
<NcButton type="secondary" size="small" @click="clear">{{ $t('general.cancel') }}</NcButton>
|
|
<NcButton type="primary" size="small" :disabled="!!error || localValue === vModel" @click="onSave">
|
|
{{ $t('general.save') }}
|
|
</NcButton>
|
|
</div>
|
|
<div v-else></div>
|
|
</div>
|
|
|
|
<Suspense>
|
|
<template #default>
|
|
<MonacoEditor
|
|
ref="inputWrapperRef"
|
|
:model-value="localValue ?? null"
|
|
class="min-w-full w-[40rem] resize overflow-auto expanded-editor"
|
|
:hide-minimap="true"
|
|
:disable-deep-compare="true"
|
|
:auto-focus="true"
|
|
:read-only="readOnly"
|
|
:monaco-config="{
|
|
wordWrap: 'on',
|
|
wrappingStrategy: 'advanced',
|
|
}"
|
|
@update:model-value="localValue = $event"
|
|
@keydown.enter.stop
|
|
@keydown.alt.stop
|
|
/>
|
|
</template>
|
|
<template #fallback>
|
|
<MonacoLoading class="min-w-full w-[40rem] expanded-editor" />
|
|
</template>
|
|
</Suspense>
|
|
|
|
<span v-if="error" class="nc-cell-field text-xs w-full py-1 text-nc-content-red-medium">
|
|
{{ error.toString() }}
|
|
</span>
|
|
</div>
|
|
<span v-else-if="ncIsNull(vModel) && showNull" class="nc-cell-field nc-null uppercase">{{ $t('general.null') }}</span>
|
|
<CellClampedText
|
|
v-else
|
|
:value="!ncIsUndefined(vModel) && !ncIsNull(vModel) ? stringifyProp(vModel) : ''"
|
|
:lines="rowHeight"
|
|
class="nc-cell-field"
|
|
/>
|
|
<NcTooltip placement="bottom" class="nc-json-expand-btn hidden absolute top-0 bottom-0 right-0">
|
|
<template #title>{{ isExpandedFormOpen ? $t('title.expand') : $t('tooltip.expandShiftSpace') }}</template>
|
|
<NcButton type="secondary" size="xsmall" class="!w-5 !h-5 !min-w-[fit-content]" @click.stop="openJSONEditor">
|
|
<component :is="iconMap.maximize" class="w-3 h-3" />
|
|
</NcButton>
|
|
</NcTooltip>
|
|
</component>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.expanded-editor {
|
|
height: min(600px, 80vh);
|
|
min-height: 300px;
|
|
max-height: 85vh;
|
|
max-width: 90vw;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.nc-cell-json:hover .nc-json-expand-btn,
|
|
.nc-grid-cell:hover .nc-json-expand-btn {
|
|
@apply flex items-center;
|
|
}
|
|
.nc-default-value-wrapper .nc-cell-json,
|
|
.nc-grid-cell .nc-cell-json {
|
|
min-height: 20px !important;
|
|
}
|
|
.nc-expanded-cell .nc-cell-json .nc-cell-field {
|
|
margin: 4px 0;
|
|
}
|
|
.nc-expand-col-JSON.nc-expanded-form-row .nc-cell-json {
|
|
min-height: 34px;
|
|
@apply !flex items-center max-w-full;
|
|
& > div {
|
|
@apply !max-w-full w-full;
|
|
}
|
|
}
|
|
|
|
.nc-default-value-wrapper,
|
|
.nc-expanded-cell,
|
|
.ant-form-item-control-input {
|
|
.nc-json-expand-btn {
|
|
@apply flex items-center;
|
|
}
|
|
}
|
|
</style>
|