@ -1,124 +1,146 @@
import type { MaybeRef } from '@vueuse/core'
import type { MaybeRef } from '@vueuse/core'
import { UITypes } from 'nocodb-sdk'
import type { ColumnType , LinkToAnotherRecordType , TableType } from 'nocodb-sdk'
import { message , reactive , ref , unref , useCopy , useEventListener , useI18n } from '#imports'
import { RelationTypes , UITypes , isVirtualCol } from 'nocodb-sdk'
import type { Cell } from './cellRange'
interface SelectedBlock {
import { CellRange } from './cellRange'
row : number | null
import convertCellData from './convertCellData'
col : number | null
import type { Row } from '~/lib'
}
import {
copyTable ,
extractPkFromRow ,
extractSdkResponseErrorMsg ,
isMac ,
message ,
reactive ,
ref ,
unref ,
useCopy ,
useEventListener ,
useI18n ,
useMetas ,
useProject ,
} from '#imports'
/ * *
/ * *
* Utility to help with multi - selecting rows / cells in the smartsheet
* Utility to help with multi - selecting rows / cells in the smartsheet
* /
* /
export function useMultiSelect (
export function useMultiSelect (
fields : MaybeRef < any [ ] > ,
_meta : MaybeRef < TableType > ,
data : MaybeRef < any [ ] > ,
fields : MaybeRef < ColumnType [ ] > ,
data : MaybeRef < Row [ ] > ,
_editEnabled : MaybeRef < boolean > ,
_editEnabled : MaybeRef < boolean > ,
isPkAvail : MaybeRef < boolean | undefined > ,
isPkAvail : MaybeRef < boolean | undefined > ,
clearCell : Function ,
clearCell : Function ,
makeEditable : Function ,
makeEditable : Function ,
scrollToActiveCell ? : ( row? : number | null , col? : number | null ) = > void ,
scrollToActiveCell ? : ( row? : number | null , col? : number | null ) = > void ,
keyEventHandler? : Function ,
keyEventHandler? : Function ,
syncCellData? : Function ,
) {
) {
const meta = ref ( _meta )
const { t } = useI18n ( )
const { t } = useI18n ( )
const { copy } = useCopy ( )
const { copy } = useCopy ( )
const editEnabled = ref ( _editEnabled )
const { getMeta } = useMetas ( )
const selected = reactive < SelectedBlock > ( { row : null , col : null } )
const { isMysql } = useProject ( )
// save the first and the last column where the mouse is down while the value isSelectedRow is true
let clipboardContext = $ref < { value : any ; uidt : UITypes } | null > ( null )
const selectedRows = reactive ( { startCol : NaN , endCol : NaN , startRow : NaN , endRow : NaN } )
// calculate the min and the max column where the mouse is down while the value isSelectedRow is true
const editEnabled = ref ( _editEnabled )
const rangeRows = reactive ( { minRow : NaN , maxRow : NaN , minCol : NaN , maxCol : NaN } )
// check if mouse is down or up false=mouseup and true=mousedown
const selectedCell = reactive < Cell > ( { row : null , col : null } )
let isSelectedBlock = $ref ( false )
const selectedRange = reactive ( new CellRange ( ) )
let isMouseDown = $ref ( false )
const columnLength = $computed ( ( ) = > unref ( fields ) ? . length )
const columnLength = $computed ( ( ) = > unref ( fields ) ? . length )
async function copyValue ( ctx? : Cell ) {
try {
if ( ! selectedRange . isEmpty ( ) ) {
const cprows = unref ( data ) . slice ( selectedRange . start . row , selectedRange . end . row + 1 ) // slice the selected rows for copy
const cpcols = unref ( fields ) . slice ( selectedRange . start . col , selectedRange . end . col + 1 ) // slice the selected cols for copy
await copyTable ( cprows , cpcols )
message . success ( t ( 'msg.info.copiedToClipboard' ) )
} else {
// if copy was called with context (right click position) - copy value from context
// else if there is just one selected cell, copy it's value
const cpRow = ctx ? . row ? ? selectedCell ? . row
const cpCol = ctx ? . col ? ? selectedCell ? . col
if ( cpRow != null && cpCol != null ) {
const rowObj = unref ( data ) [ cpRow ]
const columnObj = unref ( fields ) [ cpCol ]
let textToCopy = ( columnObj . title && rowObj . row [ columnObj . title ] ) || ''
if ( columnObj . uidt === UITypes . Checkbox ) {
textToCopy = ! ! textToCopy
}
if ( typeof textToCopy === 'object' ) {
textToCopy = JSON . stringify ( textToCopy )
}
await copy ( textToCopy )
message . success ( t ( 'msg.info.copiedToClipboard' ) )
}
}
} catch {
message . error ( t ( 'msg.error.copyToClipboardError' ) )
}
}
function selectCell ( row : number , col : number ) {
function selectCell ( row : number , col : number ) {
clearRangeRows ( )
selectedRange . clear ( )
if ( selected . row === row && selected . col === col ) return
if ( selectedCell . row === row && selectedCell . col === col ) return
editEnabled . value = false
editEnabled . value = false
selected . row = row
selectedCell . row = row
selected . col = col
selectedCell . col = col
}
}
function selectBlock ( row : number , col : number ) {
function endSelectRange ( row : number , col : number ) {
// if selected.col and selected.row are null and isSelectedBlock is true that means you are selecting a block
if ( ! isMouseDown ) {
if ( selected . col === null || selected . row === null ) {
return
if ( isSelectedBlock ) {
// save the next value after the selectionStart
selectedRows . endCol = col
selectedRows . endRow = row
}
} else if ( selected . col !== col || selected . row !== row ) {
// if selected.col and selected.row is not null but the selected col and row is not equal at the row and col where the mouse is clicking
// and isSelectedBlock is true that means you are selecting a block
if ( isSelectedBlock ) {
selected . col = null
selected . row = null
// save the next value after the selectionStart
selectedRows . endCol = col
selectedRows . endRow = row
}
}
}
selectedCell . row = null
selectedCell . col = null
selectedRange . endRange ( { row , col } )
}
}
function selectedRange ( row : number , col : number ) {
function isCellSelected ( row : number , col : number ) {
if (
if ( selectedCell ? . row === row && selectedCell ? . col === col ) {
! isNaN ( selectedRows . startRow ) &&
return true
! isNaN ( selectedRows . startCol ) &&
}
! isNaN ( selectedRows . endRow ) &&
! isNaN ( selectedRows . endCol )
if ( selectedRange . isEmpty ( ) ) {
) {
// check if column selection is up or down
rangeRows . minRow = Math . min ( selectedRows . startRow , selectedRows . endRow )
rangeRows . maxRow = Math . max ( selectedRows . startRow , selectedRows . endRow )
rangeRows . minCol = Math . min ( selectedRows . startCol , selectedRows . endCol )
rangeRows . maxCol = Math . max ( selectedRows . startCol , selectedRows . endCol )
// return if the column is in between the selection
return col >= rangeRows . minCol && col <= rangeRows . maxCol && row >= rangeRows . minRow && row <= rangeRows . maxRow
} else {
return false
return false
}
}
return (
col >= selectedRange . start . col &&
col <= selectedRange . end . col &&
row >= selectedRange . start . row &&
row <= selectedRange . end . row
)
}
}
function startSelectRange ( event : MouseEvent , row : number , col : number ) {
function startSelectRange ( event : MouseEvent , row : number , col : number ) {
// if editEnabled but the selected col or the selected row is not equal like the actual row or col, enabled selected multiple rows
// if there was a right click on selected range, don't restart the selection
if ( unref ( editEnabled ) && ( selected . col !== col || selected . row !== row ) ) {
const leftClickButton = 0
event . preventDefault ( )
if ( event ? . button !== leftClickButton && isCellSelected ( row , col ) ) {
} else if ( ! unref ( editEnabled ) ) {
return
// if editEnabled is not true, enabled selected multiple rows
event . preventDefault ( )
}
}
// clear the selection when the mouse is down
if ( unref ( editEnabled ) ) {
selectedRows . startCol = NaN
event . preventDefault ( )
selectedRows . endCol = NaN
return
selectedRows . startRow = NaN
}
selectedRows . endRow = NaN
// asing where the selection start
selectedRows . startCol = col
selectedRows . startRow = row
isSelectedBlock = true
}
function clearRangeRows() {
isMouseDown = true
// when the selection starts or ends or when enter/arrow/tab is pressed
selectedRange . clear ( )
// this clear the previous selection
selectedRange . startRange ( { row , col } )
rangeRows . minCol = NaN
rangeRows . maxCol = NaN
rangeRows . minRow = NaN
rangeRows . maxRow = NaN
selectedRows . startRow = NaN
selectedRows . startCol = NaN
selectedRows . endRow = NaN
selectedRows . endCol = NaN
}
}
useEventListener ( document , 'mouseup' , ( e ) = > {
useEventListener ( document , 'mouseup' , ( e ) = > {
@ -127,7 +149,7 @@ export function useMultiSelect(
e . preventDefault ( )
e . preventDefault ( )
}
}
isSelectedBlock = false
isMouseDown = false
} )
} )
const onKeyDown = async ( e : KeyboardEvent ) = > {
const onKeyDown = async ( e : KeyboardEvent ) = > {
@ -136,41 +158,36 @@ export function useMultiSelect(
return true
return true
}
}
if (
if ( ! selectedRange . isEmpty ( ) ) {
! isNaN ( selectedRows . startRow ) &&
! isNaN ( selectedRows . startCol ) &&
! isNaN ( selectedRows . endRow ) &&
! isNaN ( selectedRows . endCol )
) {
// In case the user press tabs or arrows keys
// In case the user press tabs or arrows keys
selected . row = selectedRows . startR ow
selectedCell . row = selectedRange . start . row
selected . col = selectedRows . startC ol
selectedCell . col = selectedRange . start . col
}
}
if ( selected . row === null || selected . col === null ) return
if ( selectedCell . row === null || selectedCell . col === null ) return
/** on tab key press navigate through cells */
/** on tab key press navigate through cells */
switch ( e . key ) {
switch ( e . key ) {
case 'Tab' :
case 'Tab' :
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
selectedRange . clear ( )
if ( e . shiftKey ) {
if ( e . shiftKey ) {
if ( selected . col > 0 ) {
if ( selectedCell . col > 0 ) {
selected . col --
selectedCell . col --
editEnabled . value = false
editEnabled . value = false
} else if ( selected . row > 0 ) {
} else if ( selectedCell . row > 0 ) {
selected . row --
selectedCell . row --
selected . col = unref ( columnLength ) - 1
selectedCell . col = unref ( columnLength ) - 1
editEnabled . value = false
editEnabled . value = false
}
}
} else {
} else {
if ( selected . col < unref ( columnLength ) - 1 ) {
if ( selectedCell . col < unref ( columnLength ) - 1 ) {
selected . col ++
selectedCell . col ++
editEnabled . value = false
editEnabled . value = false
} else if ( selected . row < unref ( data ) . length - 1 ) {
} else if ( selectedCell . row < unref ( data ) . length - 1 ) {
selected . row ++
selectedCell . row ++
selected . col = 0
selectedCell . col = 0
editEnabled . value = false
editEnabled . value = false
}
}
}
}
@ -179,91 +196,147 @@ export function useMultiSelect(
/** on enter key press make cell editable */
/** on enter key press make cell editable */
case 'Enter' :
case 'Enter' :
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
selectedRange . clear ( )
makeEditable ( unref ( data ) [ selected . row ] , unref ( fields ) [ selected . col ] )
makeEditable ( unref ( data ) [ selectedCell . row ] , unref ( fields ) [ selectedCell . col ] )
break
break
/** on delete key press clear cell */
/** on delete key press clear cell */
case 'Delete' :
case 'Delete' :
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
selectedRange . clear ( )
await clearCell ( selected as { row : number ; col : number } )
await clearCell ( selectedCell as { row : number ; col : number } )
break
break
/** on arrow key press navigate through cells */
/** on arrow key press navigate through cells */
case 'ArrowRight' :
case 'ArrowRight' :
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
selectedRange . clear ( )
if ( selected . col < unref ( columnLength ) - 1 ) {
if ( selectedCell . col < unref ( columnLength ) - 1 ) {
selected . col ++
selectedCell . col ++
scrollToActiveCell ? . ( )
scrollToActiveCell ? . ( )
editEnabled . value = false
editEnabled . value = false
}
}
break
break
case 'ArrowLeft' :
case 'ArrowLeft' :
clearRangeRows ( )
selectedRange . clear ( )
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
if ( selectedCell . col > 0 ) {
if ( selected . col > 0 ) {
selectedCell . col --
selected . col --
scrollToActiveCell ? . ( )
scrollToActiveCell ? . ( )
editEnabled . value = false
editEnabled . value = false
}
}
break
break
case 'ArrowUp' :
case 'ArrowUp' :
clearRangeRows ( )
selectedRange . clear ( )
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
if ( selectedCell . row > 0 ) {
if ( selected . row > 0 ) {
selectedCell . row --
selected . row --
scrollToActiveCell ? . ( )
scrollToActiveCell ? . ( )
editEnabled . value = false
editEnabled . value = false
}
}
break
break
case 'ArrowDown' :
case 'ArrowDown' :
clearRangeRows ( )
selectedRange . clear ( )
e . preventDefault ( )
e . preventDefault ( )
clearRangeRows ( )
if ( selectedCell . row < unref ( data ) . length - 1 ) {
if ( selected . row < unref ( data ) . length - 1 ) {
selectedCell . row ++
selected . row ++
scrollToActiveCell ? . ( )
scrollToActiveCell ? . ( )
editEnabled . value = false
editEnabled . value = false
}
}
break
break
default :
default :
{
{
const rowObj = unref ( data ) [ selected . row ]
const rowObj = unref ( data ) [ selectedCell . row ]
const columnObj = unref ( fields ) [ selected . col ]
const columnObj = unref ( fields ) [ selectedCell . col ]
let cptext = '' // variable for save the text to be copy
if (
( ! unref ( editEnabled ) ||
if ( ! isNaN ( rangeRows . minRow ) && ! isNaN ( rangeRows . maxRow ) && ! isNaN ( rangeRows . minCol ) && ! isNaN ( rangeRows . maxCol ) ) {
[
const cprows = unref ( data ) . slice ( rangeRows . minRow , rangeRows . maxRow + 1 ) // slice the selected rows for copy
UITypes . DateTime ,
UITypes . Date ,
const cpcols = unref ( fields ) . slice ( rangeRows . minCol , rangeRows . maxCol + 1 ) // slice the selected cols for copy
UITypes . Year ,
UITypes . Time ,
cprows . forEach ( ( row ) = > {
UITypes . Lookup ,
cpcols . forEach ( ( col ) = > {
UITypes . Rollup ,
// todo: JSON stringify the attachment cell and LTAR contents for copy
UITypes . Formula ,
// filter attachment cells and LATR cells from copy
UITypes . Attachment ,
if ( col . uidt !== UITypes . Attachment && col . uidt !== UITypes . LinkToAnotherRecord ) {
UITypes . Checkbox ,
cptext = ` ${ cptext } ${ row . row [ col . title ] } \ t `
UITypes . Rating ,
}
] . includes ( columnObj . uidt as UITypes ) ) &&
} )
( isMac ( ) ? e.metaKey : e.ctrlKey )
) {
cptext = ` ${ cptext . trim ( ) } \ n `
} )
cptext . trim ( )
} else {
cptext = rowObj . row [ columnObj . title ] || ''
}
if ( ( ! unref ( editEnabled ) && e . metaKey ) || e . ctrlKey ) {
switch ( e . keyCode ) {
switch ( e . keyCode ) {
// copy - ctrl/cmd +c
// copy - ctrl/cmd +c
case 67 :
case 67 :
await copy ( cptext )
// set clipboard context only if single cell selected
if ( rowObj . row [ columnObj . title ! ] ) {
clipboardContext = {
value : rowObj.row [ columnObj . title ! ] ,
uidt : columnObj.uidt as UITypes ,
}
} else {
clipboardContext = null
}
await copyValue ( )
break
break
case 86 :
try {
// handle belongs to column
if (
columnObj . uidt === UITypes . LinkToAnotherRecord &&
( columnObj . colOptions as LinkToAnotherRecordType ) ? . type === RelationTypes . BELONGS_TO
) {
if ( ! clipboardContext || typeof clipboardContext . value !== 'object' ) {
return message . info ( 'Invalid data' )
}
rowObj . row [ columnObj . title ! ] = convertCellData (
{
value : clipboardContext.value ,
from : clipboardContext . uidt ,
to : columnObj.uidt as UITypes ,
} ,
isMysql . value ,
)
e . preventDefault ( )
const foreignKeyColumn = meta . value ? . columns ? . find (
( column : ColumnType ) = > column . id === ( columnObj . colOptions as LinkToAnotherRecordType ) ? . fk_child_column_id ,
)
const relatedTableMeta = await getMeta ( ( columnObj . colOptions as LinkToAnotherRecordType ) . fk_related_model_id ! )
if ( ! foreignKeyColumn ) return
rowObj . row [ foreignKeyColumn . title ! ] = extractPkFromRow (
clipboardContext . value ,
( relatedTableMeta as any ) ! . columns ! ,
)
return await syncCellData ? . ( { . . . selectedCell , updatedColumnTitle : foreignKeyColumn.title } )
}
// if it's a virtual column excluding belongs to cell type skip paste
if ( isVirtualCol ( columnObj ) ) {
return message . info ( t ( 'msg.info.pasteNotSupported' ) )
}
if ( clipboardContext ) {
rowObj . row [ columnObj . title ! ] = convertCellData (
{
value : clipboardContext.value ,
from : clipboardContext . uidt ,
to : columnObj.uidt as UITypes ,
} ,
isMysql . value ,
)
e . preventDefault ( )
syncCellData ? . ( selectedCell )
} else {
clearCell ( selectedCell as { row : number ; col : number } , true )
makeEditable ( rowObj , columnObj )
}
} catch ( error : any ) {
message . error ( await extractSdkResponseErrorMsg ( error ) )
}
}
}
}
}
@ -277,7 +350,7 @@ export function useMultiSelect(
// Update not allowed for table which doesn't have primary Key
// Update not allowed for table which doesn't have primary Key
return message . info ( t ( 'msg.info.updateNotAllowedWithoutPK' ) )
return message . info ( t ( 'msg.info.updateNotAllowedWithoutPK' ) )
}
}
if ( makeEditable ( rowObj , columnObj ) ) {
if ( makeEditable ( rowObj , columnObj ) && columnObj . title ) {
rowObj . row [ columnObj . title ] = ''
rowObj . row [ columnObj . title ] = ''
}
}
// editEnabled = true
// editEnabled = true
@ -291,12 +364,11 @@ export function useMultiSelect(
return {
return {
selectCell ,
selectCell ,
selectBlock ,
selectedRange ,
clearRangeRows ,
startSelectRange ,
startSelectRange ,
selected ,
endSelectRange ,
selectedRows ,
clearSelectedRange : selectedRange.clear.bind ( selectedRange ) ,
rangeRows ,
copyValue ,
isCellSelected ,
selectedCell ,
}
}
}
}