イベントの整理

This commit is contained in:
samunohito 2024-02-01 20:59:30 +09:00
parent 777920d739
commit f96c7224a7
8 changed files with 332 additions and 291 deletions

View File

@ -99,6 +99,10 @@ watch(() => cell.value.selected, () => {
}
});
watch(() => cell.value.value, (newValue, oldValue) => {
emitValueChange(newValue);
});
function onCellDoubleClick(ev: MouseEvent) {
switch (ev.type) {
case 'dblclick': {
@ -119,6 +123,7 @@ function onCellKeyDown(ev: KeyboardEvent) {
if (!editing.value) {
ev.preventDefault();
switch (ev.code) {
case 'NumpadEnter':
case 'Enter':
case 'F2': {
beginEditing();
@ -131,6 +136,7 @@ function onCellKeyDown(ev: KeyboardEvent) {
endEditing(false);
break;
}
case 'NumpadEnter':
case 'Enter': {
if (!ev.isComposing) {
endEditing(true);

View File

@ -3,9 +3,9 @@
ref="rootEl"
tabindex="-1"
:class="[$style.grid, $style.border]"
@mousedown="onMouseDown"
@mousedown.prevent="onMouseDown"
@keydown="onKeyDown"
@contextmenu="onContextMenu"
@contextmenu.prevent.stop="onContextMenu"
>
<thead>
<MkHeaderRow
@ -39,7 +39,6 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
import {
CellValueChangedEvent,
ColumnSetting,
DataSource,
GridColumn,
@ -51,12 +50,12 @@ import {
} from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
import { cellValidation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { calcCellWidth, equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
const props = withDefaults(defineProps<{
gridSetting?: GridSetting,
@ -69,14 +68,11 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'operation:cellValidation', violation: ValidateViolation): void;
(ev: 'operation:rowDeleting', rows: GridRow[]): void;
(ev: 'operation:cellContextMenu', cells: GridCell[], menuItems: MenuItem[]): void;
(ev: 'change:cellValue', event: CellValueChangedEvent): void;
(ev: 'event', event: GridEvent, current: GridCurrentState): void;
}>();
/**
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}おもにpropsでの伝搬が難しいイベントを伝搬するために使用する
* 子コンポーネント -> gridのイベントでは原則使用せず{@link emit}を使用する
*/
const bus = new GridEventEmitter();
@ -110,14 +106,18 @@ const selectedCell = computed(() => {
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
const rangedBounds = computed(() => {
const _cells = rangedCells.value;
const cols = _cells.map(it => it.address.col);
const rows = _cells.map(it => it.address.row);
const leftTop = {
col: Math.min(..._cells.map(it => it.address.col)),
row: Math.min(..._cells.map(it => it.address.row)),
col: Math.min(...cols),
row: Math.min(...rows),
};
const rightBottom = {
col: Math.max(..._cells.map(it => it.address.col)),
row: Math.max(..._cells.map(it => it.address.row)),
col: Math.max(...cols),
row: Math.max(...rows),
};
return {
leftTop,
rightBottom,
@ -136,8 +136,8 @@ const availableBounds = computed(() => {
});
const rangedRows = computed(() => rows.value.filter(it => it.ranged));
watch(columnSettings, refreshColumnsSetting);
watch(data, refreshData);
watch(columnSettings, refreshColumnsSetting, { immediate: true });
watch(data, refreshData, { immediate: true, deep: true });
if (_DEV_) {
watch(state, (value, oldValue) => {
@ -232,16 +232,8 @@ function onKeyDown(ev: KeyboardEvent) {
unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
expandCellRange(newBounds.leftTop, newBounds.rightBottom);
} else {
switch (ev.code) {
case 'KeyC': {
rangeCopyToClipboard();
break;
}
case 'KeyV': {
pasteFromClipboard();
break;
}
}
//
emitGridEvent({ type: 'keydown', event: ev });
}
} else {
if (ev.shiftKey) {
@ -348,19 +340,10 @@ function onKeyDown(ev: KeyboardEvent) {
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
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: {
return;
//
emitGridEvent({ type: 'keydown', event: ev });
break;
}
}
}
@ -386,11 +369,9 @@ function onMouseDown(ev: MouseEvent) {
function onLeftMouseDown(ev: MouseEvent) {
const cellAddress = getCellAddress(ev.target as HTMLElement);
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) {
case 'cellEditing': {
if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
@ -423,6 +404,8 @@ function onLeftMouseDown(ev: MouseEvent) {
const rowCells = cells.value[cellAddress.row];
selectionRange(...rowCells.map(cell => cell.address));
expandRowRange(cellAddress.row, cellAddress.row);
registerMouseUp();
registerMouseMove();
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}`);
}
ev.preventDefault();
switch (state.value) {
case 'normal': {
if (!availableCellAddress(cellAddress)) {
@ -462,10 +443,19 @@ function onRightMouseDown(ev: MouseEvent) {
function onMouseMove(ev: MouseEvent) {
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) {
case 'cellSelecting': {
const selectedCellAddress = selectedCell.value?.address;
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (equalCellAddress(previousCellAddress.value, targetCellAddress) || !availableCellAddress(targetCellAddress) || !selectedCellAddress) {
return;
}
@ -487,7 +477,6 @@ function onMouseMove(ev: MouseEvent) {
break;
}
case 'colSelecting': {
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) {
return;
}
@ -509,7 +498,6 @@ function onMouseMove(ev: MouseEvent) {
break;
}
case 'rowSelecting': {
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
return;
}
@ -560,24 +548,17 @@ function onContextMenu(ev: MouseEvent) {
console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`);
}
if (!availableCellAddress(cellAddress)) {
return;
const menuItems = Array.of<MenuItem>();
//
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) {
os.contextMenu(menuItems, ev);
}
@ -600,7 +581,7 @@ function onCellEditEnd() {
}
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
setCellValue(sender, newValue);
emitCellValue(sender, newValue);
}
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
@ -679,23 +660,44 @@ function calcLargestCellWidth(column: GridColumn) {
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 cell = cells.value[cellAddress.row][cellAddress.col];
const violation = cellValidation(cell, newValue);
emit('operation:cellValidation', violation);
emitGridEvent({ type: 'cell-validation', violation });
cell.validation = {
valid: violation.valid,
violations: violation.violations.filter(it => !it.valid),
};
cell.value = newValue;
emit('change:cellValue', {
emitGridEvent({
type: 'cell-value-change',
column: cell.column,
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;
}
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() {
const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) {
@ -863,6 +796,10 @@ function refreshColumnsSetting() {
}
function refreshData() {
if (_DEV_) {
console.log('[grid][refresh-data]');
}
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => ({
index,
@ -927,9 +864,6 @@ function unregisterMouseUp() {
}
onMounted(() => {
refreshColumnsSetting();
refreshData();
if (rootEl.value) {
resizeObserver.observe(rootEl.value);

View File

@ -1,10 +1,6 @@
<template>
<th :class="[$style.cell]">
<div
:class="[
$style.root,
]"
>
<div :class="[$style.root]">
{{ content }}
</div>
</th>

View File

@ -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[];
};

View File

@ -1,6 +1,6 @@
import { EventEmitter } from 'eventemitter3';
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 = {
rowNumberVisible: boolean;
@ -41,34 +41,6 @@ export type GridRow = {
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<{
'forceRefreshContentSize': void;
}> {

View File

@ -1,10 +1,11 @@
import * as Misskey from 'misskey-js';
export interface IGridItem {
export type GridItem = {
readonly id?: string;
readonly fileId?: string;
readonly url: string;
checked: boolean;
name: string;
category: string;
aliases: string;
@ -14,86 +15,34 @@ export interface IGridItem {
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
}
export class GridItem implements IGridItem {
readonly id?: string;
readonly fileId?: string;
readonly url: string;
public checked: boolean;
public name: string;
public category: string;
public aliases: string;
public license: string;
public isSensitive: boolean;
public localOnly: boolean;
public roleIdsThatCanBeUsedThisEmojiAsReaction: string;
private readonly origin: string;
constructor(
id: string | undefined,
fileId: string | undefined,
url: string,
name: string,
category: string,
aliases: string,
license: string,
isSensitive: boolean,
localOnly: boolean,
roleIdsThatCanBeUsedThisEmojiAsReaction: string,
) {
this.id = id;
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>;
}
export function fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
return {
id: it.id,
fileId: undefined,
url: it.url,
checked: false,
name: it.name,
category: it.category ?? '',
aliases: it.aliases.join(', '),
license: it.license ?? '',
isSensitive: it.isSensitive,
localOnly: it.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '),
};
}
export function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
return {
id: undefined,
fileId: it.id,
url: it.url,
checked: false,
name: it.name.replace(/\.[a-zA-Z0-9]+$/, ''),
category: '',
aliases: '',
license: '',
isSensitive: it.isSensitive,
localOnly: false,
roleIdsThatCanBeUsedThisEmojiAsReaction: '',
};
}

View File

@ -32,7 +32,7 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
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 { ColumnSetting } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js';
@ -65,7 +65,7 @@ const { customEmojis } = toRefs(props);
const query = ref('');
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);
@ -74,7 +74,7 @@ function onSearchButtonClicked() {
}
function refreshGridItems() {
gridItems.value = customEmojis.value.map(it => GridItem.fromEmojiDetailed(it));
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
}
onMounted(() => {

View File

@ -36,7 +36,7 @@
<div v-if="registerLogs.length > 0" style="overflow-y: scroll;">
<MkGrid
:gridSetting="{ rowNumberVisible: false }"
:data="convertedRegisterLogs"
:data="registerLogs"
:columnSettings="registerLogColumnSettings"
/>
</div>
@ -66,12 +66,9 @@
style="overflow-y: scroll;"
>
<MkGrid
:data="convertedGridItems"
:data="gridItems"
:columnSettings="columnSettings"
@operation:rowDeleting="onRowDeleting"
@operation:cellValidation="onCellValidation"
@operation:cellContextMenu="onCellContextMenu"
@change:cellValue="onChangeCellValue"
@event="onGridEvent"
/>
</div>
@ -89,11 +86,11 @@
<script setup lang="ts">
/* 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 { 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 { CellValueChangedEvent, ColumnSetting, GridRow } from '@/components/grid/grid.js';
import { ColumnSetting, GridRow } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -101,11 +98,19 @@ import { defaultStore } from '@/store.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
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 { uploadFile } from '@/scripts/upload.js';
import { GridCell } from '@/components/grid/cell.js';
import { MenuItem } from '@/types/menu.js';
import {
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 = {
id?: string;
@ -114,7 +119,7 @@ type FolderItem = {
type UploadResult = {
key: string,
item: IGridItem,
item: GridItem,
success: boolean,
err?: Error
};
@ -163,7 +168,7 @@ const emit = defineEmits<{
}>();
const uploadFolders = ref<FolderItem[]>([]);
const gridItems = ref<IGridItem[]>([]);
const gridItems = ref<GridItem[]>([]);
const selectedFolderId = ref(defaultStore.state.uploadFolder);
const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
const directoryToCategory = ref<boolean>(true);
@ -171,9 +176,6 @@ const directoryToCategory = ref<boolean>(true);
const registerButtonDisabled = ref<boolean>(false);
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() {
const dialogSelection = await os.confirm({
type: 'info',
@ -185,7 +187,7 @@ async function onRegistryClicked() {
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 result = Array.of<UploadResult>();
for (const [key, item] of items.entries()) {
@ -243,9 +245,6 @@ async function onClearClicked() {
}
async function onDrop(ev: DragEvent) {
ev.preventDefault();
ev.stopPropagation();
const dropItems = ev.dataTransfer?.items;
if (!dropItems || dropItems.length === 0) {
return;
@ -289,7 +288,7 @@ async function onDrop(ev: DragEvent) {
);
for (const { droppedFile, driveFile } of uploadedItems) {
const item = GridItem.fromDriveFile(driveFile);
const item = fromDriveFile(driveFile);
if (directoryToCategory.value) {
item.category = droppedFile.path
.replace(/^\//, '')
@ -311,13 +310,13 @@ async function onFileSelectClicked(ev: MouseEvent) {
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) {
ev.preventDefault();
const driveFiles = await chooseFileFromDrive(true);
gridItems.value.push(...driveFiles.map(GridItem.fromDriveFile));
gridItems.value.push(...driveFiles.map(fromDriveFile));
}
function onRowDeleting(rows: GridRow[]) {
@ -325,24 +324,65 @@ function onRowDeleting(rows: GridRow[]) {
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
}
function onCellValidation(violation: ValidateViolation) {
registerButtonDisabled.value = !violation.valid;
function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
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[]) {
menuItems.push(
function onGridCellValidation(event: GridCellValidationEvent, _: GridCurrentState) {
registerButtonDisabled.value = !event.violation.valid;
}
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '行を削除',
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];
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[]> {
@ -403,6 +443,77 @@ function flattenDroppedItems(items: DroppedItem[]): DroppedFile[] {
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() {
const result = await misskeyApi('drive/folders', {});
uploadFolders.value = Array.of<FolderItem>({ name: '-' }, ...result);