イベントの整理
This commit is contained in:
parent
777920d739
commit
f96c7224a7
|
@ -99,6 +99,10 @@ watch(() => cell.value.selected, () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => cell.value.value, (newValue, oldValue) => {
|
||||||
|
emitValueChange(newValue);
|
||||||
|
});
|
||||||
|
|
||||||
function onCellDoubleClick(ev: MouseEvent) {
|
function onCellDoubleClick(ev: MouseEvent) {
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case 'dblclick': {
|
case 'dblclick': {
|
||||||
|
@ -119,6 +123,7 @@ function onCellKeyDown(ev: KeyboardEvent) {
|
||||||
if (!editing.value) {
|
if (!editing.value) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
switch (ev.code) {
|
switch (ev.code) {
|
||||||
|
case 'NumpadEnter':
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
case 'F2': {
|
case 'F2': {
|
||||||
beginEditing();
|
beginEditing();
|
||||||
|
@ -131,6 +136,7 @@ function onCellKeyDown(ev: KeyboardEvent) {
|
||||||
endEditing(false);
|
endEditing(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'NumpadEnter':
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
if (!ev.isComposing) {
|
if (!ev.isComposing) {
|
||||||
endEditing(true);
|
endEditing(true);
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
ref="rootEl"
|
ref="rootEl"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:class="[$style.grid, $style.border]"
|
:class="[$style.grid, $style.border]"
|
||||||
@mousedown="onMouseDown"
|
@mousedown.prevent="onMouseDown"
|
||||||
@keydown="onKeyDown"
|
@keydown="onKeyDown"
|
||||||
@contextmenu="onContextMenu"
|
@contextmenu.prevent.stop="onContextMenu"
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<MkHeaderRow
|
<MkHeaderRow
|
||||||
|
@ -39,7 +39,6 @@
|
||||||
<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 {
|
||||||
CellValueChangedEvent,
|
|
||||||
ColumnSetting,
|
ColumnSetting,
|
||||||
DataSource,
|
DataSource,
|
||||||
GridColumn,
|
GridColumn,
|
||||||
|
@ -51,12 +50,12 @@ import {
|
||||||
} from '@/components/grid/grid.js';
|
} 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 copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import { cellValidation } from '@/components/grid/cell-validators.js';
|
||||||
import { cellValidation, 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, GridCell } from '@/components/grid/cell.js';
|
||||||
import { calcCellWidth, equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
|
import { calcCellWidth, 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';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
gridSetting?: GridSetting,
|
gridSetting?: GridSetting,
|
||||||
|
@ -69,14 +68,11 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'operation:cellValidation', violation: ValidateViolation): void;
|
(ev: 'event', event: GridEvent, current: GridCurrentState): void;
|
||||||
(ev: 'operation:rowDeleting', rows: GridRow[]): void;
|
|
||||||
(ev: 'operation:cellContextMenu', cells: GridCell[], menuItems: MenuItem[]): void;
|
|
||||||
(ev: 'change:cellValue', event: CellValueChangedEvent): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}。
|
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}。おもにpropsでの伝搬が難しいイベントを伝搬するために使用する。
|
||||||
* 子コンポーネント -> gridのイベントでは原則使用せず、{@link emit}を使用する。
|
* 子コンポーネント -> gridのイベントでは原則使用せず、{@link emit}を使用する。
|
||||||
*/
|
*/
|
||||||
const bus = new GridEventEmitter();
|
const bus = new GridEventEmitter();
|
||||||
|
@ -110,14 +106,18 @@ const selectedCell = computed(() => {
|
||||||
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 rows = _cells.map(it => it.address.row);
|
||||||
|
|
||||||
const leftTop = {
|
const leftTop = {
|
||||||
col: Math.min(..._cells.map(it => it.address.col)),
|
col: Math.min(...cols),
|
||||||
row: Math.min(..._cells.map(it => it.address.row)),
|
row: Math.min(...rows),
|
||||||
};
|
};
|
||||||
const rightBottom = {
|
const rightBottom = {
|
||||||
col: Math.max(..._cells.map(it => it.address.col)),
|
col: Math.max(...cols),
|
||||||
row: Math.max(..._cells.map(it => it.address.row)),
|
row: Math.max(...rows),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
leftTop,
|
leftTop,
|
||||||
rightBottom,
|
rightBottom,
|
||||||
|
@ -136,8 +136,8 @@ const availableBounds = computed(() => {
|
||||||
});
|
});
|
||||||
const rangedRows = computed(() => rows.value.filter(it => it.ranged));
|
const rangedRows = computed(() => rows.value.filter(it => it.ranged));
|
||||||
|
|
||||||
watch(columnSettings, refreshColumnsSetting);
|
watch(columnSettings, refreshColumnsSetting, { immediate: true });
|
||||||
watch(data, refreshData);
|
watch(data, refreshData, { immediate: true, deep: true });
|
||||||
|
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
watch(state, (value, oldValue) => {
|
watch(state, (value, oldValue) => {
|
||||||
|
@ -232,16 +232,8 @@ function onKeyDown(ev: KeyboardEvent) {
|
||||||
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
|
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
|
||||||
expandCellRange(newBounds.leftTop, newBounds.rightBottom);
|
expandCellRange(newBounds.leftTop, newBounds.rightBottom);
|
||||||
} else {
|
} else {
|
||||||
switch (ev.code) {
|
// その他のキーは外部にゆだねる
|
||||||
case 'KeyC': {
|
emitGridEvent({ type: 'keydown', event: ev });
|
||||||
rangeCopyToClipboard();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KeyV': {
|
|
||||||
pasteFromClipboard();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
|
@ -348,19 +340,10 @@ function onKeyDown(ev: KeyboardEvent) {
|
||||||
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
|
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'Delete': {
|
|
||||||
if (rangedRows.value.length > 0) {
|
|
||||||
emit('operation:rowDeleting', [...rangedRows.value]);
|
|
||||||
} else {
|
|
||||||
const ranges = rangedCells.value;
|
|
||||||
for (const range of ranges) {
|
|
||||||
range.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
default: {
|
||||||
return;
|
// その他のキーは外部にゆだねる
|
||||||
|
emitGridEvent({ type: 'keydown', event: ev });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -386,11 +369,9 @@ function onMouseDown(ev: MouseEvent) {
|
||||||
function onLeftMouseDown(ev: MouseEvent) {
|
function onLeftMouseDown(ev: MouseEvent) {
|
||||||
const cellAddress = getCellAddress(ev.target as HTMLElement);
|
const cellAddress = getCellAddress(ev.target as HTMLElement);
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.log(`[grid][mouse-left] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
|
console.log(`[grid][mouse-left] state:${state.value}, button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
switch (state.value) {
|
switch (state.value) {
|
||||||
case 'cellEditing': {
|
case 'cellEditing': {
|
||||||
if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
|
if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
|
||||||
|
@ -423,6 +404,8 @@ function onLeftMouseDown(ev: MouseEvent) {
|
||||||
const rowCells = cells.value[cellAddress.row];
|
const rowCells = cells.value[cellAddress.row];
|
||||||
selectionRange(...rowCells.map(cell => cell.address));
|
selectionRange(...rowCells.map(cell => cell.address));
|
||||||
|
|
||||||
|
expandRowRange(cellAddress.row, cellAddress.row);
|
||||||
|
|
||||||
registerMouseUp();
|
registerMouseUp();
|
||||||
registerMouseMove();
|
registerMouseMove();
|
||||||
firstSelectionRowIdx.value = cellAddress.row;
|
firstSelectionRowIdx.value = cellAddress.row;
|
||||||
|
@ -441,8 +424,6 @@ function onRightMouseDown(ev: MouseEvent) {
|
||||||
console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
|
console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
switch (state.value) {
|
switch (state.value) {
|
||||||
case 'normal': {
|
case 'normal': {
|
||||||
if (!availableCellAddress(cellAddress)) {
|
if (!availableCellAddress(cellAddress)) {
|
||||||
|
@ -462,10 +443,19 @@ function onRightMouseDown(ev: MouseEvent) {
|
||||||
|
|
||||||
function onMouseMove(ev: MouseEvent) {
|
function onMouseMove(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
|
||||||
|
if (equalCellAddress(previousCellAddress.value, targetCellAddress)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log(`[grid][mouse-move] button: ${ev.button}, cell: ${targetCellAddress.row}x${targetCellAddress.col}`);
|
||||||
|
}
|
||||||
|
|
||||||
switch (state.value) {
|
switch (state.value) {
|
||||||
case 'cellSelecting': {
|
case 'cellSelecting': {
|
||||||
const selectedCellAddress = selectedCell.value?.address;
|
const selectedCellAddress = selectedCell.value?.address;
|
||||||
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
|
|
||||||
if (equalCellAddress(previousCellAddress.value, targetCellAddress) || !availableCellAddress(targetCellAddress) || !selectedCellAddress) {
|
if (equalCellAddress(previousCellAddress.value, targetCellAddress) || !availableCellAddress(targetCellAddress) || !selectedCellAddress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -487,7 +477,6 @@ function onMouseMove(ev: MouseEvent) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'colSelecting': {
|
case 'colSelecting': {
|
||||||
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
|
|
||||||
if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) {
|
if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -509,7 +498,6 @@ function onMouseMove(ev: MouseEvent) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'rowSelecting': {
|
case 'rowSelecting': {
|
||||||
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
|
|
||||||
if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
|
if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -560,24 +548,17 @@ function onContextMenu(ev: MouseEvent) {
|
||||||
console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
|
console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!availableCellAddress(cellAddress)) {
|
const menuItems = Array.of<MenuItem>();
|
||||||
return;
|
|
||||||
|
// 外でメニュー項目を挿してもらう
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
const _rangedCells = [...rangedCells.value];
|
|
||||||
const menuItems: MenuItem[] = [
|
|
||||||
{
|
|
||||||
text: 'コピー',
|
|
||||||
icon: 'ti ti-files',
|
|
||||||
action: () => rangeCopyToClipboard(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 外からメニューを挿せるようにイベントを投げる
|
|
||||||
emit('operation:cellContextMenu', _rangedCells, menuItems);
|
|
||||||
|
|
||||||
if (menuItems.length > 0) {
|
if (menuItems.length > 0) {
|
||||||
os.contextMenu(menuItems, ev);
|
os.contextMenu(menuItems, ev);
|
||||||
}
|
}
|
||||||
|
@ -600,7 +581,7 @@ function onCellEditEnd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
|
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
|
||||||
setCellValue(sender, newValue);
|
emitCellValue(sender, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
|
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
|
||||||
|
@ -679,23 +660,44 @@ function calcLargestCellWidth(column: GridColumn) {
|
||||||
column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
|
column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
|
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)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
const violation = cellValidation(cell, newValue);
|
||||||
emit('operation:cellValidation', violation);
|
emitGridEvent({ type: 'cell-validation', violation });
|
||||||
|
|
||||||
cell.validation = {
|
cell.validation = {
|
||||||
valid: violation.valid,
|
valid: violation.valid,
|
||||||
violations: violation.violations.filter(it => !it.valid),
|
violations: violation.violations.filter(it => !it.valid),
|
||||||
};
|
};
|
||||||
cell.value = newValue;
|
|
||||||
|
|
||||||
emit('change:cellValue', {
|
emitGridEvent({
|
||||||
|
type: 'cell-value-change',
|
||||||
column: cell.column,
|
column: cell.column,
|
||||||
row: cell.row,
|
row: cell.row,
|
||||||
value: newValue,
|
oldValue: cell.value,
|
||||||
|
newValue: newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -784,75 +786,6 @@ function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
|
||||||
return cellAddress.row >= 0 && cellAddress.col === -1;
|
return cellAddress.row >= 0 && cellAddress.col === -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rangeCopyToClipboard() {
|
|
||||||
const lines = Array.of<string>();
|
|
||||||
const bounds = rangedBounds.value;
|
|
||||||
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
|
||||||
const items = Array.of<string>();
|
|
||||||
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
|
||||||
const cell = cells.value[row][col];
|
|
||||||
items.push(cell.value?.toString() ?? '');
|
|
||||||
}
|
|
||||||
lines.push(items.join('\t'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = lines.join('\n');
|
|
||||||
copyToClipboard(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pasteFromClipboard() {
|
|
||||||
function parseValue(value: string, type: ColumnSetting['type']): CellValue {
|
|
||||||
switch (type) {
|
|
||||||
case 'number': {
|
|
||||||
return Number(value);
|
|
||||||
}
|
|
||||||
case 'boolean': {
|
|
||||||
return value === 'true';
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clipBoardText = await navigator.clipboard.readText();
|
|
||||||
|
|
||||||
const bounds = rangedBounds.value;
|
|
||||||
const lines = clipBoardText.replace(/\r/g, '')
|
|
||||||
.split('\n')
|
|
||||||
.map(it => it.split('\t'));
|
|
||||||
|
|
||||||
if (lines.length === 1 && lines[0].length === 1) {
|
|
||||||
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
|
|
||||||
const ranges = rangedCells.value;
|
|
||||||
for (const cell of ranges) {
|
|
||||||
setCellValue(cell, parseValue(lines[0][0], cell.column.setting.type));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
|
|
||||||
const offsetRow = bounds.leftTop.row;
|
|
||||||
const offsetCol = bounds.leftTop.col;
|
|
||||||
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
|
||||||
const rowIdx = row - offsetRow;
|
|
||||||
if (lines.length <= rowIdx) {
|
|
||||||
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = lines[rowIdx];
|
|
||||||
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
|
||||||
const colIdx = col - offsetCol;
|
|
||||||
if (items.length <= colIdx) {
|
|
||||||
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCellValue(cells.value[row][col], parseValue(items[colIdx], cells.value[row][col].column.setting.type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshColumnsSetting() {
|
function refreshColumnsSetting() {
|
||||||
const bindToList = columnSettings.value.map(it => it.bindTo);
|
const bindToList = columnSettings.value.map(it => it.bindTo);
|
||||||
if (new Set(bindToList).size !== columnSettings.value.length) {
|
if (new Set(bindToList).size !== columnSettings.value.length) {
|
||||||
|
@ -863,6 +796,10 @@ function refreshColumnsSetting() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshData() {
|
function refreshData() {
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log('[grid][refresh-data]');
|
||||||
|
}
|
||||||
|
|
||||||
const _data: DataSource[] = data.value;
|
const _data: DataSource[] = data.value;
|
||||||
const _rows: GridRow[] = _data.map((_, index) => ({
|
const _rows: GridRow[] = _data.map((_, index) => ({
|
||||||
index,
|
index,
|
||||||
|
@ -927,9 +864,6 @@ function unregisterMouseUp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshColumnsSetting();
|
|
||||||
refreshData();
|
|
||||||
|
|
||||||
if (rootEl.value) {
|
if (rootEl.value) {
|
||||||
resizeObserver.observe(rootEl.value);
|
resizeObserver.observe(rootEl.value);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<th :class="[$style.cell]">
|
<th :class="[$style.cell]">
|
||||||
<div
|
<div :class="[$style.root]">
|
||||||
:class="[
|
|
||||||
$style.root,
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ content }}
|
{{ content }}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
|
||||||
|
import { GridColumn, GridRow, GridState } from '@/components/grid/grid.js';
|
||||||
|
import { ValidateViolation } from '@/components/grid/cell-validators.js';
|
||||||
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
|
export type GridCurrentState = {
|
||||||
|
selectedCell?: GridCell;
|
||||||
|
rangedCells: GridCell[];
|
||||||
|
rangedRows: GridRow[];
|
||||||
|
randedBounds: {
|
||||||
|
leftTop: CellAddress;
|
||||||
|
rightBottom: CellAddress;
|
||||||
|
};
|
||||||
|
availableBounds: {
|
||||||
|
leftTop: CellAddress;
|
||||||
|
rightBottom: CellAddress;
|
||||||
|
};
|
||||||
|
state: GridState;
|
||||||
|
rows: GridRow[];
|
||||||
|
columns: GridColumn[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridEvent =
|
||||||
|
GridCellValueChangeEvent |
|
||||||
|
GridKeyDownEvent |
|
||||||
|
GridMouseDownEvent |
|
||||||
|
GridCellValidationEvent |
|
||||||
|
GridCellContextMenuEvent |
|
||||||
|
GridRowContextMenuEvent |
|
||||||
|
GridColumnContextMenuEvent
|
||||||
|
;
|
||||||
|
|
||||||
|
export type GridCellValueChangeEvent = {
|
||||||
|
type: 'cell-value-change';
|
||||||
|
column: GridColumn;
|
||||||
|
row: GridRow;
|
||||||
|
oldValue: CellValue;
|
||||||
|
newValue: CellValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridCellValidationEvent = {
|
||||||
|
type: 'cell-validation';
|
||||||
|
violation: ValidateViolation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridKeyDownEvent = {
|
||||||
|
type: 'keydown';
|
||||||
|
event: KeyboardEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridMouseDownEvent = {
|
||||||
|
type: 'mousedown';
|
||||||
|
event: MouseEvent;
|
||||||
|
clickedCellAddress: CellAddress;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridCellContextMenuEvent = {
|
||||||
|
type: 'cell-context-menu';
|
||||||
|
event: MouseEvent;
|
||||||
|
menuItems: MenuItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridRowContextMenuEvent = {
|
||||||
|
type: 'row-context-menu';
|
||||||
|
event: MouseEvent;
|
||||||
|
menuItems: MenuItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GridColumnContextMenuEvent = {
|
||||||
|
type: 'column-context-menu';
|
||||||
|
event: MouseEvent;
|
||||||
|
menuItems: MenuItem[];
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { CellValidator } from '@/components/grid/cell-validators.js';
|
import { CellValidator } from '@/components/grid/cell-validators.js';
|
||||||
import { CellValue, GridCell } from '@/components/grid/cell.js';
|
import { CellValue } from '@/components/grid/cell.js';
|
||||||
|
|
||||||
export type GridSetting = {
|
export type GridSetting = {
|
||||||
rowNumberVisible: boolean;
|
rowNumberVisible: boolean;
|
||||||
|
@ -41,34 +41,6 @@ export type GridRow = {
|
||||||
ranged: boolean;
|
ranged: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CellValueChangedEvent = {
|
|
||||||
column: GridColumn;
|
|
||||||
row: GridRow;
|
|
||||||
value: CellValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GridEvent = {
|
|
||||||
current: {
|
|
||||||
selectedCell: GridCell;
|
|
||||||
rangedCells: GridCell[];
|
|
||||||
rangedRows: GridRow[];
|
|
||||||
state: GridState;
|
|
||||||
rows: GridRow[];
|
|
||||||
columns: GridColumn[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GridPreKeyDownEvent = {
|
|
||||||
type: 'pre-keydown';
|
|
||||||
event: KeyboardEvent;
|
|
||||||
prevent: boolean;
|
|
||||||
} & GridEvent;
|
|
||||||
|
|
||||||
export type GridKeyDownEvent = {
|
|
||||||
type: 'keydown';
|
|
||||||
event: KeyboardEvent;
|
|
||||||
} & GridEvent;
|
|
||||||
|
|
||||||
export class GridEventEmitter extends EventEmitter<{
|
export class GridEventEmitter extends EventEmitter<{
|
||||||
'forceRefreshContentSize': void;
|
'forceRefreshContentSize': void;
|
||||||
}> {
|
}> {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
export interface IGridItem {
|
export type GridItem = {
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
readonly fileId?: string;
|
readonly fileId?: string;
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
|
|
||||||
|
checked: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
aliases: string;
|
aliases: string;
|
||||||
|
@ -14,86 +15,34 @@ export interface IGridItem {
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
|
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GridItem implements IGridItem {
|
export function fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
|
||||||
readonly id?: string;
|
return {
|
||||||
readonly fileId?: string;
|
id: it.id,
|
||||||
readonly url: string;
|
fileId: undefined,
|
||||||
|
url: it.url,
|
||||||
public checked: boolean;
|
checked: false,
|
||||||
public name: string;
|
name: it.name,
|
||||||
public category: string;
|
category: it.category ?? '',
|
||||||
public aliases: string;
|
aliases: it.aliases.join(', '),
|
||||||
public license: string;
|
license: it.license ?? '',
|
||||||
public isSensitive: boolean;
|
isSensitive: it.isSensitive,
|
||||||
public localOnly: boolean;
|
localOnly: it.localOnly,
|
||||||
public roleIdsThatCanBeUsedThisEmojiAsReaction: string;
|
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '),
|
||||||
|
};
|
||||||
private readonly origin: string;
|
}
|
||||||
|
|
||||||
constructor(
|
export function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
|
||||||
id: string | undefined,
|
return {
|
||||||
fileId: string | undefined,
|
id: undefined,
|
||||||
url: string,
|
fileId: it.id,
|
||||||
name: string,
|
url: it.url,
|
||||||
category: string,
|
checked: false,
|
||||||
aliases: string,
|
name: it.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
||||||
license: string,
|
category: '',
|
||||||
isSensitive: boolean,
|
aliases: '',
|
||||||
localOnly: boolean,
|
license: '',
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: string,
|
isSensitive: it.isSensitive,
|
||||||
) {
|
localOnly: false,
|
||||||
this.id = id;
|
roleIdsThatCanBeUsedThisEmojiAsReaction: '',
|
||||||
this.fileId = fileId;
|
};
|
||||||
this.url = url;
|
|
||||||
|
|
||||||
this.checked = true;
|
|
||||||
this.aliases = aliases;
|
|
||||||
this.name = name;
|
|
||||||
this.category = category;
|
|
||||||
this.license = license;
|
|
||||||
this.isSensitive = isSensitive;
|
|
||||||
this.localOnly = localOnly;
|
|
||||||
this.roleIdsThatCanBeUsedThisEmojiAsReaction = roleIdsThatCanBeUsedThisEmojiAsReaction;
|
|
||||||
|
|
||||||
this.origin = JSON.stringify(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
|
|
||||||
return new GridItem(
|
|
||||||
it.id,
|
|
||||||
undefined,
|
|
||||||
it.url,
|
|
||||||
it.name,
|
|
||||||
it.category ?? '',
|
|
||||||
it.aliases.join(', '),
|
|
||||||
it.license ?? '',
|
|
||||||
it.isSensitive,
|
|
||||||
it.localOnly,
|
|
||||||
it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
|
|
||||||
return new GridItem(
|
|
||||||
undefined,
|
|
||||||
it.id,
|
|
||||||
it.url,
|
|
||||||
it.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
it.isSensitive,
|
|
||||||
false,
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get edited(): boolean {
|
|
||||||
const { origin, ..._this } = this;
|
|
||||||
return JSON.stringify(_this) !== origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public asRecord(): Record<string, never> {
|
|
||||||
return this as Record<string, never>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<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 * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { 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 { ColumnSetting } from '@/components/grid/grid.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -65,7 +65,7 @@ const { customEmojis } = toRefs(props);
|
||||||
const query = ref('');
|
const query = ref('');
|
||||||
const gridItems = ref<GridItem[]>([]);
|
const gridItems = ref<GridItem[]>([]);
|
||||||
|
|
||||||
const convertedGridItems = computed(() => gridItems.value.map(it => it.asRecord()));
|
const convertedGridItems = computed(() => gridItems.value.map(it => it as Record<string, any>));
|
||||||
|
|
||||||
watch(customEmojis, refreshGridItems);
|
watch(customEmojis, refreshGridItems);
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ function onSearchButtonClicked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshGridItems() {
|
function refreshGridItems() {
|
||||||
gridItems.value = customEmojis.value.map(it => GridItem.fromEmojiDetailed(it));
|
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<div v-if="registerLogs.length > 0" style="overflow-y: scroll;">
|
<div v-if="registerLogs.length > 0" style="overflow-y: scroll;">
|
||||||
<MkGrid
|
<MkGrid
|
||||||
:gridSetting="{ rowNumberVisible: false }"
|
:gridSetting="{ rowNumberVisible: false }"
|
||||||
:data="convertedRegisterLogs"
|
:data="registerLogs"
|
||||||
:columnSettings="registerLogColumnSettings"
|
:columnSettings="registerLogColumnSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,12 +66,9 @@
|
||||||
style="overflow-y: scroll;"
|
style="overflow-y: scroll;"
|
||||||
>
|
>
|
||||||
<MkGrid
|
<MkGrid
|
||||||
:data="convertedGridItems"
|
:data="gridItems"
|
||||||
:columnSettings="columnSettings"
|
:columnSettings="columnSettings"
|
||||||
@operation:rowDeleting="onRowDeleting"
|
@event="onGridEvent"
|
||||||
@operation:cellValidation="onCellValidation"
|
|
||||||
@operation:cellContextMenu="onCellContextMenu"
|
|
||||||
@change:cellValue="onChangeCellValue"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -89,11 +86,11 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { GridItem, IGridItem } 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 { CellValueChangedEvent, ColumnSetting, GridRow } from '@/components/grid/grid.js';
|
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';
|
||||||
|
@ -101,11 +98,19 @@ import { defaultStore } from '@/store.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { required, ValidateViolation } from '@/components/grid/cell-validators.js';
|
import { required } from '@/components/grid/cell-validators.js';
|
||||||
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
|
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
|
||||||
import { uploadFile } from '@/scripts/upload.js';
|
import { uploadFile } from '@/scripts/upload.js';
|
||||||
import { GridCell } from '@/components/grid/cell.js';
|
import {
|
||||||
import { MenuItem } from '@/types/menu.js';
|
GridCellValidationEvent,
|
||||||
|
GridCellValueChangeEvent,
|
||||||
|
GridCurrentState,
|
||||||
|
GridEvent,
|
||||||
|
GridKeyDownEvent,
|
||||||
|
GridRowContextMenuEvent,
|
||||||
|
} from '@/components/grid/grid-event.js';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
|
import { CellValue } from '@/components/grid/cell.js';
|
||||||
|
|
||||||
type FolderItem = {
|
type FolderItem = {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -114,7 +119,7 @@ type FolderItem = {
|
||||||
|
|
||||||
type UploadResult = {
|
type UploadResult = {
|
||||||
key: string,
|
key: string,
|
||||||
item: IGridItem,
|
item: GridItem,
|
||||||
success: boolean,
|
success: boolean,
|
||||||
err?: Error
|
err?: Error
|
||||||
};
|
};
|
||||||
|
@ -163,7 +168,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const uploadFolders = ref<FolderItem[]>([]);
|
const uploadFolders = ref<FolderItem[]>([]);
|
||||||
const gridItems = ref<IGridItem[]>([]);
|
const gridItems = ref<GridItem[]>([]);
|
||||||
const selectedFolderId = ref(defaultStore.state.uploadFolder);
|
const selectedFolderId = ref(defaultStore.state.uploadFolder);
|
||||||
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
|
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
|
||||||
const directoryToCategory = ref<boolean>(true);
|
const directoryToCategory = ref<boolean>(true);
|
||||||
|
@ -171,9 +176,6 @@ const directoryToCategory = ref<boolean>(true);
|
||||||
const registerButtonDisabled = ref<boolean>(false);
|
const registerButtonDisabled = ref<boolean>(false);
|
||||||
const registerLogs = ref<RegisterLogItem[]>([]);
|
const registerLogs = ref<RegisterLogItem[]>([]);
|
||||||
|
|
||||||
const convertedGridItems = computed(() => gridItems.value.map(it => it as Record<string, any>));
|
|
||||||
const convertedRegisterLogs = computed(() => registerLogs.value.map(it => it as Record<string, any>));
|
|
||||||
|
|
||||||
async function onRegistryClicked() {
|
async function onRegistryClicked() {
|
||||||
const dialogSelection = await os.confirm({
|
const dialogSelection = await os.confirm({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
|
@ -185,7 +187,7 @@ async function onRegistryClicked() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = new Map<string, IGridItem>(gridItems.value.map(it => [`${it.fileId}|${it.name}`, it]));
|
const items = new Map<string, GridItem>(gridItems.value.map(it => [`${it.fileId}|${it.name}`, it]));
|
||||||
const upload = async (): Promise<UploadResult[]> => {
|
const upload = async (): Promise<UploadResult[]> => {
|
||||||
const result = Array.of<UploadResult>();
|
const result = Array.of<UploadResult>();
|
||||||
for (const [key, item] of items.entries()) {
|
for (const [key, item] of items.entries()) {
|
||||||
|
@ -243,9 +245,6 @@ async function onClearClicked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDrop(ev: DragEvent) {
|
async function onDrop(ev: DragEvent) {
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
const dropItems = ev.dataTransfer?.items;
|
const dropItems = ev.dataTransfer?.items;
|
||||||
if (!dropItems || dropItems.length === 0) {
|
if (!dropItems || dropItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -289,7 +288,7 @@ async function onDrop(ev: DragEvent) {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const { droppedFile, driveFile } of uploadedItems) {
|
for (const { droppedFile, driveFile } of uploadedItems) {
|
||||||
const item = GridItem.fromDriveFile(driveFile);
|
const item = fromDriveFile(driveFile);
|
||||||
if (directoryToCategory.value) {
|
if (directoryToCategory.value) {
|
||||||
item.category = droppedFile.path
|
item.category = droppedFile.path
|
||||||
.replace(/^\//, '')
|
.replace(/^\//, '')
|
||||||
|
@ -311,13 +310,13 @@ async function onFileSelectClicked(ev: MouseEvent) {
|
||||||
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
gridItems.value.push(...driveFiles.map(GridItem.fromDriveFile));
|
gridItems.value.push(...driveFiles.map(fromDriveFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDriveSelectClicked(ev: MouseEvent) {
|
async function onDriveSelectClicked(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const driveFiles = await chooseFileFromDrive(true);
|
const driveFiles = await chooseFileFromDrive(true);
|
||||||
gridItems.value.push(...driveFiles.map(GridItem.fromDriveFile));
|
gridItems.value.push(...driveFiles.map(fromDriveFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRowDeleting(rows: GridRow[]) {
|
function onRowDeleting(rows: GridRow[]) {
|
||||||
|
@ -325,24 +324,65 @@ function onRowDeleting(rows: GridRow[]) {
|
||||||
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
|
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCellValidation(violation: ValidateViolation) {
|
function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
|
||||||
registerButtonDisabled.value = !violation.valid;
|
switch (event.type) {
|
||||||
|
case 'cell-validation':
|
||||||
|
onGridCellValidation(event, currentState);
|
||||||
|
break;
|
||||||
|
case 'row-context-menu':
|
||||||
|
onGridRowContextMenu(event, currentState);
|
||||||
|
break;
|
||||||
|
case 'cell-value-change':
|
||||||
|
onGridCellValueChange(event, currentState);
|
||||||
|
break;
|
||||||
|
case 'keydown':
|
||||||
|
onGridKeyDown(event, currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCellContextMenu(cells: GridCell[], menuItems: MenuItem[]) {
|
function onGridCellValidation(event: GridCellValidationEvent, _: GridCurrentState) {
|
||||||
menuItems.push(
|
registerButtonDisabled.value = !event.violation.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
|
||||||
|
event.menuItems.push(
|
||||||
{
|
{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
text: '行を削除',
|
text: '行を削除',
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
action: (ev: MouseEvent) => onRowDeleting(cells.map(it => it.row)),
|
action: () => onRowDeleting(currentState.rangedRows),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeCellValue(event: CellValueChangedEvent) {
|
function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) {
|
||||||
const item = gridItems.value[event.row.index];
|
const item = gridItems.value[event.row.index];
|
||||||
item[event.column.setting.bindTo] = event.value;
|
item[event.column.setting.bindTo] = event.newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
|
||||||
|
switch (event.event.code) {
|
||||||
|
case 'KeyC': {
|
||||||
|
rangeCopyToClipboard(currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'KeyV': {
|
||||||
|
pasteFromClipboard(currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Delete': {
|
||||||
|
if (currentState.rangedRows.length > 0) {
|
||||||
|
onRowDeleting(currentState.rangedRows);
|
||||||
|
} else {
|
||||||
|
const ranges = currentState.rangedCells;
|
||||||
|
for (const cell of ranges) {
|
||||||
|
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function eachDroppedItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
|
async function eachDroppedItems(itemList: DataTransferItemList): Promise<DroppedItem[]> {
|
||||||
|
@ -403,6 +443,77 @@ function flattenDroppedItems(items: DroppedItem[]): DroppedFile[] {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rangeCopyToClipboard(currentState: GridCurrentState) {
|
||||||
|
const lines = Array.of<string>();
|
||||||
|
const bounds = currentState.randedBounds;
|
||||||
|
|
||||||
|
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
||||||
|
const items = Array.of<string>();
|
||||||
|
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
||||||
|
const cell = gridItems.value[row][col];
|
||||||
|
items.push(cell.value?.toString() ?? '');
|
||||||
|
}
|
||||||
|
lines.push(items.join('\t'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = lines.join('\n');
|
||||||
|
copyToClipboard(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pasteFromClipboard(currentState: GridCurrentState) {
|
||||||
|
function parseValue(value: string, type: ColumnSetting['type']): CellValue {
|
||||||
|
switch (type) {
|
||||||
|
case 'number': {
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
case 'boolean': {
|
||||||
|
return value === 'true';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells = currentState.rangedCells;
|
||||||
|
const clipBoardText = await navigator.clipboard.readText();
|
||||||
|
|
||||||
|
const bounds = currentState.randedBounds;
|
||||||
|
const lines = clipBoardText.replace(/\r/g, '')
|
||||||
|
.split('\n')
|
||||||
|
.map(it => it.split('\t'));
|
||||||
|
|
||||||
|
if (lines.length === 1 && lines[0].length === 1) {
|
||||||
|
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
|
||||||
|
const ranges = currentState.rangedCells;
|
||||||
|
for (const cell of ranges) {
|
||||||
|
gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
|
||||||
|
const offsetRow = bounds.leftTop.row;
|
||||||
|
const offsetCol = bounds.leftTop.col;
|
||||||
|
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
|
||||||
|
const rowIdx = row - offsetRow;
|
||||||
|
if (lines.length <= rowIdx) {
|
||||||
|
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = lines[rowIdx];
|
||||||
|
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
|
||||||
|
const colIdx = col - offsetCol;
|
||||||
|
if (items.length <= colIdx) {
|
||||||
|
// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gridItems.value[row][col] = parseValue(items[colIdx], cells[row][col].column.setting.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshUploadFolders() {
|
async function refreshUploadFolders() {
|
||||||
const result = await misskeyApi('drive/folders', {});
|
const result = await misskeyApi('drive/folders', {});
|
||||||
uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result);
|
uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result);
|
||||||
|
|
Loading…
Reference in New Issue