イベントの整理

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) { 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);

View File

@ -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);

View File

@ -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>

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 { 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;
}> { }> {

View File

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

View File

@ -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(() => {

View File

@ -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);