Browse Source

fix: snap fill handle

Signed-off-by: mertmit <mertmit99@gmail.com>
pull/5896/head
mertmit 1 year ago
parent
commit
c1b5e76a43
  1. 498
      packages/nc-gui/components/smartsheet/Grid.vue

498
packages/nc-gui/components/smartsheet/Grid.vue

@ -2,7 +2,6 @@
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, GridType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { UseElementBoundingReturn } from '@vueuse/core'
import { import {
ActiveViewInj, ActiveViewInj,
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
@ -112,6 +111,8 @@ const tableHeadEl = ref<HTMLElement>()
const tableBodyEl = ref<HTMLElement>() const tableBodyEl = ref<HTMLElement>()
const fillHandle = ref<HTMLElement>() const fillHandle = ref<HTMLElement>()
const gridRect = useElementBounding(gridWrapper)
const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value) const isAddingColumnAllowed = $computed(() => !readOnly.value && !isLocked.value && isUIAllowed('add-column') && !isSqlView.value)
const isAddingEmptyRowAllowed = $computed(() => !isView && !isLocked.value && hasEditPermission && !isSqlView.value) const isAddingEmptyRowAllowed = $computed(() => !isView && !isLocked.value && hasEditPermission && !isSqlView.value)
@ -891,16 +892,19 @@ function addEmptyRow(row?: number) {
return rowObj return rowObj
} }
const tableRect = useElementBounding(smartTable)
const { height: tableHeight, width: tableWidth } = tableRect
const { x: gridX, y: gridY } = useScroll(gridWrapper)
const fillHandleTop = ref() const fillHandleTop = ref()
const fillHandleLeft = ref() const fillHandleLeft = ref()
const cellRefs = ref<{ el: HTMLElement }[]>([]) const cellRefs = ref<{ el: HTMLElement }[]>([])
let cellRect: UseElementBoundingReturn | null = null
const showFillHandle = computed(
() =>
!readOnly.value &&
!isLocked.value &&
!editEnabled &&
(!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null)) &&
!data.value[selectedRange.end.row ?? activeCell.row]?.rowMeta?.new,
)
const refreshFillHandle = () => { const refreshFillHandle = () => {
const cellRef = cellRefs.value.find( const cellRef = cellRefs.value.find(
@ -908,28 +912,22 @@ const refreshFillHandle = () => {
cell.el.dataset.rowIndex === String(selectedRange.end.row) && cell.el.dataset.colIndex === String(selectedRange.end.col), cell.el.dataset.rowIndex === String(selectedRange.end.row) && cell.el.dataset.colIndex === String(selectedRange.end.col),
) )
if (cellRef) { if (cellRef) {
cellRect = useElementBounding(cellRef.el) const cellRect = useElementBounding(cellRef.el)
if (!cellRect || !tableRect) return if (!cellRect || !gridWrapper.value) return
if (selectedRange.end.col === 0) { fillHandleTop.value = cellRect.top.value + cellRect.height.value - gridRect.top.value + gridWrapper.value.scrollTop
fillHandleTop.value = cellRect.top.value - tableRect.top.value + cellRect.height.value + gridY.value fillHandleLeft.value = cellRect.left.value + cellRect.width.value - gridRect.left.value + gridWrapper.value.scrollLeft
fillHandleLeft.value = cellRect.left.value - tableRect.left.value + cellRect.width.value
return
}
fillHandleTop.value = cellRect.top.value - tableRect.top.value + cellRect.height.value + gridY.value
fillHandleLeft.value = cellRect.left.value - tableRect.left.value + cellRect.width.value + gridX.value
} }
} }
watch( watch([() => selectedRange.end.row, () => selectedRange.end.col], (n, o) => {
() => `${selectedRange.end.row}-${selectedRange.end.col}`, if (n !== o) {
(n, o) => { refreshFillHandle()
if (n !== o) { }
if (gridWrapper.value) { })
refreshFillHandle()
} useEventListener(gridWrapper, 'scroll', () => {
} refreshFillHandle()
}, })
)
</script> </script>
<template> <template>
@ -940,233 +938,233 @@ watch(
</div> </div>
</general-overlay> </general-overlay>
<div <div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull relative">
ref="gridWrapper"
class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull"
:class="{
relative:
!readOnly &&
!isLocked &&
(!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null)) &&
((!selectedRange.isEmpty() && selectedRange.end.col !== 0) || (selectedRange.isEmpty() && activeCell.col !== 0)),
}"
>
<a-dropdown <a-dropdown
v-model:visible="contextMenu" v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']" :trigger="isSqlView ? [] : ['contextmenu']"
overlay-class-name="nc-dropdown-grid-context-menu" overlay-class-name="nc-dropdown-grid-context-menu"
> >
<table <div class="table-overlay">
ref="smartTable" <table
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white" ref="smartTable"
@contextmenu="showContextMenu" class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
> @contextmenu="showContextMenu"
<thead ref="tableHeadEl"> >
<tr class="nc-grid-header"> <thead ref="tableHeadEl">
<th class="w-[85px] min-w-[85px]" data-testid="grid-id-column"> <tr class="nc-grid-header">
<div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all"> <th class="w-[85px] min-w-[85px]" data-testid="grid-id-column">
<template v-if="!readOnly"> <div class="w-full h-full bg-gray-100 flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div> <template v-if="!readOnly">
<div <div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }" <div
class="nc-check-all w-full items-center" :class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
> class="nc-check-all w-full items-center"
<a-checkbox v-model:checked="selectedAllRecords" /> >
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" /> <span class="flex-1" />
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="text-gray-500">#</div> <div class="text-gray-500">#</div>
</template> </template>
</div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div>
</th>
<th
v-if="isAddingColumnAllowed"
v-e="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
>
<a-dropdown
v-model:visible="addColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-grid-add-column"
>
<div class="h-full w-[60px] flex items-center justify-center">
<component :is="iconMap.plus" class="text-sm nc-column-add" />
</div> </div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<template #overlay> <LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
<SmartsheetColumnEditOrAddProvider </div>
v-if="addColumnDropdown" </th>
:preload="preloadColumn" <th
:column-position="columnOrder" v-if="isAddingColumnAllowed"
@submit="closeAddColumnDropdown(true)" v-e="['c:column:add']"
@cancel="closeAddColumnDropdown()" class="cursor-pointer"
@click.stop @click.stop="addColumnDropdown = true"
@keydown.stop
/>
</template>
</a-dropdown>
</th>
</tr>
</thead>
<tbody ref="tableBodyEl">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr
class="nc-grid-row"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`"
> >
<td <a-dropdown
key="row-index" v-model:visible="addColumnDropdown"
class="caption nc-grid-cell pl-5 pr-1" :trigger="['click']"
:data-testid="`cell-Id-${rowIndex}`" overlay-class-name="nc-dropdown-grid-add-column"
@contextmenu="contextMenuTarget = null"
> >
<div class="items-center flex gap-1 min-w-[60px]"> <div class="h-full w-[60px] flex items-center justify-center">
<div <component :is="iconMap.plus" class="text-sm nc-column-add" />
v-if="!readOnly || !isLocked" </div>
class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ ((paginationData.page ?? 1) - 1) * (paginationData.pageSize ?? 25) + rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div <template #overlay>
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)" <SmartsheetColumnEditOrAddProvider
class="nc-expand" v-if="addColumnDropdown"
:data-testid="`nc-expand-${rowIndex}`" :preload="preloadColumn"
:class="{ 'nc-comment': row.rowMeta?.commentCount }" :column-position="columnOrder"
> @submit="closeAddColumnDropdown(true)"
<a-spin @cancel="closeAddColumnDropdown()"
v-if="row.rowMeta.saving" @click.stop
class="!flex items-center" @keydown.stop
:data-testid="`row-save-spinner-${rowIndex}`" />
/> </template>
<template v-else> </a-dropdown>
<span </th>
v-if="row.rowMeta?.commentCount" </tr>
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)" </thead>
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }" <tbody ref="tableBodyEl">
@click="expandForm(row, state)" <LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
> <template #default="{ state }">
{{ row.rowMeta.commentCount }} <tr
</span> class="nc-grid-row"
<div :style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
v-else :data-testid="`grid-row-${rowIndex}`"
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)" >
> <td key="row-index" class="caption nc-grid-cell pl-5 pr-1" :data-testid="`cell-Id-${rowIndex}`">
<component <div class="items-center flex gap-1 min-w-[60px]">
:is="iconMap.expand" <div
v-e="['c:row-expand']" v-if="!readOnly || !isLocked"
class="select-none transform hover:(text-accent scale-120) nc-row-expand" class="nc-row-no text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
>
{{ ((paginationData.page ?? 1) - 1) * (paginationData.pageSize ?? 25) + rowIndex + 1 }}
</div>
<div
v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div
v-if="!readOnly || hasRole('commenter', true) || hasRole('viewer', true)"
class="nc-expand"
:data-testid="`nc-expand-${rowIndex}`"
:class="{ 'nc-comment': row.rowMeta?.commentCount }"
>
<a-spin
v-if="row.rowMeta.saving"
class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`"
/>
<template v-else>
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandForm(row, state)" @click="expandForm(row, state)"
/> >
</div> {{ row.rowMeta.commentCount }}
</template> </span>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<component
:is="iconMap.expand"
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)"
/>
</div>
</template>
</div>
</div> </div>
</div> </td>
</td> <SmartsheetTableDataCell
<SmartsheetTableDataCell v-for="(columnObj, colIndex) of fields"
v-for="(columnObj, colIndex) of fields" :key="columnObj.id"
:key="columnObj.id" ref="cellRefs"
ref="cellRefs" class="cell relative nc-grid-cell"
class="cell relative nc-grid-cell" :class="{
:class="{ 'cursor-pointer': hasEditPermission,
'cursor-pointer': hasEditPermission, 'active': hasEditPermission && isCellSelected(rowIndex, colIndex),
'active': hasEditPermission && isCellSelected(rowIndex, colIndex), 'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row),
'nc-required-cell': isColumnRequiredAndNull(columnObj, row.row), 'align-middle': !rowHeight || rowHeight === 1,
'align-middle': !rowHeight || rowHeight === 1, 'align-top': rowHeight && rowHeight !== 1,
'align-top': rowHeight && rowHeight !== 1, 'filling': isCellInFillRange(rowIndex, colIndex),
'filling': isCellInFillRange(rowIndex, colIndex), }"
}" :data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-testid="`cell-${columnObj.title}-${rowIndex}`" :data-key="rowIndex + columnObj.id"
:data-key="rowIndex + columnObj.id" :data-col="columnObj.id"
:data-col="columnObj.id" :data-title="columnObj.title"
:data-title="columnObj.title" :data-row-index="rowIndex"
:data-row-index="rowIndex" :data-col-index="colIndex"
:data-col-index="colIndex" @mousedown="handleMouseDown($event, rowIndex, colIndex)"
@mousedown="handleMouseDown($event, rowIndex, colIndex)" @mouseover="handleMouseOver($event, rowIndex, colIndex)"
@mouseover="handleMouseOver($event, rowIndex, colIndex)" @click="handleCellClick($event, rowIndex, colIndex)"
@click="handleCellClick($event, rowIndex, colIndex)" @dblclick="makeEditable(row, columnObj)"
@dblclick="makeEditable(row, columnObj)" @contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })" >
> <div v-if="!switchingTab" class="w-full h-full">
<div v-if="!switchingTab" class="w-full h-full"> <LazySmartsheetVirtualCell
<LazySmartsheetVirtualCell v-if="isVirtualCol(columnObj)"
v-if="isVirtualCol(columnObj)" v-model="row.row[columnObj.title]"
v-model="row.row[columnObj.title]" :column="columnObj"
:column="columnObj" :active="activeCell.col === colIndex && activeCell.row === rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex" :row="row"
:row="row" :read-only="readOnly"
:read-only="readOnly" @navigate="onNavigate"
@navigate="onNavigate" @save="updateOrSaveRow(row, '', state)"
@save="updateOrSaveRow(row, '', state)" />
/>
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else
v-model="row.row[columnObj.title]" v-model="row.row[columnObj.title]"
:column="columnObj" :column="columnObj"
:edit-enabled=" :edit-enabled="
!!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex !!hasEditPermission && !!editEnabled && activeCell.col === colIndex && activeCell.row === rowIndex
" "
:row-index="rowIndex" :row-index="rowIndex"
:active="activeCell.col === colIndex && activeCell.row === rowIndex" :active="activeCell.col === colIndex && activeCell.row === rowIndex"
:read-only="readOnly" :read-only="readOnly"
@update:edit-enabled="editEnabled = $event" @update:edit-enabled="editEnabled = $event"
@save="updateOrSaveRow(row, columnObj.title, state)" @save="updateOrSaveRow(row, columnObj.title, state)"
@navigate="onNavigate" @navigate="onNavigate"
@cancel="editEnabled = false" @cancel="editEnabled = false"
/> />
</div> </div>
</SmartsheetTableDataCell> </SmartsheetTableDataCell>
</tr> </tr>
</template> </template>
</LazySmartsheetRow> </LazySmartsheetRow>
<tr <tr
v-if="isAddingEmptyRowAllowed" v-if="isAddingEmptyRowAllowed"
v-e="['c:row:add:grid-bottom']" v-e="['c:row:add:grid-bottom']"
class="cursor-pointer" class="cursor-pointer relative z-3"
@mouseup.stop @mouseup.stop
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<td class="text-left pointer nc-grid-add-new-cell sticky left-0 !z-5 !border-r-0"> <td class="text-left pointer nc-grid-add-new-cell sticky left-0 !z-5 !border-r-0">
<div class="px-2 w-full flex items-center text-gray-500"> <div class="px-2 w-full flex items-center text-gray-500">
<component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" /> <component :is="iconMap.plus" class="text-pint-500 text-xs ml-2 text-primary" />
</div> </div>
</td> </td>
<td :colspan="visibleColLength"></td> <td :colspan="visibleColLength"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- Fill Handle -->
<div
v-show="showFillHandle"
ref="fillHandle"
class="nc-fill-handle"
:class="
(!selectedRange.isEmpty() && selectedRange.end.col !== 0) || (selectedRange.isEmpty() && activeCell.col !== 0)
? 'z-3'
: 'z-4'
"
:style="{ top: `${fillHandleTop}px`, left: `${fillHandleLeft}px`, cursor: 'crosshair' }"
/>
</div>
<template v-if="!isLocked && hasEditPermission" #overlay> <template v-if="!isLocked && hasEditPermission" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false"> <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
@ -1243,28 +1241,6 @@ watch(
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<div
class="table-overlay absolute top-0 left-0 pointer-events-none overflow-hidden"
:style="{ height: `${tableHeight}px`, width: `${tableWidth}px` }"
>
<!-- Fill Handle -->
<div
v-show="
!readOnly &&
!isLocked &&
!editEnabled &&
(!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null))
"
ref="fillHandle"
class="nc-fill-handle"
:class="
(!selectedRange.isEmpty() && selectedRange.end.col !== 0) || (selectedRange.isEmpty() && activeCell.col !== 0)
? 'z-3'
: 'z-4'
"
:style="{ top: `${fillHandleTop}px`, left: `${fillHandleLeft}px`, cursor: 'crosshair' }"
/>
</div>
</div> </div>
<div <div

Loading…
Cancel
Save