refactor grid
This commit is contained in:
parent
f96c7224a7
commit
ff48c77827
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}> {
|
}> {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export type GridRow = {
|
||||||
|
index: number;
|
||||||
|
ranged: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRow(index: number): GridRow {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
ranged: false,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 },
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue