misskey/packages/frontend/src/components/grid/MkGrid.vue

931 lines
25 KiB
Vue
Raw Normal View History

2024-01-22 01:37:44 +00:00
<template>
2024-01-23 06:34:52 +00:00
<table
2024-01-27 03:02:50 +00:00
ref="rootEl"
tabindex="-1"
2024-01-29 22:59:21 +00:00
:class="[$style.grid, $style.border]"
2024-01-23 06:34:52 +00:00
@mousedown="onMouseDown"
2024-01-26 13:16:17 +00:00
@keydown="onKeyDown"
2024-01-23 06:34:52 +00:00
>
<thead>
<MkHeaderRow
:columns="columns"
2024-01-29 22:59:21 +00:00
:gridSetting="gridSetting"
2024-01-23 06:34:52 +00:00
:bus="bus"
2024-01-26 13:16:17 +00:00
@operation:beginWidthChange="onHeaderCellWidthBeginChange"
@operation:endWidthChange="onHeaderCellWidthEndChange"
@operation:widthLargest="onHeaderCellWidthLargest"
@change:width="onHeaderCellChangeWidth"
@change:contentSize="onHeaderCellChangeContentSize"
2024-01-23 06:34:52 +00:00
/>
</thead>
<tbody>
<MkDataRow
v-for="row in rows"
:key="row.index"
:row="row"
:cells="cells[row.index]"
2024-01-29 22:59:21 +00:00
:gridSetting="gridSetting"
2024-01-23 06:34:52 +00:00
:bus="bus"
2024-01-26 13:16:17 +00:00
@operation:beginEdit="onCellEditBegin"
@operation:endEdit="onCellEditEnd"
@change:value="onChangeCellValue"
@change:contentSize="onChangeCellContentSize"
2024-01-23 06:34:52 +00:00
/>
</tbody>
</table>
2024-01-22 01:37:44 +00:00
</template>
<script setup lang="ts">
2024-01-30 00:53:47 +00:00
import { computed, nextTick, onMounted, ref, toRefs, watch } from 'vue';
2024-01-23 06:34:52 +00:00
import {
2024-01-28 13:10:24 +00:00
CellValueChangedEvent,
2024-01-23 06:34:52 +00:00
ColumnSetting,
DataSource,
GridColumn,
GridEventEmitter,
GridRow,
2024-01-29 22:59:21 +00:00
GridSetting,
2024-01-23 06:34:52 +00:00
GridState,
2024-01-26 13:16:17 +00:00
Size,
2024-01-28 13:10:24 +00:00
} from '@/components/grid/grid.js';
2024-01-23 06:34:52 +00:00
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
2024-01-27 03:02:50 +00:00
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
2024-01-28 13:10:24 +00:00
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { calcCellWidth, equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
2024-01-22 01:37:44 +00:00
2024-01-29 22:59:21 +00:00
const props = withDefaults(defineProps<{
gridSetting?: GridSetting,
2024-01-22 01:37:44 +00:00
columnSettings: ColumnSetting[],
data: DataSource[]
2024-01-29 22:59:21 +00:00
}>(), {
gridSetting: () => ({
rowNumberVisible: true,
}),
});
2024-01-22 01:37:44 +00:00
2024-01-27 03:02:50 +00:00
const emit = defineEmits<{
2024-01-28 13:10:24 +00:00
(ev: 'operation:cellValidation', violation: ValidateViolation): void;
2024-01-29 13:52:18 +00:00
(ev: 'operation:rowDeleting', rows: GridRow[]): void;
2024-01-27 03:02:50 +00:00
(ev: 'change:cellValue', event: CellValueChangedEvent): void;
}>();
2024-01-30 00:53:47 +00:00
/**
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}
* 子コンポーネント -> gridのイベントでは原則使用せず{@link emit}を使用する
*/
2024-01-26 13:16:17 +00:00
const bus = new GridEventEmitter();
2024-01-30 00:53:47 +00:00
/**
* テーブルコンポーネントのリサイズイベントを監視するための{@link ResizeObserver}
* 表示切替を検知しサイズの再計算要求を発行するために使用するマウント時にコンテンツが表示されていない場合初手のサイズの自動計算が正常に働かないため
*
* {@link setTimeout}を経由している理由は{@link onResize}の中でサイズ再計算要求サイズ変更が発生するとループとみなされ
* ResizeObserver loop completed with undelivered notifications.という警告が発生するため状態管理してるので実際にはループしない
*
* @see {@link onResize}
*/
const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
2024-01-26 13:16:17 +00:00
2024-01-29 22:59:21 +00:00
const { gridSetting, columnSettings, data } = toRefs(props);
2024-01-23 06:34:52 +00:00
2024-01-27 03:02:50 +00:00
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
2024-01-23 06:34:52 +00:00
const columns = ref<GridColumn[]>([]);
const rows = ref<GridRow[]>([]);
const cells = ref<GridCell[][]>([]);
2024-01-26 13:16:17 +00:00
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
const state = ref<GridState>('normal');
2024-01-26 03:48:29 +00:00
const selectedCell = computed(() => {
const selected = cells.value.flat().filter(it => it.selected);
return selected.length > 0 ? selected[0] : undefined;
});
2024-01-23 06:34:52 +00:00
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
2024-01-27 03:02:50 +00:00
const rangedBounds = computed(() => {
const _cells = rangedCells.value;
const leftTop = {
col: Math.min(..._cells.map(it => it.address.col)),
row: Math.min(..._cells.map(it => it.address.row)),
};
const rightBottom = {
col: Math.max(..._cells.map(it => it.address.col)),
row: Math.max(..._cells.map(it => it.address.row)),
};
return {
leftTop,
rightBottom,
};
});
const availableBounds = computed(() => {
const leftTop = {
col: 0,
row: 0,
};
const rightBottom = {
col: Math.max(...columns.value.map(it => it.index)),
row: Math.max(...rows.value.map(it => it.index)),
};
return { leftTop, rightBottom };
});
2024-01-29 04:41:13 +00:00
const rangedRows = computed(() => rows.value.filter(it => it.ranged));
2024-01-22 01:37:44 +00:00
watch(columnSettings, refreshColumnsSetting);
watch(data, refreshData);
2024-01-27 03:02:50 +00:00
2024-01-23 06:34:52 +00:00
if (_DEV_) {
2024-01-30 00:53:47 +00:00
watch(state, (value, oldValue) => {
console.log(`[grid][state] ${oldValue} -> ${value}`);
2024-01-23 06:34:52 +00:00
});
}
2024-01-30 00:53:47 +00:00
function onResize(entries: ResizeObserverEntry[]) {
if (entries.length !== 1 || entries[0].target !== rootEl.value) {
return;
}
const contentRect = entries[0].contentRect;
if (_DEV_) {
console.log(`[grid][resize] contentRect: ${contentRect.width}x${contentRect.height}`);
}
switch (state.value) {
case 'hidden': {
if (contentRect.width > 0 && contentRect.height > 0) {
state.value = 'normal';
// 選択状態が狂うかもしれないので解除しておく
unSelectionRange();
bus.emit('forceRefreshContentSize');
}
break;
}
default: {
if (contentRect.width === 0 || contentRect.height === 0) {
state.value = 'hidden';
}
break;
}
}
}
2024-01-26 13:16:17 +00:00
function onKeyDown(ev: KeyboardEvent) {
2024-01-27 03:02:50 +00:00
if (_DEV_) {
2024-01-30 00:53:47 +00:00
console.log(`[grid][key] ctrl: ${ev.ctrlKey}, shift: ${ev.shiftKey}, code: ${ev.code}`);
2024-01-27 03:02:50 +00:00
}
2024-01-26 13:16:17 +00:00
switch (state.value) {
case 'normal': {
2024-01-27 03:02:50 +00:00
ev.preventDefault();
if (ev.ctrlKey) {
if (ev.shiftKey) {
// ctrl + shiftキーが押されている場合は選択セルの範囲拡大最大範囲
const selectedCellAddress = requireSelectionCell();
const max = availableBounds.value;
const bounds = rangedBounds.value;
let newBounds: { leftTop: CellAddress, rightBottom: CellAddress };
switch (ev.code) {
case 'ArrowRight': {
newBounds = {
leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row },
rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row },
};
break;
}
case 'ArrowLeft': {
newBounds = {
leftTop: { col: max.leftTop.col, row: bounds.leftTop.row },
rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row },
};
break;
}
case 'ArrowUp': {
newBounds = {
leftTop: { col: bounds.leftTop.col, row: max.leftTop.row },
rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row },
};
break;
}
case 'ArrowDown': {
newBounds = {
leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row },
rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row },
};
break;
}
default: {
return;
}
}
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
2024-01-29 04:41:13 +00:00
expandCellRange(newBounds.leftTop, newBounds.rightBottom);
2024-01-27 03:02:50 +00:00
} else {
switch (ev.code) {
case 'KeyC': {
rangeCopyToClipboard();
break;
}
case 'KeyV': {
pasteFromClipboard();
break;
}
}
2024-01-26 13:16:17 +00:00
}
2024-01-27 03:02:50 +00:00
} else {
if (ev.shiftKey) {
// shiftキーが押されている場合は選択セルの範囲拡大隣のセルまで
const selectedCellAddress = requireSelectionCell();
const bounds = rangedBounds.value;
let newBounds: { leftTop: CellAddress, rightBottom: CellAddress };
switch (ev.code) {
case 'ArrowRight': {
newBounds = {
leftTop: {
col: bounds.leftTop.col < selectedCellAddress.col
? bounds.leftTop.col + 1
: selectedCellAddress.col,
row: bounds.leftTop.row,
},
rightBottom: {
col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col)
? bounds.rightBottom.col + 1
: selectedCellAddress.col,
row: bounds.rightBottom.row,
},
};
break;
}
case 'ArrowLeft': {
newBounds = {
leftTop: {
col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col)
? bounds.leftTop.col - 1
: selectedCellAddress.col,
row: bounds.leftTop.row,
},
rightBottom: {
col: bounds.rightBottom.col > selectedCellAddress.col
? bounds.rightBottom.col - 1
: selectedCellAddress.col,
row: bounds.rightBottom.row,
},
};
break;
}
case 'ArrowUp': {
newBounds = {
leftTop: {
col: bounds.leftTop.col,
row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row)
? bounds.leftTop.row - 1
: selectedCellAddress.row,
},
rightBottom: {
col: bounds.rightBottom.col,
row: bounds.rightBottom.row > selectedCellAddress.row
? bounds.rightBottom.row - 1
: selectedCellAddress.row,
},
};
break;
}
case 'ArrowDown': {
newBounds = {
leftTop: {
col: bounds.leftTop.col,
row: bounds.leftTop.row < selectedCellAddress.row
? bounds.leftTop.row + 1
: selectedCellAddress.row,
},
rightBottom: {
col: bounds.rightBottom.col,
row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row)
? bounds.rightBottom.row + 1
: selectedCellAddress.row,
},
};
break;
}
default: {
return;
}
}
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
2024-01-29 04:41:13 +00:00
expandCellRange(newBounds.leftTop, newBounds.rightBottom);
2024-01-27 03:02:50 +00:00
} else {
// shiftキーもctrlキーが押されていない場合
switch (ev.code) {
case 'ArrowRight': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row });
break;
}
case 'ArrowLeft': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row });
break;
}
case 'ArrowUp': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 });
break;
}
case 'ArrowDown': {
const selectedCellAddress = requireSelectionCell();
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
break;
}
case 'Delete': {
2024-01-29 13:52:18 +00:00
if (rangedRows.value.length > 0) {
emit('operation:rowDeleting', [...rangedRows.value]);
} else {
const ranges = rangedCells.value;
for (const range of ranges) {
range.value = undefined;
}
2024-01-27 03:02:50 +00:00
}
break;
}
default: {
return;
}
}
2024-01-26 13:16:17 +00:00
}
}
break;
}
}
}
2024-01-23 06:34:52 +00:00
function onMouseDown(ev: MouseEvent) {
const cellAddress = getCellAddress(ev.target as HTMLElement);
switch (state.value) {
case 'cellEditing': {
if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
selectionCell(cellAddress);
}
break;
}
case 'normal': {
if (availableCellAddress(cellAddress)) {
selectionCell(cellAddress);
2024-01-26 13:16:17 +00:00
registerMouseUp();
registerMouseMove();
2024-01-23 06:34:52 +00:00
state.value = 'cellSelecting';
2024-01-26 13:16:17 +00:00
} 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';
2024-01-27 03:02:50 +00:00
rootEl.value?.focus();
2024-01-26 13:16:17 +00:00
} 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';
2024-01-27 03:02:50 +00:00
rootEl.value?.focus();
2024-01-23 06:34:52 +00:00
}
break;
}
}
}
function onMouseMove(ev: MouseEvent) {
2024-01-27 03:02:50 +00:00
ev.preventDefault();
2024-01-23 06:34:52 +00:00
switch (state.value) {
case 'cellSelecting': {
2024-01-26 03:48:29 +00:00
const selectedCellAddress = selectedCell.value?.address;
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (equalCellAddress(previousCellAddress.value, targetCellAddress) || !availableCellAddress(targetCellAddress) || !selectedCellAddress) {
return;
}
const leftTop = {
col: Math.min(targetCellAddress.col, selectedCellAddress.col),
row: Math.min(targetCellAddress.row, selectedCellAddress.row),
};
const rightBottom = {
col: Math.max(targetCellAddress.col, selectedCellAddress.col),
row: Math.max(targetCellAddress.row, selectedCellAddress.row),
};
2024-01-26 13:16:17 +00:00
unSelectionOutOfRange(leftTop, rightBottom);
2024-01-29 04:41:13 +00:00
expandCellRange(leftTop, rightBottom);
2024-01-26 13:16:17 +00:00
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);
2024-01-29 04:41:13 +00:00
expandCellRange(leftTop, rightBottom);
2024-01-26 13:16:17 +00:00
previousCellAddress.value = targetCellAddress;
break;
}
case 'rowSelecting': {
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
return;
2024-01-23 06:34:52 +00:00
}
2024-01-26 13:16:17 +00:00
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);
2024-01-29 04:41:13 +00:00
expandCellRange(leftTop, rightBottom);
rows.value[targetCellAddress.row].ranged = true;
const rangedRowIndexes = rangedRows.value.map(it => it.index);
expandRowRange(Math.min(...rangedRowIndexes), Math.max(...rangedRowIndexes));
2024-01-28 23:12:11 +00:00
2024-01-26 03:48:29 +00:00
previousCellAddress.value = targetCellAddress;
2024-01-23 06:34:52 +00:00
break;
}
}
}
2024-01-26 13:16:17 +00:00
function onMouseUp(ev: MouseEvent) {
2024-01-27 03:02:50 +00:00
ev.preventDefault();
2024-01-26 13:16:17 +00:00
switch (state.value) {
case 'rowSelecting':
case 'colSelecting':
case 'cellSelecting': {
unregisterMouseUp();
unregisterMouseMove();
state.value = 'normal';
previousCellAddress.value = CELL_ADDRESS_NONE;
break;
}
}
}
2024-01-23 06:34:52 +00:00
function onCellEditBegin(sender: GridCell) {
state.value = 'cellEditing';
editingCellAddress.value = sender.address;
for (const cell of cells.value.flat()) {
if (cell.address.col !== sender.address.col || cell.address.row !== sender.address.row) {
// 編集状態となったセル以外は全部選択解除
cell.selected = false;
}
}
}
function onCellEditEnd() {
editingCellAddress.value = CELL_ADDRESS_NONE;
state.value = 'normal';
}
2024-01-26 13:16:17 +00:00
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
2024-01-27 03:02:50 +00:00
setCellValue(sender, newValue);
2024-01-26 13:16:17 +00:00
}
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
cells.value[sender.address.row][sender.address.col].contentSize = contentSize;
2024-01-29 02:28:08 +00:00
if (sender.column.setting.width === 'auto') {
2024-01-30 00:53:47 +00:00
calcLargestCellWidth(sender.column);
2024-01-29 02:28:08 +00:00
}
2024-01-26 13:16:17 +00:00
}
2024-01-27 03:02:50 +00:00
function onHeaderCellWidthBeginChange() {
2024-01-23 06:34:52 +00:00
switch (state.value) {
case 'normal': {
state.value = 'colResizing';
break;
}
}
}
2024-01-27 03:02:50 +00:00
function onHeaderCellWidthEndChange() {
2024-01-23 06:34:52 +00:00
switch (state.value) {
case 'colResizing': {
state.value = 'normal';
break;
}
}
}
2024-01-26 13:16:17 +00:00
function onHeaderCellChangeWidth(sender: GridColumn, width: string) {
2024-01-23 06:34:52 +00:00
switch (state.value) {
case 'colResizing': {
const column = columns.value[sender.index];
column.width = width;
break;
}
}
}
2024-01-26 13:16:17 +00:00
function onHeaderCellChangeContentSize(sender: GridColumn, newSize: Size) {
switch (state.value) {
case 'normal': {
columns.value[sender.index].contentSize = newSize;
2024-01-29 02:28:08 +00:00
if (sender.setting.width === 'auto') {
2024-01-30 00:53:47 +00:00
calcLargestCellWidth(sender);
2024-01-29 02:28:08 +00:00
}
2024-01-26 13:16:17 +00:00
break;
}
}
}
2024-01-23 06:34:52 +00:00
function onHeaderCellWidthLargest(sender: GridColumn) {
switch (state.value) {
case 'normal': {
2024-01-30 00:53:47 +00:00
calcLargestCellWidth(sender);
2024-01-23 06:34:52 +00:00
break;
}
}
}
2024-01-30 00:53:47 +00:00
function calcLargestCellWidth(column: GridColumn) {
2024-01-29 02:28:08 +00:00
const _cells = cells.value;
const largestColumnWidth = columns.value[column.index].contentSize.width;
const largestCellWidth = (_cells.length > 0)
2024-01-29 04:41:13 +00:00
? _cells
2024-01-29 02:28:08 +00:00
.map(row => row[column.index])
.reduce(
(acc, value) => Math.max(acc, value.contentSize.width),
0,
)
: 0;
2024-01-30 00:53:47 +00:00
if (_DEV_) {
console.log(`[grid][calc-largest] idx:${column.setting.bindTo}, col:${largestColumnWidth}, cell:${largestCellWidth}`);
}
2024-01-29 02:28:08 +00:00
column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
}
2024-01-27 03:02:50 +00:00
function setCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
const cellAddress = 'address' in sender ? sender.address : sender;
2024-01-28 13:10:24 +00:00
const cell = cells.value[cellAddress.row][cellAddress.col];
const violation = cellValidation(cell, newValue);
emit('operation:cellValidation', violation);
cell.validation = {
valid: violation.valid,
violations: violation.violations.filter(it => !it.valid),
};
cell.value = newValue;
2024-01-27 03:02:50 +00:00
emit('change:cellValue', {
2024-01-28 13:10:24 +00:00
column: cell.column,
row: cell.row,
2024-01-27 03:02:50 +00:00
value: newValue,
});
2024-01-23 06:34:52 +00:00
}
function selectionCell(target: CellAddress) {
2024-01-26 13:16:17 +00:00
if (!availableCellAddress(target)) {
return;
}
2024-01-26 03:48:29 +00:00
unSelectionRange();
2024-01-23 06:34:52 +00:00
2024-01-26 03:48:29 +00:00
const _cells = cells.value;
2024-01-23 06:34:52 +00:00
_cells[target.row][target.col].selected = true;
_cells[target.row][target.col].ranged = true;
}
2024-01-27 03:02:50 +00:00
function requireSelectionCell(): CellAddress {
const selected = selectedCell.value;
if (!selected) {
throw new Error('No selected cell');
}
return selected.address;
}
2024-01-23 06:34:52 +00:00
function selectionRange(...targets: CellAddress[]) {
const _cells = cells.value;
for (const target of targets) {
_cells[target.row][target.col].ranged = true;
}
}
function unSelectionRange() {
2024-01-26 03:48:29 +00:00
const _cells = rangedCells.value;
for (const cell of _cells) {
cell.selected = false;
cell.ranged = false;
2024-01-23 06:34:52 +00:00
}
2024-01-29 04:41:13 +00:00
const _rows = rows.value;
for (const row of _rows) {
row.ranged = false;
}
2024-01-23 06:34:52 +00:00
}
2024-01-26 13:16:17 +00:00
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;
}
}
2024-01-29 04:41:13 +00:00
const outOfRangeRows = rows.value.filter((_, index) => index < leftTop.row || index > rightBottom.row);
for (const row of outOfRangeRows) {
row.ranged = false;
}
2024-01-26 13:16:17 +00:00
}
2024-01-29 04:41:13 +00:00
function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
2024-01-26 03:48:29 +00:00
const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
2024-01-23 06:34:52 +00:00
for (const row of targetRows) {
2024-01-26 03:48:29 +00:00
for (const cell of row.slice(leftTop.col, rightBottom.col + 1)) {
2024-01-23 06:34:52 +00:00
cell.ranged = true;
}
}
}
2024-01-29 04:41:13 +00:00
function expandRowRange(top: number, bottom: number) {
const targetRows = rows.value.slice(top, bottom + 1);
for (const row of targetRows) {
row.ranged = true;
}
}
2024-01-23 06:34:52 +00:00
function availableCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length;
}
2024-01-22 01:37:44 +00:00
2024-01-26 13:16:17 +00:00
function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row === -1 && cellAddress.col >= 0;
}
function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col === -1;
}
2024-01-27 03:02:50 +00:00
function rangeCopyToClipboard() {
const lines = Array.of<string>();
const bounds = rangedBounds.value;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const items = Array.of<string>();
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const cell = cells.value[row][col];
items.push(cell.value?.toString() ?? '');
}
lines.push(items.join('\t'));
}
const text = lines.join('\n');
copyToClipboard(text);
}
async function pasteFromClipboard() {
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();
const bounds = rangedBounds.value;
const lines = clipBoardText.replace(/\r/g, '')
.split('\n')
.map(it => it.split('\t'));
if (lines.length === 1 && lines[0].length === 1) {
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
const ranges = rangedCells.value;
for (const cell of ranges) {
setCellValue(cell, parseValue(lines[0][0], cell.column.setting.type));
}
} else {
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
const offsetRow = bounds.leftTop.row;
const offsetCol = bounds.leftTop.col;
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;
}
setCellValue(cells.value[row][col], parseValue(items[colIdx], cells.value[row][col].column.setting.type));
}
}
}
}
2024-01-22 01:37:44 +00:00
function refreshColumnsSetting() {
const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) {
throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
}
refreshData();
}
function refreshData() {
2024-01-23 06:34:52 +00:00
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => ({
index,
2024-01-29 13:52:18 +00:00
ranged: false,
2024-01-23 06:34:52 +00:00
}));
const _columns: GridColumn[] = columnSettings.value.map((setting, index) => ({
index,
setting,
width: calcCellWidth(setting.width),
contentSize: { width: 0, height: 0 },
}));
const _cells = Array.of<GridCell[]>();
2024-01-22 01:37:44 +00:00
for (const [rowIndex, row] of _rows.entries()) {
2024-01-23 06:34:52 +00:00
const rowCells = Array.of<GridCell>();
2024-01-22 01:37:44 +00:00
for (const [colIndex, column] of _columns.entries()) {
2024-01-23 06:34:52 +00:00
const value = (column.setting.bindTo in _data[rowIndex])
? _data[rowIndex][column.setting.bindTo]
: undefined;
2024-01-22 01:37:44 +00:00
2024-01-23 06:34:52 +00:00
const cell: GridCell = {
2024-01-22 01:37:44 +00:00
address: { col: colIndex, row: rowIndex },
value,
2024-01-23 06:34:52 +00:00
column: column,
row: row,
selected: false,
ranged: false,
contentSize: { width: 0, height: 0 },
2024-01-28 13:10:24 +00:00
validation: {
valid: true,
violations: [],
},
2024-01-22 01:37:44 +00:00
};
2024-01-23 06:34:52 +00:00
rowCells.push(cell);
2024-01-22 01:37:44 +00:00
}
2024-01-23 06:34:52 +00:00
_cells.push(rowCells);
2024-01-22 01:37:44 +00:00
}
rows.value = _rows;
columns.value = _columns;
2024-01-23 06:34:52 +00:00
cells.value = _cells;
2024-01-22 01:37:44 +00:00
}
2024-01-26 13:16:17 +00:00
function registerMouseMove() {
unregisterMouseMove();
addEventListener('mousemove', onMouseMove);
}
function unregisterMouseMove() {
removeEventListener('mousemove', onMouseMove);
}
function registerMouseUp() {
unregisterMouseUp();
addEventListener('mouseup', onMouseUp);
}
function unregisterMouseUp() {
removeEventListener('mouseup', onMouseUp);
}
2024-01-30 00:53:47 +00:00
onMounted(() => {
refreshColumnsSetting();
refreshData();
if (rootEl.value) {
resizeObserver.observe(rootEl.value);
2024-01-22 01:37:44 +00:00
2024-01-30 00:53:47 +00:00
// 初期表示時にコンテンツが表示されていない場合はhidden状態にしておく。
// コンテンツ表示時にresizeイベントが発生するが、そのときにhidden状態にしておかないとサイズの再計算が走らないので
const bounds = rootEl.value.getBoundingClientRect();
if (bounds.width === 0 || bounds.height === 0) {
state.value = 'hidden';
}
}
});
2024-01-22 01:37:44 +00:00
</script>
<style module lang="scss">
2024-01-29 22:59:21 +00:00
$borderSetting: solid 0.5px var(--divider);
$borderRadius: var(--radius);
2024-01-22 01:37:44 +00:00
.grid {
overflow: scroll;
2024-01-23 06:34:52 +00:00
table-layout: fixed;
user-select: none;
2024-01-29 22:59:21 +00:00
}
2024-01-22 01:37:44 +00:00
2024-01-29 22:59:21 +00:00
.border {
2024-01-23 06:34:52 +00:00
border-spacing: 0;
2024-01-29 22:59:21 +00:00
thead {
tr {
th {
border-left: $borderSetting;
border-top: $borderSetting;
&:first-child {
// 左上セル
border-top-left-radius: $borderRadius;
}
&:last-child {
// 右上セル
border-top-right-radius: $borderRadius;
border-right: $borderSetting;
}
}
}
}
tbody {
tr {
td, th {
border-left: $borderSetting;
border-top: $borderSetting;
&:last-child {
// 一番右端の列
border-right: $borderSetting;
}
}
&:last-child {
td, th {
// 一番下の行
border-bottom: $borderSetting;
&:first-child {
// 左下セル
border-bottom-left-radius: $borderRadius;
}
&:last-child {
// 右下セル
border-bottom-right-radius: $borderRadius;
}
}
}
}
}
2024-01-22 01:37:44 +00:00
}
</style>