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

1144 lines
34 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"
2024-01-29 22:59:21 +00:00
:class="[$style.grid, $style.border]"
2024-02-01 11:59:30 +00:00
@mousedown.prevent="onMouseDown"
2024-01-26 13:16:17 +00:00
@keydown="onKeyDown"
2024-02-01 11:59:30 +00:00
@contextmenu.prevent.stop="onContextMenu"
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"
2024-02-03 03:19:37 +00:00
:cells="cells[row.index].cells"
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-02-04 00:24:10 +00:00
import { computed, nextTick, onMounted, ref, toRefs, watch } from 'vue';
2024-02-02 00:32:49 +00:00
import { DataSource, GridEventEmitter, GridSetting, GridState, Size } 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-02-03 02:17:16 +00:00
import { cellValidation } from '@/components/grid/cell-validators.js';
2024-02-02 00:32:49 +00:00
import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js';
2024-02-03 11:10:56 +00:00
import { equalCellAddress, getCellAddress, getCellElement } from '@/components/grid/grid-utils.js';
2024-01-30 23:39:56 +00:00
import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
2024-02-01 11:59:30 +00:00
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
2024-02-02 00:32:49 +00:00
import { ColumnSetting, createColumn, GridColumn } from '@/components/grid/column.js';
import { createRow, GridRow } from '@/components/grid/row.js';
2024-01-22 01:37:44 +00:00
2024-02-03 03:19:37 +00:00
type RowHolder = {
cells: GridCell[],
origin: DataSource,
}
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-02-04 04:44:58 +00:00
rowSelectable: true,
2024-01-29 22:59:21 +00:00
}),
});
2024-02-02 00:32:49 +00:00
const { gridSetting, columnSettings, data } = toRefs(props);
2024-01-22 01:37:44 +00:00
2024-01-27 03:02:50 +00:00
const emit = defineEmits<{
2024-02-01 11:59:30 +00:00
(ev: 'event', event: GridEvent, current: GridCurrentState): void;
2024-01-27 03:02:50 +00:00
}>();
2024-02-02 00:32:49 +00:00
// #region Event Definitions
// region Event Definitions
2024-01-30 00:53:47 +00:00
/**
2024-02-01 11:59:30 +00:00
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}おもにpropsでの伝搬が難しいイベントを伝搬するために使用する
2024-01-30 00:53:47 +00:00
* 子コンポーネント -> 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}の中でサイズ再計算要求サイズ変更が発生するとループとみなされ
2024-01-30 01:07:54 +00:00
* ResizeObserver loop completed with undelivered notifications.という警告が発生するため再計算が完全に終われば通知は発生しなくなるので実際にはループしない
2024-01-30 00:53:47 +00:00
*
* @see {@link onResize}
*/
const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
2024-01-26 13:16:17 +00:00
2024-01-27 03:02:50 +00:00
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
2024-02-02 00:32:49 +00:00
/**
* グリッドの最も上位にある状態
*/
const state = ref<GridState>('normal');
/**
* グリッドの列定義propsで受け取った{@link columnSettings}をもとに{@link refreshColumnsSetting}で再計算される
*/
2024-01-23 06:34:52 +00:00
const columns = ref<GridColumn[]>([]);
2024-02-02 00:32:49 +00:00
/**
* グリッドの行定義propsで受け取った{@link data}をもとに{@link refreshData}で再計算される
*/
2024-01-23 06:34:52 +00:00
const rows = ref<GridRow[]>([]);
2024-02-02 00:32:49 +00:00
/**
* グリッドのセル定義propsで受け取った{@link data}をもとに{@link refreshData}で再計算される
*/
2024-02-03 03:19:37 +00:00
const cells = ref<RowHolder[]>([]);
2024-02-02 00:32:49 +00:00
/**
* mousemoveイベントが発生した際にイベントから取得したセルアドレスを保持するための変数
* セルアドレスが変わった瞬間にイベントを起こしたい時のために前回値として使用する
*/
2024-01-26 13:16:17 +00:00
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
2024-02-02 00:32:49 +00:00
/**
* 編集中のセルのアドレスを保持するための変数
*/
2024-01-26 13:16:17 +00:00
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
2024-02-02 00:32:49 +00:00
/**
* 列の範囲選択をする際の開始地点となるインデックスを保持するための変数
* この開始地点からマウスが動いた地点までの範囲を選択する
*/
2024-01-26 13:16:17 +00:00
const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
2024-02-02 00:32:49 +00:00
/**
* 行の範囲選択をする際の開始地点となるインデックスを保持するための変数
* この開始地点からマウスが動いた地点までの範囲を選択する
*/
2024-01-26 13:16:17 +00:00
const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
2024-02-02 00:32:49 +00:00
/**
* 選択状態のセルを取得するための計算プロパティ選択状態とは{@link GridCell.selected}がtrueのセルのこと
*/
2024-01-26 03:48:29 +00:00
const selectedCell = computed(() => {
2024-02-03 03:19:37 +00:00
const selected = cells.value.flatMap(it => it.cells).filter(it => it.selected);
2024-01-26 03:48:29 +00:00
return selected.length > 0 ? selected[0] : undefined;
});
2024-02-02 00:32:49 +00:00
/**
* 範囲選択状態のセルを取得するための計算プロパティ範囲選択状態とは{@link GridCell.ranged}がtrueのセルのこと
*/
2024-02-03 03:19:37 +00:00
const rangedCells = computed(() => cells.value.flatMap(it => it.cells).filter(it => it.ranged));
2024-02-02 00:32:49 +00:00
/**
* 範囲選択状態のセルの範囲を取得するための計算プロパティ左上のセル番地と右下のセル番地を計算する
*/
2024-01-27 03:02:50 +00:00
const rangedBounds = computed(() => {
const _cells = rangedCells.value;
2024-02-02 00:32:49 +00:00
const _cols = _cells.map(it => it.address.col);
const _rows = _cells.map(it => it.address.row);
2024-02-01 11:59:30 +00:00
2024-01-27 03:02:50 +00:00
const leftTop = {
2024-02-02 00:32:49 +00:00
col: Math.min(..._cols),
row: Math.min(..._rows),
2024-01-27 03:02:50 +00:00
};
const rightBottom = {
2024-02-02 00:32:49 +00:00
col: Math.max(..._cols),
row: Math.max(..._rows),
2024-01-27 03:02:50 +00:00
};
2024-02-01 11:59:30 +00:00
2024-01-27 03:02:50 +00:00
return {
leftTop,
rightBottom,
};
});
2024-02-02 00:32:49 +00:00
/**
* グリッドの中で使用可能なセルの範囲を取得するための計算プロパティ左上のセル番地と右下のセル番地を計算する
*/
2024-01-27 03:02:50 +00:00
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-02-02 00:32:49 +00:00
/**
* 範囲選択状態の行を取得するための計算プロパティ範囲選択状態とは{@link GridRow.ranged}がtrueの行のこと
*/
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
2024-02-02 00:32:49 +00:00
// endregion
// #endregion
2024-02-03 02:17:16 +00:00
watch(columnSettings, refreshColumnsSetting);
watch(data, patchData, { deep: true });
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-02-02 00:32:49 +00:00
// #region Event Handlers
// region Event Handlers
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) {
2024-01-30 01:07:54 +00:00
// 先に状態を変更しておき、再計算要求が複数回走らないようにする
2024-01-30 00:53:47 +00:00
state.value = 'normal';
// 選択状態が狂うかもしれないので解除しておく
2024-02-02 00:32:49 +00:00
unSelectionRangeAll();
2024-01-30 22:25:26 +00:00
// 再計算要求を発行。各セル側で最低限必要な横幅を算出し、emitで返してくるようになっている
2024-01-30 00:53:47 +00:00
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-02-02 00:32:49 +00:00
function emitKeyEvent() {
emitGridEvent({ type: 'keydown', event: ev });
}
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: {
2024-02-02 00:32:49 +00:00
// その他のキーは外部にゆだねる
emitKeyEvent();
2024-01-27 03:02:50 +00:00
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 {
2024-02-01 11:59:30 +00:00
// その他のキーは外部にゆだねる
2024-02-02 00:32:49 +00:00
emitKeyEvent();
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: {
2024-02-02 00:32:49 +00:00
// その他のキーは外部にゆだねる
emitKeyEvent();
2024-01-27 03:02:50 +00:00
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;
}
default: {
2024-02-01 11:59:30 +00:00
// その他のキーは外部にゆだねる
2024-02-02 00:32:49 +00:00
emitKeyEvent();
2024-02-01 11:59:30 +00:00
break;
2024-01-27 03:02:50 +00:00
}
}
2024-01-26 13:16:17 +00:00
}
}
break;
}
}
}
2024-01-23 06:34:52 +00:00
function onMouseDown(ev: MouseEvent) {
2024-01-30 23:39:56 +00:00
switch (ev.button) {
case 0: {
onLeftMouseDown(ev);
break;
}
case 2: {
onRightMouseDown(ev);
break;
}
}
}
function onLeftMouseDown(ev: MouseEvent) {
2024-02-03 09:51:35 +00:00
const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting.value);
2024-01-30 23:39:56 +00:00
if (_DEV_) {
2024-02-01 11:59:30 +00:00
console.log(`[grid][mouse-left] state:${state.value}, button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
2024-01-30 23:39:56 +00:00
}
2024-01-23 06:34:52 +00:00
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)) {
2024-02-02 00:32:49 +00:00
unSelectionRangeAll();
2024-01-26 13:16:17 +00:00
2024-02-03 03:19:37 +00:00
const colCells = cells.value.map(row => row.cells[cellAddress.col]);
2024-01-26 13:16:17 +00:00
selectionRange(...colCells.map(cell => cell.address));
registerMouseUp();
registerMouseMove();
firstSelectionColumnIdx.value = cellAddress.col;
state.value = 'colSelecting';
2024-01-27 03:02:50 +00:00
2024-02-03 11:10:56 +00:00
// フォーカスを当てないとキーイベントが拾えないので
getCellElement(ev.target as HTMLElement)?.focus();
2024-01-26 13:16:17 +00:00
} else if (isRowNumberCellAddress(cellAddress)) {
2024-02-02 00:32:49 +00:00
unSelectionRangeAll();
2024-01-26 13:16:17 +00:00
2024-02-03 03:19:37 +00:00
const rowCells = cells.value[cellAddress.row].cells;
2024-01-26 13:16:17 +00:00
selectionRange(...rowCells.map(cell => cell.address));
2024-02-01 11:59:30 +00:00
expandRowRange(cellAddress.row, cellAddress.row);
2024-01-26 13:16:17 +00:00
registerMouseUp();
registerMouseMove();
firstSelectionRowIdx.value = cellAddress.row;
state.value = 'rowSelecting';
2024-01-27 03:02:50 +00:00
2024-02-03 11:10:56 +00:00
// フォーカスを当てないとキーイベントが拾えないので
getCellElement(ev.target as HTMLElement)?.focus();
2024-01-23 06:34:52 +00:00
}
break;
}
}
}
2024-01-30 23:39:56 +00:00
function onRightMouseDown(ev: MouseEvent) {
2024-02-03 09:51:35 +00:00
const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting.value);
2024-01-30 23:39:56 +00:00
if (_DEV_) {
console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
}
switch (state.value) {
case 'normal': {
if (!availableCellAddress(cellAddress)) {
return;
}
const _rangedCells = [...rangedCells.value];
if (!_rangedCells.some(it => equalCellAddress(it.address, cellAddress))) {
// 範囲選択外を右クリックした場合は、範囲選択を解除(範囲選択内であれば範囲選択を維持する)
selectionCell(cellAddress);
}
break;
}
}
}
2024-01-23 06:34:52 +00:00
function onMouseMove(ev: MouseEvent) {
2024-01-27 03:02:50 +00:00
ev.preventDefault();
2024-02-01 11:59:30 +00:00
2024-02-03 09:51:35 +00:00
const targetCellAddress = getCellAddress(ev.target as HTMLElement, gridSetting.value);
2024-02-01 11:59:30 +00:00
if (equalCellAddress(previousCellAddress.value, targetCellAddress)) {
return;
}
if (_DEV_) {
console.log(`[grid][mouse-move] button: ${ev.button}, cell: ${targetCellAddress.row}x${targetCellAddress.col}`);
}
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;
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': {
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;
2024-02-03 11:10:56 +00:00
// フォーカスを当てないとキーイベントが拾えないので
getCellElement(ev.target as HTMLElement)?.focus();
2024-01-26 13:16:17 +00:00
break;
}
case 'rowSelecting': {
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 = {
2024-02-03 03:19:37 +00:00
col: Math.min(...cells.value.map(it => it.cells.length - 1)),
2024-01-26 13:16:17 +00:00
row: Math.max(targetCellAddress.row, firstSelectionRowIdx.value),
};
unSelectionOutOfRange(leftTop, rightBottom);
2024-01-29 04:41:13 +00:00
expandCellRange(leftTop, rightBottom);
2024-02-04 04:44:58 +00:00
const rangedRowIndexes = [rows.value[targetCellAddress.row].index, ...rangedRows.value.map(it => it.index)];
2024-01-29 04:41:13 +00:00
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-02-03 11:10:56 +00:00
// フォーカスを当てないとキーイベントが拾えないので
getCellElement(ev.target as HTMLElement)?.focus();
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-30 23:39:56 +00:00
function onContextMenu(ev: MouseEvent) {
2024-02-03 09:51:35 +00:00
const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting.value);
2024-01-30 23:39:56 +00:00
if (_DEV_) {
console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
}
2024-02-01 11:59:30 +00:00
const menuItems = Array.of<MenuItem>();
2024-01-30 23:39:56 +00:00
2024-02-01 11:59:30 +00:00
// 外でメニュー項目を挿してもらう
if (availableCellAddress(cellAddress)) {
emitGridEvent({ type: 'cell-context-menu', event: ev, menuItems });
} else if (isRowNumberCellAddress(cellAddress)) {
emitGridEvent({ type: 'row-context-menu', event: ev, menuItems });
} else if (isColumnHeaderCellAddress(cellAddress)) {
emitGridEvent({ type: 'column-context-menu', event: ev, menuItems });
}
2024-01-30 23:39:56 +00:00
if (menuItems.length > 0) {
os.contextMenu(menuItems, ev);
}
}
2024-01-23 06:34:52 +00:00
function onCellEditBegin(sender: GridCell) {
state.value = 'cellEditing';
editingCellAddress.value = sender.address;
2024-02-03 03:19:37 +00:00
for (const cell of cells.value.flatMap(it => it.cells)) {
2024-01-23 06:34:52 +00:00
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-02-01 11:59:30 +00:00
emitCellValue(sender, newValue);
2024-01-26 13:16:17 +00:00
}
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
2024-02-04 00:24:10 +00:00
const _cells = cells.value;
if (_cells.length > sender.address.row && _cells[sender.address.row].cells.length > sender.address.col) {
const currentSize = _cells[sender.address.row].cells[sender.address.col].contentSize;
if (currentSize.width !== contentSize.width || currentSize.height !== contentSize.height) {
_cells[sender.address.row].cells[sender.address.col].contentSize = contentSize;
if (sender.column.setting.width === 'auto') {
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': {
2024-02-04 00:24:10 +00:00
const currentSize = columns.value[sender.index].contentSize;
if (currentSize.width !== newSize.width || currentSize.height !== newSize.height) {
columns.value[sender.index].contentSize = newSize;
if (sender.setting.width === 'auto') {
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-02-02 00:32:49 +00:00
// endregion
// #endregion
// #region Methods
// region Methods
/**
* カラム内のコンテンツを表示しきるために必要な横幅と各セルのコンテンツを表示しきるために必要な横幅を比較し大きい方を列全体の横幅として採用する
*/
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-02-03 03:19:37 +00:00
.map(row => row.cells[column.index])
2024-01-29 02:28:08 +00:00
.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-02-02 00:32:49 +00:00
/**
* {@link emit}を使用してイベントを発行する
*/
2024-02-01 11:59:30 +00:00
function emitGridEvent(ev: GridEvent) {
const currentState: GridCurrentState = {
selectedCell: selectedCell.value,
rangedCells: rangedCells.value,
rangedRows: rangedRows.value,
randedBounds: rangedBounds.value,
availableBounds: availableBounds.value,
state: state.value,
rows: rows.value,
columns: columns.value,
};
emit(
'event',
ev,
// 直接書き換えられると状態が狂う可能性があるのでコピーを渡す
JSON.parse(JSON.stringify(currentState)),
);
}
2024-02-02 00:32:49 +00:00
/**
* 親コンポーネントに新しい値を通知するセル値のバリデーション結果は問わない親コンポーネント側で制御する
*/
2024-02-01 11:59:30 +00:00
function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
2024-01-27 03:02:50 +00:00
const cellAddress = 'address' in sender ? sender.address : sender;
2024-02-03 03:19:37 +00:00
const cell = cells.value[cellAddress.row].cells[cellAddress.col];
2024-01-28 13:10:24 +00:00
2024-02-03 09:51:35 +00:00
const violation = cellValidation(cell, newValue);
cell.violation = violation;
emitGridEvent({
type: 'cell-validation',
violation: violation,
all: cells.value.flatMap(it => it.cells).map(it => it.violation),
});
emitGridEvent({
type: 'cell-value-change',
column: cell.column,
row: cell.row,
violation: violation,
oldValue: cell.value,
newValue: newValue,
});
if (_DEV_) {
console.log(`[grid][cell-value] row:${cell.row}, col:${cell.column.index}, value:${newValue}`);
2024-02-03 04:17:26 +00:00
}
2024-01-23 06:34:52 +00:00
}
2024-02-02 00:32:49 +00:00
/**
* {@link selectedCell}のセル番地を取得する
* いずれかのセルが選択されている状態で呼ばれることを想定しているため選択されていない場合は例外を投げる
*/
function requireSelectionCell(): CellAddress {
const selected = selectedCell.value;
if (!selected) {
throw new Error('No selected cell');
}
return selected.address;
}
/**
* {@link target}のセルを選択状態にする
* その際{@link target}以外の行およびセルの範囲選択状態を解除する
*/
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-02-02 00:32:49 +00:00
unSelectionRangeAll();
2024-01-23 06:34:52 +00:00
2024-01-26 03:48:29 +00:00
const _cells = cells.value;
2024-02-03 03:19:37 +00:00
_cells[target.row].cells[target.col].selected = true;
_cells[target.row].cells[target.col].ranged = true;
2024-01-23 06:34:52 +00:00
}
2024-02-02 00:32:49 +00:00
/**
* {@link targets}のセルを範囲選択状態にする
*/
2024-01-23 06:34:52 +00:00
function selectionRange(...targets: CellAddress[]) {
const _cells = cells.value;
for (const target of targets) {
2024-02-03 03:19:37 +00:00
_cells[target.row].cells[target.col].ranged = true;
2024-01-23 06:34:52 +00:00
}
}
2024-02-02 00:32:49 +00:00
/**
* 行およびセルの範囲選択状態をすべて解除する
*/
function unSelectionRangeAll() {
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-02-02 00:32:49 +00:00
/**
* {@link leftTop}から{@link rightBottom}の範囲外にあるセルを範囲選択状態から外す
*/
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-02-02 00:32:49 +00:00
/**
* {@link leftTop}から{@link rightBottom}の範囲内にあるセルを範囲選択状態にする
*/
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-02-03 03:19:37 +00:00
for (const cell of row.cells.slice(leftTop.col, rightBottom.col + 1)) {
2024-01-23 06:34:52 +00:00
cell.ranged = true;
}
}
}
2024-02-02 00:32:49 +00:00
/**
* {@link top}から{@link bottom}までの行を範囲選択状態にする
*/
2024-01-29 04:41:13 +00:00
function expandRowRange(top: number, bottom: number) {
2024-02-04 04:44:58 +00:00
if (!gridSetting.value.rowSelectable) {
return;
}
2024-01-29 04:41:13 +00:00
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-02-02 00:32:49 +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-22 01:37:44 +00:00
function refreshColumnsSetting() {
const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) {
2024-02-02 00:32:49 +00:00
// 取得元のプロパティ名重複は許容したくない
2024-01-22 01:37:44 +00:00
throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
}
refreshData();
}
function refreshData() {
2024-02-01 11:59:30 +00:00
if (_DEV_) {
2024-02-04 00:24:10 +00:00
console.log('[grid][refresh-data][begin]');
2024-02-01 11:59:30 +00:00
}
2024-01-23 06:34:52 +00:00
const _data: DataSource[] = data.value;
2024-02-03 03:19:37 +00:00
const _rows: GridRow[] = _data.map((it, index) => createRow(index));
2024-02-02 00:32:49 +00:00
const _cols: GridColumn[] = columnSettings.value.map(createColumn);
// 行・列の定義から、元データの配列より値を取得してセルを作成する。
// 行・列の定義はそれぞれインデックスを持っており、そのインデックスは元データの配列番地に対応している。
2024-02-03 03:19:37 +00:00
const _cells: RowHolder[] = _rows.map((row, rowIndex) => (
{
2024-02-03 09:51:35 +00:00
cells: _cols.map(col => {
const cell = createCell(
2024-02-03 03:19:37 +00:00
col,
row,
(col.setting.bindTo in _data[rowIndex]) ? _data[rowIndex][col.setting.bindTo] : undefined,
2024-02-03 09:51:35 +00:00
);
// 元の値の時点で不正な場合もあり得るので、バリデーションを実行してすぐに警告できるようにしておく
cell.violation = cellValidation(cell, cell.value);
return cell;
}),
2024-02-03 03:19:37 +00:00
origin: _data[rowIndex],
}
));
2024-01-22 01:37:44 +00:00
rows.value = _rows;
2024-02-02 00:32:49 +00:00
columns.value = _cols;
2024-01-23 06:34:52 +00:00
cells.value = _cells;
2024-02-04 00:24:10 +00:00
if (_DEV_) {
console.log('[grid][refresh-data][end]');
}
2024-01-22 01:37:44 +00:00
}
2024-02-03 02:17:16 +00:00
/**
* セル値を部分更新するこの関数は外部起因でデータが変更された場合に呼ばれる
*
* 外部起因でデータが変更された場合は{@link data}の値が変更されるが何処の番地がどのように変わったのかまでは検知できない
* セルをすべて作り直せばいいがその手法だと以下のデメリットがある
* - 描画負荷がかかる
* - 各セルが持つ個別の状態選択中状態やバリデーション結果などが失われる
*
2024-02-03 03:59:59 +00:00
* そこで新しい値とセルが持つ値を突き合わせ変更があった場合のみ値を更新しセルそのものは使いまわしつつ値を最新化する
2024-02-03 02:17:16 +00:00
*/
function patchData(newItems: DataSource[]) {
2024-02-04 00:24:10 +00:00
if (_DEV_) {
console.log(`[grid][patch-data][begin] new:${newItems.length} old:${cells.value.length}`);
}
2024-02-03 12:47:07 +00:00
2024-02-04 04:44:58 +00:00
if (cells.value.length != newItems.length) {
const _cells = [...cells.value];
const _rows = [...rows.value];
const _cols = columns.value;
2024-02-03 12:47:07 +00:00
2024-02-03 11:10:56 +00:00
// 状態が壊れるかもしれないので選択を全解除
unSelectionRangeAll();
2024-02-04 00:24:10 +00:00
const oldOrigins = _cells.map(it => it.origin);
2024-02-03 12:47:07 +00:00
const newRows = Array.of<GridRow>();
const newCells = Array.of<RowHolder>();
2024-02-04 00:24:10 +00:00
2024-02-03 12:47:07 +00:00
for (const it of newItems) {
const idx = oldOrigins.indexOf(it);
if (idx >= 0) {
// 既存の行
2024-02-04 00:24:10 +00:00
newRows.push(_rows[idx]);
newCells.push(_cells[idx]);
2024-02-03 12:47:07 +00:00
} else {
// 新規の行
const newRow = createRow(newRows.length);
newRows.push(newRow);
newCells.push({
2024-02-04 00:24:10 +00:00
cells: _cols.map(col => {
2024-02-03 12:47:07 +00:00
const cell = createCell(col, newRow, it[col.setting.bindTo]);
cell.violation = cellValidation(cell, cell.value);
return cell;
}),
origin: it,
});
}
2024-02-03 03:19:37 +00:00
}
2024-02-03 02:17:16 +00:00
2024-02-04 00:24:10 +00:00
// 行数が変わっているので再度番地を振り直す
2024-02-03 12:47:07 +00:00
for (let rowIdx = 0; rowIdx < newCells.length; rowIdx++) {
newRows[rowIdx].index = rowIdx;
for (const cell of newCells[rowIdx].cells) {
2024-02-03 03:19:37 +00:00
cell.address.row = rowIdx;
}
}
2024-02-03 12:47:07 +00:00
rows.value = newRows;
cells.value = newCells;
2024-02-04 04:44:58 +00:00
}
for (let rowIdx = 0; rowIdx < cells.value.length; rowIdx++) {
const oldCells = cells.value[rowIdx].cells;
const newItem = newItems[rowIdx];
for (let colIdx = 0; colIdx < oldCells.length; colIdx++) {
const _col = columns.value[colIdx];
const oldCell = oldCells[colIdx];
const newValue = newItem[_col.setting.bindTo];
if (oldCell.value !== newValue) {
oldCell.violation = cellValidation(oldCell, newValue);
oldCell.value = newValue;
2024-02-03 02:17:16 +00:00
}
}
}
2024-02-04 00:24:10 +00:00
// セル値が書き換わっており、バリデーションの結果も変わっているので外部に通知する必要がある
emitGridEvent({
type: 'cell-validation',
all: cells.value.flatMap(it => it.cells).map(it => it.violation).filter(it => !it.valid),
});
if (_DEV_) {
console.log('[grid][patch-data][end]');
}
2024-02-03 02:17:16 +00:00
}
2024-02-02 00:32:49 +00:00
// endregion
// #endregion
2024-01-26 13:16:17 +00:00
2024-01-30 00:53:47 +00:00
onMounted(() => {
2024-02-03 02:17:16 +00:00
refreshColumnsSetting();
2024-01-30 00:53:47 +00:00
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>