refactor grid

This commit is contained in:
samunohito 2024-02-02 09:32:49 +09:00
parent f96c7224a7
commit ff48c77827
14 changed files with 284 additions and 154 deletions

View File

@ -56,10 +56,12 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { CellValue, GridCell } from '@/components/grid/cell.js'; import { CellValue, GridCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js'; import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void; (ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'operation:endEdit', sender: GridCell): void; (ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'operation:validation', sender: GridCell, violation: ValidateViolation): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void; (ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void; (ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>(); }>();
@ -210,7 +212,9 @@ function endEditing(applyValue: boolean) {
} }
function emitValueChange(newValue: CellValue) { function emitValueChange(newValue: CellValue) {
emit('change:value', cell.value, newValue); const _cell = cell.value;
const violation = cellValidation(_cell, newValue);
emit('operation:validation', _cell, violation);
} }
function emitContentSizeChanged() { function emitContentSizeChanged() {

View File

@ -20,10 +20,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { toRefs } from 'vue'; import { toRefs } from 'vue';
import { GridEventEmitter, GridRow, GridSetting, Size } from '@/components/grid/grid.js'; import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import MkDataCell from '@/components/grid/MkDataCell.vue'; import MkDataCell from '@/components/grid/MkDataCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue'; import MkNumberCell from '@/components/grid/MkNumberCell.vue';
import { CellValue, GridCell } from '@/components/grid/cell.js'; import { CellValue, GridCell } from '@/components/grid/cell.js';
import { GridRow } from '@/components/grid/row.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void; (ev: 'operation:beginEdit', sender: GridCell): void;

View File

@ -38,24 +38,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue'; import { computed, onMounted, ref, toRefs, watch } from 'vue';
import { import { DataSource, GridEventEmitter, GridSetting, GridState, Size } from '@/components/grid/grid.js';
ColumnSetting,
DataSource,
GridColumn,
GridEventEmitter,
GridRow,
GridSetting,
GridState,
Size,
} from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import { cellValidation } from '@/components/grid/cell-validators.js'; import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js';
import { calcCellWidth, equalCellAddress, getCellAddress } from '@/components/grid/utils.js'; import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js'; import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
import { ColumnSetting, createColumn, GridColumn } from '@/components/grid/column.js';
import { createRow, GridRow } from '@/components/grid/row.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
gridSetting?: GridSetting, gridSetting?: GridSetting,
@ -66,11 +59,15 @@ const props = withDefaults(defineProps<{
rowNumberVisible: true, rowNumberVisible: true,
}), }),
}); });
const { gridSetting, columnSettings, data } = toRefs(props);
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'event', event: GridEvent, current: GridCurrentState): void; (ev: 'event', event: GridEvent, current: GridCurrentState): void;
}>(); }>();
// #region Event Definitions
// region Event Definitions
/** /**
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}おもにpropsでの伝搬が難しいイベントを伝搬するために使用する * grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}おもにpropsでの伝搬が難しいイベントを伝搬するために使用する
* 子コンポーネント -> gridのイベントでは原則使用せず{@link emit}を使用する * 子コンポーネント -> gridのイベントでは原則使用せず{@link emit}を使用する
@ -87,35 +84,70 @@ const bus = new GridEventEmitter();
*/ */
const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries))); const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
const { gridSetting, columnSettings, data } = toRefs(props);
const rootEl = ref<InstanceType<typeof HTMLTableElement>>(); const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
const columns = ref<GridColumn[]>([]); /**
const rows = ref<GridRow[]>([]); * グリッドの最も上位にある状態
const cells = ref<GridCell[][]>([]); */
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'); const state = ref<GridState>('normal');
/**
* グリッドの列定義propsで受け取った{@link columnSettings}をもとに{@link refreshColumnsSetting}で再計算される
*/
const columns = ref<GridColumn[]>([]);
/**
* グリッドの行定義propsで受け取った{@link data}をもとに{@link refreshData}で再計算される
*/
const rows = ref<GridRow[]>([]);
/**
* グリッドのセル定義propsで受け取った{@link data}をもとに{@link refreshData}で再計算される
*/
const cells = ref<GridCell[][]>([]);
/**
* mousemoveイベントが発生した際にイベントから取得したセルアドレスを保持するための変数
* セルアドレスが変わった瞬間にイベントを起こしたい時のために前回値として使用する
*/
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);
/**
* 選択状態のセルを取得するための計算プロパティ選択状態とは{@link GridCell.selected}がtrueのセルのこと
*/
const selectedCell = computed(() => { const selectedCell = computed(() => {
const selected = cells.value.flat().filter(it => it.selected); const selected = cells.value.flat().filter(it => it.selected);
return selected.length > 0 ? selected[0] : undefined; return selected.length > 0 ? selected[0] : undefined;
}); });
/**
* 範囲選択状態のセルを取得するための計算プロパティ範囲選択状態とは{@link GridCell.ranged}がtrueのセルのこと
*/
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged)); const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
/**
* 範囲選択状態のセルの範囲を取得するための計算プロパティ左上のセル番地と右下のセル番地を計算する
*/
const rangedBounds = computed(() => { const rangedBounds = computed(() => {
const _cells = rangedCells.value; const _cells = rangedCells.value;
const cols = _cells.map(it => it.address.col); const _cols = _cells.map(it => it.address.col);
const rows = _cells.map(it => it.address.row); const _rows = _cells.map(it => it.address.row);
const leftTop = { const leftTop = {
col: Math.min(...cols), col: Math.min(..._cols),
row: Math.min(...rows), row: Math.min(..._rows),
}; };
const rightBottom = { const rightBottom = {
col: Math.max(...cols), col: Math.max(..._cols),
row: Math.max(...rows), row: Math.max(..._rows),
}; };
return { return {
@ -123,6 +155,9 @@ const rangedBounds = computed(() => {
rightBottom, rightBottom,
}; };
}); });
/**
* グリッドの中で使用可能なセルの範囲を取得するための計算プロパティ左上のセル番地と右下のセル番地を計算する
*/
const availableBounds = computed(() => { const availableBounds = computed(() => {
const leftTop = { const leftTop = {
col: 0, col: 0,
@ -134,8 +169,14 @@ const availableBounds = computed(() => {
}; };
return { leftTop, rightBottom }; return { leftTop, rightBottom };
}); });
/**
* 範囲選択状態の行を取得するための計算プロパティ範囲選択状態とは{@link GridRow.ranged}がtrueの行のこと
*/
const rangedRows = computed(() => rows.value.filter(it => it.ranged)); const rangedRows = computed(() => rows.value.filter(it => it.ranged));
// endregion
// #endregion
watch(columnSettings, refreshColumnsSetting, { immediate: true }); watch(columnSettings, refreshColumnsSetting, { immediate: true });
watch(data, refreshData, { immediate: true, deep: true }); watch(data, refreshData, { immediate: true, deep: true });
@ -145,6 +186,9 @@ if (_DEV_) {
}); });
} }
// #region Event Handlers
// region Event Handlers
function onResize(entries: ResizeObserverEntry[]) { function onResize(entries: ResizeObserverEntry[]) {
if (entries.length !== 1 || entries[0].target !== rootEl.value) { if (entries.length !== 1 || entries[0].target !== rootEl.value) {
return; return;
@ -162,7 +206,7 @@ function onResize(entries: ResizeObserverEntry[]) {
state.value = 'normal'; state.value = 'normal';
// //
unSelectionRange(); unSelectionRangeAll();
// emit // emit
bus.emit('forceRefreshContentSize'); bus.emit('forceRefreshContentSize');
@ -179,6 +223,10 @@ function onResize(entries: ResizeObserverEntry[]) {
} }
function onKeyDown(ev: KeyboardEvent) { function onKeyDown(ev: KeyboardEvent) {
function emitKeyEvent() {
emitGridEvent({ type: 'keydown', event: ev });
}
if (_DEV_) { if (_DEV_) {
console.log(`[grid][key] ctrl: ${ev.ctrlKey}, shift: ${ev.shiftKey}, code: ${ev.code}`); console.log(`[grid][key] ctrl: ${ev.ctrlKey}, shift: ${ev.shiftKey}, code: ${ev.code}`);
} }
@ -225,6 +273,8 @@ function onKeyDown(ev: KeyboardEvent) {
break; break;
} }
default: { default: {
//
emitKeyEvent();
return; return;
} }
} }
@ -233,7 +283,7 @@ function onKeyDown(ev: KeyboardEvent) {
expandCellRange(newBounds.leftTop, newBounds.rightBottom); expandCellRange(newBounds.leftTop, newBounds.rightBottom);
} else { } else {
// //
emitGridEvent({ type: 'keydown', event: ev }); emitKeyEvent();
} }
} else { } else {
if (ev.shiftKey) { if (ev.shiftKey) {
@ -311,6 +361,8 @@ function onKeyDown(ev: KeyboardEvent) {
break; break;
} }
default: { default: {
//
emitKeyEvent();
return; return;
} }
} }
@ -342,7 +394,7 @@ function onKeyDown(ev: KeyboardEvent) {
} }
default: { default: {
// //
emitGridEvent({ type: 'keydown', event: ev }); emitKeyEvent();
break; break;
} }
} }
@ -387,7 +439,7 @@ function onLeftMouseDown(ev: MouseEvent) {
registerMouseMove(); registerMouseMove();
state.value = 'cellSelecting'; state.value = 'cellSelecting';
} else if (isColumnHeaderCellAddress(cellAddress)) { } else if (isColumnHeaderCellAddress(cellAddress)) {
unSelectionRange(); unSelectionRangeAll();
const colCells = cells.value.map(row => row[cellAddress.col]); const colCells = cells.value.map(row => row[cellAddress.col]);
selectionRange(...colCells.map(cell => cell.address)); selectionRange(...colCells.map(cell => cell.address));
@ -399,7 +451,7 @@ function onLeftMouseDown(ev: MouseEvent) {
rootEl.value?.focus(); rootEl.value?.focus();
} else if (isRowNumberCellAddress(cellAddress)) { } else if (isRowNumberCellAddress(cellAddress)) {
unSelectionRange(); unSelectionRangeAll();
const rowCells = cells.value[cellAddress.row]; const rowCells = cells.value[cellAddress.row];
selectionRange(...rowCells.map(cell => cell.address)); selectionRange(...rowCells.map(cell => cell.address));
@ -580,6 +632,11 @@ function onCellEditEnd() {
state.value = 'normal'; state.value = 'normal';
} }
function onCellValidation(sender: GridCell, violation: ValidateViolation) {
sender.validation = violation;
emitGridEvent({ type: 'cell-validation', violation });
}
function onChangeCellValue(sender: GridCell, newValue: CellValue) { function onChangeCellValue(sender: GridCell, newValue: CellValue) {
emitCellValue(sender, newValue); emitCellValue(sender, newValue);
} }
@ -640,6 +697,15 @@ function onHeaderCellWidthLargest(sender: GridColumn) {
} }
} }
// endregion
// #endregion
// #region Methods
// region Methods
/**
* カラム内のコンテンツを表示しきるために必要な横幅と各セルのコンテンツを表示しきるために必要な横幅を比較し大きい方を列全体の横幅として採用する
*/
function calcLargestCellWidth(column: GridColumn) { function calcLargestCellWidth(column: GridColumn) {
const _cells = cells.value; const _cells = cells.value;
const largestColumnWidth = columns.value[column.index].contentSize.width; const largestColumnWidth = columns.value[column.index].contentSize.width;
@ -660,6 +726,9 @@ function calcLargestCellWidth(column: GridColumn) {
column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`; column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
} }
/**
* {@link emit}を使用してイベントを発行する
*/
function emitGridEvent(ev: GridEvent) { function emitGridEvent(ev: GridEvent) {
const currentState: GridCurrentState = { const currentState: GridCurrentState = {
selectedCell: selectedCell.value, selectedCell: selectedCell.value,
@ -680,39 +749,27 @@ function emitGridEvent(ev: GridEvent) {
); );
} }
/**
* 親コンポーネントに新しい値を通知するセル値のバリデーション結果は問わない親コンポーネント側で制御する
*/
function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) { function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
const cellAddress = 'address' in sender ? sender.address : sender; const cellAddress = 'address' in sender ? sender.address : sender;
const cell = cells.value[cellAddress.row][cellAddress.col]; const cell = cells.value[cellAddress.row][cellAddress.col];
const violation = cellValidation(cell, newValue);
emitGridEvent({ type: 'cell-validation', violation });
cell.validation = {
valid: violation.valid,
violations: violation.violations.filter(it => !it.valid),
};
emitGridEvent({ emitGridEvent({
type: 'cell-value-change', type: 'cell-value-change',
column: cell.column, column: cell.column,
row: cell.row, row: cell.row,
violation: cell.validation,
oldValue: cell.value, oldValue: cell.value,
newValue: newValue, newValue: newValue,
}); });
} }
function selectionCell(target: CellAddress) { /**
if (!availableCellAddress(target)) { * {@link selectedCell}のセル番地を取得する
return; * いずれかのセルが選択されている状態で呼ばれることを想定しているため選択されていない場合は例外を投げる
} */
unSelectionRange();
const _cells = cells.value;
_cells[target.row][target.col].selected = true;
_cells[target.row][target.col].ranged = true;
}
function requireSelectionCell(): CellAddress { function requireSelectionCell(): CellAddress {
const selected = selectedCell.value; const selected = selectedCell.value;
if (!selected) { if (!selected) {
@ -722,6 +779,25 @@ function requireSelectionCell(): CellAddress {
return selected.address; return selected.address;
} }
/**
* {@link target}のセルを選択状態にする
* その際{@link target}以外の行およびセルの範囲選択状態を解除する
*/
function selectionCell(target: CellAddress) {
if (!availableCellAddress(target)) {
return;
}
unSelectionRangeAll();
const _cells = cells.value;
_cells[target.row][target.col].selected = true;
_cells[target.row][target.col].ranged = true;
}
/**
* {@link targets}のセルを範囲選択状態にする
*/
function selectionRange(...targets: CellAddress[]) { function selectionRange(...targets: CellAddress[]) {
const _cells = cells.value; const _cells = cells.value;
for (const target of targets) { for (const target of targets) {
@ -729,7 +805,10 @@ function selectionRange(...targets: CellAddress[]) {
} }
} }
function unSelectionRange() { /**
* 行およびセルの範囲選択状態をすべて解除する
*/
function unSelectionRangeAll() {
const _cells = rangedCells.value; const _cells = rangedCells.value;
for (const cell of _cells) { for (const cell of _cells) {
cell.selected = false; cell.selected = false;
@ -742,6 +821,9 @@ function unSelectionRange() {
} }
} }
/**
* {@link leftTop}から{@link rightBottom}の範囲外にあるセルを範囲選択状態から外す
*/
function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) { function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
const _cells = rangedCells.value; const _cells = rangedCells.value;
for (const cell of _cells) { for (const cell of _cells) {
@ -758,6 +840,9 @@ function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
} }
} }
/**
* {@link leftTop}から{@link rightBottom}の範囲内にあるセルを範囲選択状態にする
*/
function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) { function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1); const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
for (const row of targetRows) { for (const row of targetRows) {
@ -767,6 +852,9 @@ function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
} }
} }
/**
* {@link top}から{@link bottom}までの行を範囲選択状態にする
*/
function expandRowRange(top: number, bottom: number) { function expandRowRange(top: number, bottom: number) {
const targetRows = rows.value.slice(top, bottom + 1); const targetRows = rows.value.slice(top, bottom + 1);
for (const row of targetRows) { for (const row of targetRows) {
@ -786,65 +874,6 @@ function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col === -1; 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) {
throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
}
refreshData();
}
function refreshData() {
if (_DEV_) {
console.log('[grid][refresh-data]');
}
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => ({
index,
ranged: false,
}));
const _columns: GridColumn[] = columnSettings.value.map((setting, index) => ({
index,
setting,
width: calcCellWidth(setting.width),
contentSize: { width: 0, height: 0 },
}));
const _cells = Array.of<GridCell[]>();
for (const [rowIndex, row] of _rows.entries()) {
const rowCells = Array.of<GridCell>();
for (const [colIndex, column] of _columns.entries()) {
const value = (column.setting.bindTo in _data[rowIndex])
? _data[rowIndex][column.setting.bindTo]
: undefined;
const cell: GridCell = {
address: { col: colIndex, row: rowIndex },
value,
column: column,
row: row,
selected: false,
ranged: false,
contentSize: { width: 0, height: 0 },
validation: {
valid: true,
violations: [],
},
};
rowCells.push(cell);
}
_cells.push(rowCells);
}
rows.value = _rows;
columns.value = _columns;
cells.value = _cells;
}
function registerMouseMove() { function registerMouseMove() {
unregisterMouseMove(); unregisterMouseMove();
addEventListener('mousemove', onMouseMove); addEventListener('mousemove', onMouseMove);
@ -863,6 +892,45 @@ function unregisterMouseUp() {
removeEventListener('mouseup', onMouseUp); removeEventListener('mouseup', onMouseUp);
} }
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() {
if (_DEV_) {
console.log('[grid][refresh-data]');
}
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => createRow(index));
const _cols: GridColumn[] = columnSettings.value.map(createColumn);
//
//
const _cells = _rows.map((row, rowIndex) =>
_cols.map(col =>
createCell(
col,
row,
(col.setting.bindTo in _data[rowIndex]) ? _data[rowIndex][col.setting.bindTo] : undefined,
),
),
);
rows.value = _rows;
columns.value = _cols;
cells.value = _cells;
}
// endregion
// #endregion
onMounted(() => { onMounted(() => {
if (rootEl.value) { if (rootEl.value) {
resizeObserver.observe(rootEl.value); resizeObserver.observe(rootEl.value);

View File

@ -23,7 +23,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'; import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
import { GridColumn, GridEventEmitter, Size } from '@/components/grid/grid.js'; import { GridEventEmitter, Size } from '@/components/grid/grid.js';
import { GridColumn } from '@/components/grid/column.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'operation:beginWidthChange', sender: GridColumn): void; (ev: 'operation:beginWidthChange', sender: GridColumn): void;

View File

@ -20,9 +20,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { GridColumn, GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js'; import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import MkHeaderCell from '@/components/grid/MkHeaderCell.vue'; import MkHeaderCell from '@/components/grid/MkHeaderCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue'; import MkNumberCell from '@/components/grid/MkNumberCell.vue';
import { GridColumn } from '@/components/grid/column.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'operation:beginWidthChange', sender: GridColumn): void; (ev: 'operation:beginWidthChange', sender: GridColumn): void;

View File

@ -7,7 +7,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { GridRow } from '@/components/grid/grid.js';
import { GridRow } from '@/components/grid/row.js';
defineProps<{ defineProps<{
content: string, content: string,

View File

@ -1,5 +1,6 @@
import { GridColumn, GridRow } from '@/components/grid/grid.js';
import { CellValue, GridCell } from '@/components/grid/cell.js'; import { CellValue, GridCell } from '@/components/grid/cell.js';
import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
export type ValidatorParams = { export type ValidatorParams = {
column: GridColumn; column: GridColumn;
@ -14,6 +15,7 @@ export type ValidatorResult = {
export type CellValidator = { export type CellValidator = {
name?: string; name?: string;
ignoreViolation?: boolean;
validate: (params: ValidatorParams) => ValidatorResult; validate: (params: ValidatorParams) => ValidatorResult;
} }

View File

@ -1,5 +1,7 @@
import { ValidateViolationItem } from '@/components/grid/cell-validators.js'; import { ValidateViolation, ValidateViolationItem } from '@/components/grid/cell-validators.js';
import { GridColumn, GridRow, Size } from '@/components/grid/grid.js'; import { Size } from '@/components/grid/grid.js';
import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
export type CellValue = string | boolean | number | undefined | null export type CellValue = string | boolean | number | undefined | null
@ -21,9 +23,30 @@ export type GridCell = {
selected: boolean; selected: boolean;
ranged: boolean; ranged: boolean;
contentSize: Size; contentSize: Size;
validation: { validation: ValidateViolation;
valid: boolean;
violations: ValidateViolationItem[];
}
} }
export function createCell(
column: GridColumn,
row: GridRow,
value: CellValue,
): GridCell {
return {
address: { row: row.index, col: column.index },
value,
column,
row,
selected: false,
ranged: false,
contentSize: { width: 0, height: 0 },
validation: {
valid: true,
params: {
column,
row,
value,
},
violations: [],
},
};
}

View File

@ -0,0 +1,31 @@
import { CellValidator } from '@/components/grid/cell-validators.js';
import { Size, SizeStyle } from '@/components/grid/grid.js';
import { calcCellWidth } from '@/components/grid/utils.js';
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
export type ColumnSetting = {
bindTo: string;
title?: string;
icon?: string;
type: ColumnType;
width: SizeStyle;
editable?: boolean;
validators?: CellValidator[];
};
export type GridColumn = {
index: number;
setting: ColumnSetting;
width: string;
contentSize: Size;
}
export function createColumn(setting: ColumnSetting, index: number): GridColumn {
return {
index,
setting,
width: calcCellWidth(setting.width),
contentSize: { width: 0, height: 0 },
};
}

View File

@ -1,7 +1,9 @@
import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { GridColumn, GridRow, GridState } from '@/components/grid/grid.js'; import { GridState } from '@/components/grid/grid.js';
import { ValidateViolation } from '@/components/grid/cell-validators.js'; import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
export type GridCurrentState = { export type GridCurrentState = {
selectedCell?: GridCell; selectedCell?: GridCell;
@ -34,6 +36,7 @@ export type GridCellValueChangeEvent = {
type: 'cell-value-change'; type: 'cell-value-change';
column: GridColumn; column: GridColumn;
row: GridRow; row: GridRow;
violation: ValidateViolation;
oldValue: CellValue; oldValue: CellValue;
newValue: CellValue; newValue: CellValue;
}; };

View File

@ -1,5 +1,4 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { CellValidator } from '@/components/grid/cell-validators.js';
import { CellValue } from '@/components/grid/cell.js'; import { CellValue } from '@/components/grid/cell.js';
export type GridSetting = { export type GridSetting = {
@ -8,7 +7,15 @@ export type GridSetting = {
export type DataSource = Record<string, CellValue>; export type DataSource = Record<string, CellValue>;
export type GridState = 'normal' | 'cellSelecting' | 'cellEditing' | 'colResizing' | 'colSelecting' | 'rowSelecting' | 'hidden' export type GridState =
'normal' |
'cellSelecting' |
'cellEditing' |
'colResizing' |
'colSelecting' |
'rowSelecting' |
'hidden'
;
export type Size = { export type Size = {
width: number; width: number;
@ -17,30 +24,6 @@ export type Size = {
export type SizeStyle = number | 'auto' | undefined; export type SizeStyle = number | 'auto' | undefined;
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
export type ColumnSetting = {
bindTo: string;
title?: string;
icon?: string;
type: ColumnType;
width: SizeStyle;
editable?: boolean;
validators?: CellValidator[];
};
export type GridColumn = {
index: number;
setting: ColumnSetting;
width: string;
contentSize: Size;
}
export type GridRow = {
index: number;
ranged: boolean;
}
export class GridEventEmitter extends EventEmitter<{ export class GridEventEmitter extends EventEmitter<{
'forceRefreshContentSize': void; 'forceRefreshContentSize': void;
}> { }> {

View File

@ -0,0 +1,11 @@
export type GridRow = {
index: number;
ranged: boolean;
}
export function createRow(index: number): GridRow {
return {
index,
ranged: false,
};
}

View File

@ -34,11 +34,11 @@ import { computed, onMounted, ref, toRefs, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import { required } from '@/components/grid/cell-validators.js'; import { required } from '@/components/grid/cell-validators.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { ColumnSetting } from '@/components/grid/column.js';
const columnSettings: ColumnSetting[] = [ const columnSettings: ColumnSetting[] = [
{ bindTo: 'selected', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, { bindTo: 'selected', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },

View File

@ -90,7 +90,6 @@ import { onMounted, ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting, GridRow } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
@ -111,6 +110,8 @@ import {
} from '@/components/grid/grid-event.js'; } from '@/components/grid/grid-event.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { CellValue } from '@/components/grid/cell.js'; import { CellValue } from '@/components/grid/cell.js';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
type FolderItem = { type FolderItem = {
id?: string; id?: string;