イベントの整理
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) {
|
||||
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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
<template>
|
||||
<th :class="[$style.cell]">
|
||||
<div
|
||||
:class="[
|
||||
$style.root,
|
||||
]"
|
||||
>
|
||||
<div :class="[$style.root]">
|
||||
{{ content }}
|
||||
</div>
|
||||
</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 { 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;
|
||||
}> {
|
||||
|
|
|
@ -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: '',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue