From 8d1a5734cd16bdc7943f92eb0c5b74ddb0ec21b7 Mon Sep 17 00:00:00 2001 From: samunohito <46447427+samunohito@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:16:17 +0900 Subject: [PATCH] wip --- packages/frontend/package.json | 1 - .../src/components/grid/MkDataCell.vue | 67 ++--- .../src/components/grid/MkDataRow.vue | 23 +- .../frontend/src/components/grid/MkGrid.vue | 240 +++++++++++++++--- .../src/components/grid/MkHeaderCell.vue | 43 ++-- .../src/components/grid/MkHeaderRow.vue | 23 +- .../src/components/grid/MkNumberCell.vue | 21 +- .../frontend/src/components/grid/types.ts | 2 +- .../src/pages/admin/custom-emojis-grid.vue | 86 ++++++- pnpm-lock.yaml | 7 - 10 files changed, 357 insertions(+), 156 deletions(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 106c941483..9e88df58a1 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -98,7 +98,6 @@ "@storybook/vue3": "7.6.10", "@storybook/vue3-vite": "7.6.10", "@testing-library/vue": "8.0.1", - "@types/blueimp-load-image": "^5.16.6", "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.5", "@types/matter-js": "0.19.6", diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index 8d3f71034f..12acecbe45 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -54,13 +54,15 @@ import { equalCellAddress, getCellAddress, GridCell, - GridEventEmitter, + GridEventEmitter, Size, } from '@/components/grid/types.js'; const emit = defineEmits<{ - (ev: 'edit:begin', sender: GridCell): void; - (ev: 'edit:end', sender: GridCell): void; - (ev: 'selection:move', sender: GridCell, next: CellAddress): void; + (ev: 'operation:beginEdit', sender: GridCell): void; + (ev: 'operation:endEdit', sender: GridCell): void; + (ev: 'operation:selectionMove', sender: GridCell, next: CellAddress): void; + (ev: 'change:value', sender: GridCell, newValue: CellValue): void; + (ev: 'change:contentSize', sender: GridCell, newSize: Size): void; }>(); const props = defineProps<{ cell: GridCell, @@ -87,10 +89,9 @@ const needsContentCentering = computed(() => { } }); -watch(cellWidth, updateContentSize); watch(() => [cell, cell.value.value], () => { // 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する - nextTick(updateContentSize); + nextTick(emitContentSizeChanged); }); watch(() => cell.value.selected, () => { if (cell.value.selected) { @@ -116,44 +117,13 @@ function onOutsideMouseDown(ev: MouseEvent) { function onCellKeyDown(ev: KeyboardEvent) { if (!editing.value) { + ev.preventDefault(); switch (ev.code) { case 'Enter': case 'F2': { beginEditing(); break; } - case 'ArrowRight': { - const next = { - col: cell.value.address.col + 1, - row: cell.value.address.row, - }; - emit('selection:move', cell.value, next); - break; - } - case 'ArrowLeft': { - const next = { - col: cell.value.address.col - 1, - row: cell.value.address.row, - }; - emit('selection:move', cell.value, next); - break; - } - case 'ArrowUp': { - const next = { - col: cell.value.address.col, - row: cell.value.address.row - 1, - }; - emit('selection:move', cell.value, next); - break; - } - case 'ArrowDown': { - const next = { - col: cell.value.address.col, - row: cell.value.address.row + 1, - }; - emit('selection:move', cell.value, next); - break; - } } } else { switch (ev.code) { @@ -193,9 +163,10 @@ function beginEditing() { editingValue.value = cell.value.value; editing.value = true; registerOutsideMouseDown(); - emit('edit:begin', cell.value); + emit('operation:beginEdit', cell.value); nextTick(() => { + // inputの展開後にフォーカスを当てたい if (inputAreaEl.value) { (inputAreaEl.value.querySelector('*') as HTMLElement).focus(); } @@ -204,7 +175,7 @@ function beginEditing() { } case 'boolean': { // とくに特殊なUIは設けず、トグルするだけ - cell.value.value = !cell.value.value; + emitValueChange(!cell.value.value); break; } } @@ -215,22 +186,28 @@ function endEditing(applyValue: boolean) { return; } - emit('edit:end', cell.value); + emit('operation:endEdit', cell.value); unregisterOutsideMouseDown(); if (applyValue) { - cell.value.value = editingValue.value; + emitValueChange(editingValue.value); } + + editingValue.value = undefined; editing.value = false; rootEl.value?.focus(); } -function updateContentSize() { - cell.value.contentSize = { +function emitValueChange(newValue: CellValue) { + emit('change:value', cell.value, newValue); +} + +function emitContentSizeChanged() { + emit('change:contentSize', cell.value, { width: contentAreaEl.value?.clientWidth ?? 0, height: contentAreaEl.value?.clientHeight ?? 0, - }; + }); } diff --git a/packages/frontend/src/components/grid/MkDataRow.vue b/packages/frontend/src/components/grid/MkDataRow.vue index 393238bdb0..cfa4337ceb 100644 --- a/packages/frontend/src/components/grid/MkDataRow.vue +++ b/packages/frontend/src/components/grid/MkDataRow.vue @@ -4,31 +4,35 @@ :content="(row.index + 1).toString()" :selectable="true" :row="row" - @selection:row="(sender) => emit('selection:row', sender)" + @operation:selectionRow="(sender) => emit('operation:selectionRow', sender)" /> diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index dae28b37b7..f6a7dde603 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -2,18 +2,18 @@ @@ -23,10 +23,12 @@ :row="row" :cells="cells[row.index]" :bus="bus" - @edit:begin="onCellEditBegin" - @edit:end="onCellEditEnd" - @selection:move="onSelectionMove" - @selection:row="onSelectionRow" + @operation:beginEdit="onCellEditBegin" + @operation:endEdit="onCellEditEnd" + @operation:selectionMove="onSelectionMove" + @operation:selectionRow="onSelectionRow" + @change:value="onChangeCellValue" + @change:contentSize="onChangeCellContentSize" />
@@ -38,6 +40,7 @@ import { calcCellWidth, CELL_ADDRESS_NONE, CellAddress, + CellValue, ColumnSetting, DataSource, equalCellAddress, @@ -47,7 +50,7 @@ import { GridEventEmitter, GridRow, GridState, - isCellElement, + Size, } from '@/components/grid/types.js'; import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; @@ -57,21 +60,24 @@ const props = defineProps<{ data: DataSource[] }>(); +const bus = new GridEventEmitter(); + const { columnSettings, data } = toRefs(props); const columns = ref([]); const rows = ref([]); const cells = ref([]); +const previousCellAddress = ref(CELL_ADDRESS_NONE); +const editingCellAddress = ref(CELL_ADDRESS_NONE); +const firstSelectionColumnIdx = ref(CELL_ADDRESS_NONE.col); +const firstSelectionRowIdx = ref(CELL_ADDRESS_NONE.row); +const state = ref('normal'); + const selectedCell = computed(() => { const selected = cells.value.flat().filter(it => it.selected); return selected.length > 0 ? selected[0] : undefined; }); const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged)); -const previousCellAddress = ref(CELL_ADDRESS_NONE); -const editingCellAddress = ref(CELL_ADDRESS_NONE); - -const state = ref('normal'); -const bus = new GridEventEmitter(); watch(columnSettings, refreshColumnsSetting); watch(data, refreshData); @@ -81,6 +87,43 @@ if (_DEV_) { }); } +function onKeyDown(ev: KeyboardEvent) { + switch (state.value) { + case 'normal': { + const selectedCellAddress = selectedCell.value?.address; + if (!selectedCellAddress) { + return; + } + + let next: CellAddress; + switch (ev.code) { + case 'ArrowRight': { + next = { col: selectedCellAddress.col + 1, row: selectedCellAddress.row }; + break; + } + case 'ArrowLeft': { + next = { col: selectedCellAddress.col - 1, row: selectedCellAddress.row }; + break; + } + case 'ArrowUp': { + next = { col: selectedCellAddress.col, row: selectedCellAddress.row - 1 }; + break; + } + case 'ArrowDown': { + next = { col: selectedCellAddress.col, row: selectedCellAddress.row + 1 }; + break; + } + default: { + return; + } + } + + selectionCell(next); + break; + } + } +} + function onMouseDown(ev: MouseEvent) { const cellAddress = getCellAddress(ev.target as HTMLElement); switch (state.value) { @@ -91,20 +134,34 @@ function onMouseDown(ev: MouseEvent) { break; } case 'normal': { + const cellAddress = getCellAddress(ev.target as HTMLElement); if (availableCellAddress(cellAddress)) { selectionCell(cellAddress); - state.value = 'cellSelecting'; - } - break; - } - } -} -function onMouseUp() { - switch (state.value) { - case 'cellSelecting': { - state.value = 'normal'; - previousCellAddress.value = CELL_ADDRESS_NONE; + registerMouseUp(); + registerMouseMove(); + state.value = 'cellSelecting'; + } else if (isColumnHeaderCellAddress(cellAddress)) { + unSelectionRange(); + + const colCells = cells.value.map(row => row[cellAddress.col]); + selectionRange(...colCells.map(cell => cell.address)); + + registerMouseUp(); + registerMouseMove(); + firstSelectionColumnIdx.value = cellAddress.col; + state.value = 'colSelecting'; + } else if (isRowNumberCellAddress(cellAddress)) { + unSelectionRange(); + + const rowCells = cells.value[cellAddress.row]; + selectionRange(...rowCells.map(cell => cell.address)); + + registerMouseUp(); + registerMouseMove(); + firstSelectionRowIdx.value = cellAddress.row; + state.value = 'rowSelecting'; + } break; } } @@ -129,19 +186,70 @@ function onMouseMove(ev: MouseEvent) { row: Math.max(targetCellAddress.row, selectedCellAddress.row), }; - for (const cell of rangedCells.value) { - const outOfRangeCol = cell.address.col < leftTop.col || cell.address.col > rightBottom.col; - const outOfRangeRow = cell.address.row < leftTop.row || cell.address.row > rightBottom.row; - if (outOfRangeCol || outOfRangeRow) { - cell.ranged = false; - } - } - + unSelectionOutOfRange(leftTop, rightBottom); expandRange(leftTop, rightBottom); previousCellAddress.value = targetCellAddress; break; } + case 'colSelecting': { + const targetCellAddress = getCellAddress(ev.target as HTMLElement); + if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) { + return; + } + + const leftTop = { + col: Math.min(targetCellAddress.col, firstSelectionColumnIdx.value), + row: 0, + }; + + const rightBottom = { + col: Math.max(targetCellAddress.col, firstSelectionColumnIdx.value), + row: cells.value.length - 1, + }; + + unSelectionOutOfRange(leftTop, rightBottom); + expandRange(leftTop, rightBottom); + previousCellAddress.value = targetCellAddress; + + break; + } + case 'rowSelecting': { + const targetCellAddress = getCellAddress(ev.target as HTMLElement); + if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) { + return; + } + + const leftTop = { + col: 0, + row: Math.min(targetCellAddress.row, firstSelectionRowIdx.value), + }; + + const rightBottom = { + col: Math.min(...cells.value.map(it => it.length - 1)), + row: Math.max(targetCellAddress.row, firstSelectionRowIdx.value), + }; + + unSelectionOutOfRange(leftTop, rightBottom); + expandRange(leftTop, rightBottom); + previousCellAddress.value = targetCellAddress; + + break; + } + } +} + +function onMouseUp(ev: MouseEvent) { + switch (state.value) { + case 'rowSelecting': + case 'colSelecting': + case 'cellSelecting': { + unregisterMouseUp(); + unregisterMouseMove(); + state.value = 'normal'; + previousCellAddress.value = CELL_ADDRESS_NONE; + break; + } } } @@ -161,6 +269,14 @@ function onCellEditEnd() { state.value = 'normal'; } +function onChangeCellValue(sender: GridCell, newValue: CellValue) { + cells.value[sender.address.row][sender.address.col].value = newValue; +} + +function onChangeCellContentSize(sender: GridCell, contentSize: Size) { + cells.value[sender.address.row][sender.address.col].contentSize = contentSize; +} + function onSelectionMove(_: GridCell, next: CellAddress) { if (availableCellAddress(next)) { selectionCell(next); @@ -185,7 +301,7 @@ function onHeaderCellWidthEndChange(_: GridColumn) { } } -function onHeaderCellWidthChanging(sender: GridColumn, width: string) { +function onHeaderCellChangeWidth(sender: GridColumn, width: string) { switch (state.value) { case 'colResizing': { const column = columns.value[sender.index]; @@ -195,6 +311,15 @@ function onHeaderCellWidthChanging(sender: GridColumn, width: string) { } } +function onHeaderCellChangeContentSize(sender: GridColumn, newSize: Size) { + switch (state.value) { + case 'normal': { + columns.value[sender.index].contentSize = newSize; + break; + } + } +} + function onHeaderCellWidthLargest(sender: GridColumn) { switch (state.value) { case 'normal': { @@ -231,6 +356,10 @@ function onSelectionRow(sender: GridRow) { } function selectionCell(target: CellAddress) { + if (!availableCellAddress(target)) { + return; + } + unSelectionRange(); const _cells = cells.value; @@ -253,6 +382,17 @@ function unSelectionRange() { } } +function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) { + const _cells = rangedCells.value; + for (const cell of _cells) { + const outOfRangeCol = cell.address.col < leftTop.col || cell.address.col > rightBottom.col; + const outOfRangeRow = cell.address.row < leftTop.row || cell.address.row > rightBottom.row; + if (outOfRangeCol || outOfRangeRow) { + cell.ranged = false; + } + } +} + function expandRange(leftTop: CellAddress, rightBottom: CellAddress) { const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1); for (const row of targetRows) { @@ -266,6 +406,14 @@ function availableCellAddress(cellAddress: CellAddress): boolean { return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length; } +function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean { + return cellAddress.row === -1 && cellAddress.col >= 0; +} + +function isRowNumberCellAddress(cellAddress: CellAddress): boolean { + return cellAddress.row >= 0 && cellAddress.col === -1; +} + function refreshColumnsSetting() { const bindToList = columnSettings.value.map(it => it.bindTo); if (new Set(bindToList).size !== columnSettings.value.length) { @@ -316,6 +464,24 @@ function refreshData() { cells.value = _cells; } +function registerMouseMove() { + unregisterMouseMove(); + addEventListener('mousemove', onMouseMove); +} + +function unregisterMouseMove() { + removeEventListener('mousemove', onMouseMove); +} + +function registerMouseUp() { + unregisterMouseUp(); + addEventListener('mouseup', onMouseUp); +} + +function unregisterMouseUp() { + removeEventListener('mouseup', onMouseUp); +} + refreshColumnsSetting(); refreshData(); diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue index 06d3be3886..f3a7d4fc0b 100644 --- a/packages/frontend/src/components/grid/MkHeaderCell.vue +++ b/packages/frontend/src/components/grid/MkHeaderCell.vue @@ -6,7 +6,7 @@ >
-
+
{{ text }}
@@ -22,15 +22,14 @@ diff --git a/packages/frontend/src/components/grid/MkHeaderRow.vue b/packages/frontend/src/components/grid/MkHeaderRow.vue index 7ff1e25b3e..3aa63773a4 100644 --- a/packages/frontend/src/components/grid/MkHeaderRow.vue +++ b/packages/frontend/src/components/grid/MkHeaderRow.vue @@ -10,26 +10,27 @@ :key="column.index" :column="column" :bus="bus" - @width:beginChange="(sender) => emit('width:begin-change', sender)" - @width:endChange="(sender) => emit('width:end-change', sender)" - @width:changing="(sender, width) => emit('width:changing', sender, width)" - @width:largest="(sender) => emit('width:largest', sender)" - @selection:column="(sender) => emit('selection:column', sender)" + @operation:beginWidthChange="(sender) => emit('operation:beginWidthChange', sender)" + @operation:endWidthChange="(sender) => emit('operation:endWidthChange', sender)" + @operation:widthLargest="(sender) => emit('operation:widthLargest', sender)" + @change:width="(sender, width) => emit('change:width', sender, width)" + @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)" /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a90c1bd55..c98b891d27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -905,9 +905,6 @@ importers: '@testing-library/vue': specifier: 8.0.1 version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.15) - '@types/blueimp-load-image': - specifier: ^5.16.6 - version: 5.16.6 '@types/escape-regexp': specifier: 0.0.3 version: 0.0.3 @@ -7876,10 +7873,6 @@ packages: resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} dev: true - /@types/blueimp-load-image@5.16.6: - resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==} - dev: true - /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: