|
|
|
@ -1,18 +1,16 @@
|
|
|
|
|
import type { MaybeRef } from '@vueuse/core' |
|
|
|
|
import { UITypes } from 'nocodb-sdk' |
|
|
|
|
import { message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports' |
|
|
|
|
|
|
|
|
|
interface SelectedBlock { |
|
|
|
|
row: number | null |
|
|
|
|
col: number | null |
|
|
|
|
} |
|
|
|
|
import type { ColumnType } from 'nocodb-sdk' |
|
|
|
|
import type { Cell } from './cellRange' |
|
|
|
|
import { CellRange } from './cellRange' |
|
|
|
|
import { copyTable, message, reactive, ref, unref, useCopy, useEventListener, useI18n } from '#imports' |
|
|
|
|
import type { Row } from '~/lib' |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Utility to help with multi-selecting rows/cells in the smartsheet |
|
|
|
|
*/ |
|
|
|
|
export function useMultiSelect( |
|
|
|
|
fields: MaybeRef<any[]>, |
|
|
|
|
data: MaybeRef<any[]>, |
|
|
|
|
fields: MaybeRef<ColumnType[]>, |
|
|
|
|
data: MaybeRef<Row[]>, |
|
|
|
|
_editEnabled: MaybeRef<boolean>, |
|
|
|
|
isPkAvail: MaybeRef<boolean | undefined>, |
|
|
|
|
clearCell: Function, |
|
|
|
@ -26,99 +24,92 @@ export function useMultiSelect(
|
|
|
|
|
|
|
|
|
|
const editEnabled = ref(_editEnabled) |
|
|
|
|
|
|
|
|
|
const selected = reactive<SelectedBlock>({ row: null, col: null }) |
|
|
|
|
const selectedCell = reactive<Cell>({ row: null, col: null }) |
|
|
|
|
const selectedRange = reactive(new CellRange()) |
|
|
|
|
let isMouseDown = $ref(false) |
|
|
|
|
|
|
|
|
|
// save the first and the last column where the mouse is down while the value isSelectedRow is true
|
|
|
|
|
const selectedRows = reactive({ startCol: NaN, endCol: NaN, startRow: NaN, endRow: NaN }) |
|
|
|
|
const columnLength = $computed(() => unref(fields)?.length) |
|
|
|
|
|
|
|
|
|
// calculate the min and the max column where the mouse is down while the value isSelectedRow is true
|
|
|
|
|
const rangeRows = reactive({ minRow: NaN, maxRow: NaN, minCol: NaN, maxCol: NaN }) |
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
// check if mouse is down or up false=mouseup and true=mousedown
|
|
|
|
|
let isSelectedBlock = $ref(false) |
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
const columnLength = $computed(() => unref(fields)?.length) |
|
|
|
|
if (cpRow != null && cpCol != null) { |
|
|
|
|
const rowObj = unref(data)[cpRow] |
|
|
|
|
const columnObj = unref(fields)[cpCol] |
|
|
|
|
|
|
|
|
|
let textToCopy = (columnObj.title && rowObj.row[columnObj.title]) || '' |
|
|
|
|
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) { |
|
|
|
|
clearRangeRows() |
|
|
|
|
if (selected.row === row && selected.col === col) return |
|
|
|
|
selectedRange.clear() |
|
|
|
|
if (selectedCell.row === row && selectedCell.col === col) return |
|
|
|
|
editEnabled.value = false |
|
|
|
|
selected.row = row |
|
|
|
|
selected.col = col |
|
|
|
|
selectedCell.row = row |
|
|
|
|
selectedCell.col = col |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function selectBlock(row: number, col: number) { |
|
|
|
|
// if selected.col and selected.row are null and isSelectedBlock is true that means you are selecting a block
|
|
|
|
|
if (selected.col === null || selected.row === null) { |
|
|
|
|
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 |
|
|
|
|
} |
|
|
|
|
function endSelectRange(row: number, col: number) { |
|
|
|
|
if (!isMouseDown) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
selectedCell.row = null |
|
|
|
|
selectedCell.col = null |
|
|
|
|
selectedRange.endRange({ row, col }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function selectedRange(row: number, col: number) { |
|
|
|
|
if ( |
|
|
|
|
!isNaN(selectedRows.startRow) && |
|
|
|
|
!isNaN(selectedRows.startCol) && |
|
|
|
|
!isNaN(selectedRows.endRow) && |
|
|
|
|
!isNaN(selectedRows.endCol) |
|
|
|
|
) { |
|
|
|
|
// 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) |
|
|
|
|
function isCellSelected(row: number, col: number) { |
|
|
|
|
if (selectedCell?.row === row && selectedCell?.col === col) { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// return if the column is in between the selection
|
|
|
|
|
return col >= rangeRows.minCol && col <= rangeRows.maxCol && row >= rangeRows.minRow && row <= rangeRows.maxRow |
|
|
|
|
} else { |
|
|
|
|
if (selectedRange.isEmpty()) { |
|
|
|
|
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) { |
|
|
|
|
// if editEnabled but the selected col or the selected row is not equal like the actual row or col, enabled selected multiple rows
|
|
|
|
|
if (unref(editEnabled) && (selected.col !== col || selected.row !== row)) { |
|
|
|
|
event.preventDefault() |
|
|
|
|
} else if (!unref(editEnabled)) { |
|
|
|
|
// if editEnabled is not true, enabled selected multiple rows
|
|
|
|
|
event.preventDefault() |
|
|
|
|
// if there was a right click on selected range, don't restart the selection
|
|
|
|
|
const leftClickButton = 0 |
|
|
|
|
if (event?.button !== leftClickButton && isCellSelected(row, col)) { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// clear the selection when the mouse is down
|
|
|
|
|
selectedRows.startCol = NaN |
|
|
|
|
selectedRows.endCol = NaN |
|
|
|
|
selectedRows.startRow = NaN |
|
|
|
|
selectedRows.endRow = NaN |
|
|
|
|
// asing where the selection start
|
|
|
|
|
selectedRows.startCol = col |
|
|
|
|
selectedRows.startRow = row |
|
|
|
|
isSelectedBlock = true |
|
|
|
|
if (unref(editEnabled)) { |
|
|
|
|
event.preventDefault() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function clearRangeRows() { |
|
|
|
|
// when the selection starts or ends or when enter/arrow/tab is pressed
|
|
|
|
|
// this clear the previous selection
|
|
|
|
|
rangeRows.minCol = NaN |
|
|
|
|
rangeRows.maxCol = NaN |
|
|
|
|
rangeRows.minRow = NaN |
|
|
|
|
rangeRows.maxRow = NaN |
|
|
|
|
selectedRows.startRow = NaN |
|
|
|
|
selectedRows.startCol = NaN |
|
|
|
|
selectedRows.endRow = NaN |
|
|
|
|
selectedRows.endCol = NaN |
|
|
|
|
isMouseDown = true |
|
|
|
|
selectedRange.clear() |
|
|
|
|
selectedRange.startRange({ row, col }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
useEventListener(document, 'mouseup', (e) => { |
|
|
|
@ -127,7 +118,7 @@ export function useMultiSelect(
|
|
|
|
|
e.preventDefault() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
isSelectedBlock = false |
|
|
|
|
isMouseDown = false |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const onKeyDown = async (e: KeyboardEvent) => { |
|
|
|
@ -136,41 +127,36 @@ export function useMultiSelect(
|
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
!isNaN(selectedRows.startRow) && |
|
|
|
|
!isNaN(selectedRows.startCol) && |
|
|
|
|
!isNaN(selectedRows.endRow) && |
|
|
|
|
!isNaN(selectedRows.endCol) |
|
|
|
|
) { |
|
|
|
|
if (!selectedRange.isEmpty()) { |
|
|
|
|
// In case the user press tabs or arrows keys
|
|
|
|
|
selected.row = selectedRows.startRow |
|
|
|
|
selected.col = selectedRows.startCol |
|
|
|
|
selectedCell.row = selectedRange.start.row |
|
|
|
|
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 */ |
|
|
|
|
switch (e.key) { |
|
|
|
|
case 'Tab': |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
selectedRange.clear() |
|
|
|
|
|
|
|
|
|
if (e.shiftKey) { |
|
|
|
|
if (selected.col > 0) { |
|
|
|
|
selected.col-- |
|
|
|
|
if (selectedCell.col > 0) { |
|
|
|
|
selectedCell.col-- |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} else if (selected.row > 0) { |
|
|
|
|
selected.row-- |
|
|
|
|
selected.col = unref(columnLength) - 1 |
|
|
|
|
} else if (selectedCell.row > 0) { |
|
|
|
|
selectedCell.row-- |
|
|
|
|
selectedCell.col = unref(columnLength) - 1 |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if (selected.col < unref(columnLength) - 1) { |
|
|
|
|
selected.col++ |
|
|
|
|
if (selectedCell.col < unref(columnLength) - 1) { |
|
|
|
|
selectedCell.col++ |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} else if (selected.row < unref(data).length - 1) { |
|
|
|
|
selected.row++ |
|
|
|
|
selected.col = 0 |
|
|
|
|
} else if (selectedCell.row < unref(data).length - 1) { |
|
|
|
|
selectedCell.row++ |
|
|
|
|
selectedCell.col = 0 |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -179,90 +165,63 @@ export function useMultiSelect(
|
|
|
|
|
/** on enter key press make cell editable */ |
|
|
|
|
case 'Enter': |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
makeEditable(unref(data)[selected.row], unref(fields)[selected.col]) |
|
|
|
|
selectedRange.clear() |
|
|
|
|
makeEditable(unref(data)[selectedCell.row], unref(fields)[selectedCell.col]) |
|
|
|
|
break |
|
|
|
|
/** on delete key press clear cell */ |
|
|
|
|
case 'Delete': |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
await clearCell(selected as { row: number; col: number }) |
|
|
|
|
selectedRange.clear() |
|
|
|
|
await clearCell(selectedCell as { row: number; col: number }) |
|
|
|
|
break |
|
|
|
|
/** on arrow key press navigate through cells */ |
|
|
|
|
case 'ArrowRight': |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
if (selected.col < unref(columnLength) - 1) { |
|
|
|
|
selected.col++ |
|
|
|
|
selectedRange.clear() |
|
|
|
|
if (selectedCell.col < unref(columnLength) - 1) { |
|
|
|
|
selectedCell.col++ |
|
|
|
|
scrollToActiveCell?.() |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} |
|
|
|
|
break |
|
|
|
|
case 'ArrowLeft': |
|
|
|
|
clearRangeRows() |
|
|
|
|
selectedRange.clear() |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
if (selected.col > 0) { |
|
|
|
|
selected.col-- |
|
|
|
|
if (selectedCell.col > 0) { |
|
|
|
|
selectedCell.col-- |
|
|
|
|
scrollToActiveCell?.() |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} |
|
|
|
|
break |
|
|
|
|
case 'ArrowUp': |
|
|
|
|
clearRangeRows() |
|
|
|
|
selectedRange.clear() |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
if (selected.row > 0) { |
|
|
|
|
selected.row-- |
|
|
|
|
if (selectedCell.row > 0) { |
|
|
|
|
selectedCell.row-- |
|
|
|
|
scrollToActiveCell?.() |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} |
|
|
|
|
break |
|
|
|
|
case 'ArrowDown': |
|
|
|
|
clearRangeRows() |
|
|
|
|
selectedRange.clear() |
|
|
|
|
e.preventDefault() |
|
|
|
|
clearRangeRows() |
|
|
|
|
if (selected.row < unref(data).length - 1) { |
|
|
|
|
selected.row++ |
|
|
|
|
if (selectedCell.row < unref(data).length - 1) { |
|
|
|
|
selectedCell.row++ |
|
|
|
|
scrollToActiveCell?.() |
|
|
|
|
editEnabled.value = false |
|
|
|
|
} |
|
|
|
|
break |
|
|
|
|
default: |
|
|
|
|
{ |
|
|
|
|
const rowObj = unref(data)[selected.row] |
|
|
|
|
|
|
|
|
|
const columnObj = unref(fields)[selected.col] |
|
|
|
|
|
|
|
|
|
let cptext = '' // variable for save the text to be copy
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
const cpcols = unref(fields).slice(rangeRows.minCol, rangeRows.maxCol + 1) // slice the selected cols for copy
|
|
|
|
|
const rowObj = unref(data)[selectedCell.row] |
|
|
|
|
|
|
|
|
|
cprows.forEach((row) => { |
|
|
|
|
cpcols.forEach((col) => { |
|
|
|
|
// todo: JSON stringify the attachment cell and LTAR contents for copy
|
|
|
|
|
// filter attachment cells and LATR cells from copy
|
|
|
|
|
if (col.uidt !== UITypes.Attachment && col.uidt !== UITypes.LinkToAnotherRecord) { |
|
|
|
|
cptext = `${cptext} ${row.row[col.title]} \t` |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
cptext = `${cptext.trim()}\n` |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
cptext.trim() |
|
|
|
|
} else { |
|
|
|
|
cptext = rowObj.row[columnObj.title] || '' |
|
|
|
|
} |
|
|
|
|
const columnObj = unref(fields)[selectedCell.col] |
|
|
|
|
|
|
|
|
|
if ((!unref(editEnabled) && e.metaKey) || e.ctrlKey) { |
|
|
|
|
switch (e.keyCode) { |
|
|
|
|
// copy - ctrl/cmd +c
|
|
|
|
|
case 67: |
|
|
|
|
await copy(cptext) |
|
|
|
|
await copyValue() |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -277,7 +236,7 @@ export function useMultiSelect(
|
|
|
|
|
// Update not allowed for table which doesn't have primary Key
|
|
|
|
|
return message.info(t('msg.info.updateNotAllowedWithoutPK')) |
|
|
|
|
} |
|
|
|
|
if (makeEditable(rowObj, columnObj)) { |
|
|
|
|
if (makeEditable(rowObj, columnObj) && columnObj.title) { |
|
|
|
|
rowObj.row[columnObj.title] = '' |
|
|
|
|
} |
|
|
|
|
// editEnabled = true
|
|
|
|
@ -291,12 +250,11 @@ export function useMultiSelect(
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
selectCell, |
|
|
|
|
selectBlock, |
|
|
|
|
selectedRange, |
|
|
|
|
clearRangeRows, |
|
|
|
|
startSelectRange, |
|
|
|
|
selected, |
|
|
|
|
selectedRows, |
|
|
|
|
rangeRows, |
|
|
|
|
endSelectRange, |
|
|
|
|
clearSelectedRange: selectedRange.clear.bind(selectedRange), |
|
|
|
|
copyValue, |
|
|
|
|
isCellSelected, |
|
|
|
|
selectedCell, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|