diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index cb2fb818a8..5d358cd38b 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -66,7 +66,7 @@ import { GridEventEmitter, Size } from '@/components/grid/grid.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import * as os from '@/os.js'; import { CellValue, GridCell } from '@/components/grid/cell.js'; -import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js'; +import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js'; const emit = defineEmits<{ diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index f25d846b23..49c7407743 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -43,7 +43,7 @@ import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; import { cellValidation } from '@/components/grid/cell-validators.js'; import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js'; -import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js'; +import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; import { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js'; @@ -774,6 +774,10 @@ function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) { oldValue: cell.value, newValue: newValue, }); + + if (_DEV_) { + console.log(`[grid][cell-value] row:${cell.row}, col:${cell.column.index}, value:${newValue}`); + } } } diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts index a7c299b3a4..d1b3cffdab 100644 --- a/packages/frontend/src/components/grid/column.ts +++ b/packages/frontend/src/components/grid/column.ts @@ -1,6 +1,6 @@ import { CellValidator } from '@/components/grid/cell-validators.js'; import { Size, SizeStyle } from '@/components/grid/grid.js'; -import { calcCellWidth } from '@/components/grid/utils.js'; +import { calcCellWidth } from '@/components/grid/grid-utils.js'; export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image'; diff --git a/packages/frontend/src/components/grid/utils.ts b/packages/frontend/src/components/grid/grid-utils.ts similarity index 95% rename from packages/frontend/src/components/grid/utils.ts rename to packages/frontend/src/components/grid/grid-utils.ts index 909749648a..6106c51cf9 100644 --- a/packages/frontend/src/components/grid/utils.ts +++ b/packages/frontend/src/components/grid/grid-utils.ts @@ -1,4 +1,4 @@ -import { SizeStyle } from '@/components/grid/types.js'; +import { SizeStyle } from '@/components/grid/grid.js'; import { CELL_ADDRESS_NONE, CellAddress } from '@/components/grid/cell.js'; export function isCellElement(elem: any): elem is HTMLTableCellElement { diff --git a/packages/frontend/src/components/grid/optin-utils.ts b/packages/frontend/src/components/grid/optin-utils.ts new file mode 100644 index 0000000000..fa82feae12 --- /dev/null +++ b/packages/frontend/src/components/grid/optin-utils.ts @@ -0,0 +1,146 @@ +import { Ref } from 'vue'; +import { GridCellValueChangeEvent, GridCurrentState, GridKeyDownEvent } from '@/components/grid/grid-event.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { ColumnSetting } from '@/components/grid/column.js'; +import { CellValue } from '@/components/grid/cell.js'; +import { DataSource } from '@/components/grid/grid.js'; + +class OptInGridUtils { + async applyCellValueFromEvent(gridItems: Ref, event: GridCellValueChangeEvent) { + const { row, column, newValue } = event; + gridItems.value[row.index][column.setting.bindTo] = newValue; + } + + async commonKeyDownHandler(gridItems: Ref, event: GridKeyDownEvent, currentState: GridCurrentState) { + const { ctrlKey, shiftKey, code } = event.event; + + switch (true) { + case ctrlKey && shiftKey: { + break; + } + case ctrlKey: { + switch (code) { + case 'KeyC': { + this.rangeCopyToClipboard(gridItems, currentState); + break; + } + case 'KeyV': { + await this.pasteFromClipboard(gridItems, currentState); + break; + } + } + break; + } + case shiftKey: { + break; + } + default: { + switch (code) { + case 'Delete': { + this.deleteSelectionRange(gridItems, currentState); + break; + } + } + break; + } + } + } + + rangeCopyToClipboard(gridItems: Ref, currentState: GridCurrentState) { + const lines = Array.of(); + const bounds = currentState.randedBounds; + + for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { + const items = Array.of(); + for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) { + const bindTo = currentState.columns[col].setting.bindTo; + const cell = gridItems.value[row][bindTo]; + items.push(cell?.toString() ?? ''); + } + lines.push(items.join('\t')); + } + + const text = lines.join('\n'); + copyToClipboard(text); + + if (_DEV_) { + console.log(`Copied to clipboard: ${text}`); + } + } + + async pasteFromClipboard( + gridItems: Ref, + currentState: GridCurrentState, + ) { + function parseValue(value: string, type: ColumnSetting['type']): CellValue { + switch (type) { + case 'number': { + return Number(value); + } + case 'boolean': { + return value === 'true'; + } + default: { + return value; + } + } + } + + const clipBoardText = await navigator.clipboard.readText(); + if (_DEV_) { + console.log(`Paste from clipboard: ${clipBoardText}`); + } + + const bounds = currentState.randedBounds; + const lines = clipBoardText.replace(/\r/g, '') + .split('\n') + .map(it => it.split('\t')); + + if (lines.length === 1 && lines[0].length === 1) { + // 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける + const ranges = currentState.rangedCells; + for (const cell of ranges) { + gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type); + } + } else { + // 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける + const offsetRow = bounds.leftTop.row; + const offsetCol = bounds.leftTop.col; + const columns = currentState.columns; + for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { + const rowIdx = row - offsetRow; + if (lines.length <= rowIdx) { + // クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る + break; + } + + const items = lines[rowIdx]; + for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) { + const colIdx = col - offsetCol; + if (items.length <= colIdx) { + // クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る + break; + } + + gridItems.value[row][columns[col].setting.bindTo] = parseValue(items[colIdx], columns[col].setting.type); + } + } + } + } + + deleteSelectionRange(gridItems: Ref, currentState: GridCurrentState) { + if (currentState.rangedRows.length > 0) { + const deletedIndexes = currentState.rangedRows.map(it => it.index); + gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index)); + } else { + const ranges = currentState.rangedCells; + for (const cell of ranges) { + if (cell.column.setting.editable) { + gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined; + } + } + } + } +} + +export const optInGridUtils = new OptInGridUtils(); diff --git a/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue b/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue index ac9705cf09..fc2970463c 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-grid.register.vue @@ -56,8 +56,8 @@ @@ -88,7 +88,7 @@