fix copy/paste and delete

This commit is contained in:
samunohito 2024-02-20 20:25:42 +09:00
parent 0f896f6bdb
commit effe586092
11 changed files with 286 additions and 385 deletions

View File

@ -45,7 +45,14 @@ import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import { cellValidation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell, resetCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress, getCellElement } from '@/components/grid/grid-utils.js';
import {
copyGridDataToClipboard,
equalCellAddress,
getCellAddress,
getCellElement,
pasteToGridFromClipboard,
removeDataFromGrid,
} from '@/components/grid/grid-utils.js';
import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { GridContext, GridEvent } from '@/components/grid/grid-event.js';
@ -255,11 +262,7 @@ function onKeyDown(ev: KeyboardEvent) {
case 'normal': {
ev.preventDefault();
const selectedCellAddress = selectedCell.value?.address;
if (!selectedCellAddress) {
return;
}
const selectedCellAddress = selectedCell.value?.address ?? CELL_ADDRESS_NONE;
const max = availableBounds.value;
const bounds = rangedBounds.value;
@ -267,6 +270,35 @@ function onKeyDown(ev: KeyboardEvent) {
{
code: 'any', handler: () => emitGridEvent({ type: 'keydown', event: ev }),
},
{
code: 'Delete', handler: () => {
if (rangedRows.value.length > 0) {
if (rowSetting.events.delete) {
rowSetting.events.delete(rangedRows.value);
}
} else {
const context = createContext();
removeDataFromGrid(context, (cell) => {
emitCellValue(cell, undefined);
});
}
},
},
{
code: 'KeyC', modifiers: ['Control'], handler: () => {
const context = createContext();
copyGridDataToClipboard(data.value, context);
},
},
{
code: 'KeyV', modifiers: ['Control'], handler: async () => {
const _cells = cells.value;
const context = createContext();
await pasteToGridFromClipboard(context, (row, col, parsedValue) => {
emitCellValue(_cells[row.index].cells[col.index], parsedValue);
});
},
},
{
code: 'ArrowRight', modifiers: ['Control', 'Shift'], handler: () => {
updateSelectionRange({
@ -854,19 +886,10 @@ function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
const cellAddress = 'address' in sender ? sender.address : sender;
const cell = cells.value[cellAddress.row].cells[cellAddress.col];
const violation = cellValidation(cell, newValue);
cell.violation = violation;
emitGridEvent({
type: 'cell-validation',
violation: violation,
all: cells.value.flatMap(it => it.cells).map(it => it.violation),
});
emitGridEvent({
type: 'cell-value-change',
column: cell.column,
row: cell.row,
violation: violation,
oldValue: cell.value,
newValue: newValue,
});
@ -876,19 +899,6 @@ function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
}
}
/**
* {@link selectedCell}のセル番地を取得する
* いずれかのセルが選択されている状態で呼ばれることを想定しているため選択されていない場合は例外を投げる
*/
function requireSelectionCell(): CellAddress {
const selected = selectedCell.value;
if (!selected) {
throw new Error('No selected cell');
}
return selected.address;
}
/**
* {@link target}のセルを選択状態にする
* その際{@link target}以外の行およびセルの範囲選択状態を解除する
@ -911,7 +921,10 @@ function selectionCell(target: CellAddress) {
function selectionRange(...targets: CellAddress[]) {
const _cells = cells.value;
for (const target of targets) {
_cells[target.row].cells[target.col].ranged = true;
const row = _cells[target.row];
if (row.row.using) {
row.cells[target.col].ranged = true;
}
}
}
@ -935,16 +948,18 @@ function unSelectionRangeAll() {
* {@link leftTop}から{@link rightBottom}の範囲外にあるセルを範囲選択状態から外す
*/
function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
const safeBounds = getSafeAddressBounds({ leftTop, rightBottom });
const _cells = rangedCells.value;
for (const cell of _cells) {
const outOfRangeCol = cell.address.col < leftTop.col || cell.address.col > rightBottom.col;
const outOfRangeRow = cell.address.row < leftTop.row || cell.address.row > rightBottom.row;
const outOfRangeCol = cell.address.col < safeBounds.leftTop.col || cell.address.col > safeBounds.rightBottom.col;
const outOfRangeRow = cell.address.row < safeBounds.leftTop.row || cell.address.row > safeBounds.rightBottom.row;
if (outOfRangeCol || outOfRangeRow) {
cell.ranged = false;
}
}
const outOfRangeRows = rows.value.filter((_, index) => index < leftTop.row || index > rightBottom.row);
const outOfRangeRows = rows.value.filter((_, index) => index < safeBounds.leftTop.row || index > safeBounds.rightBottom.row);
for (const row of outOfRangeRows) {
row.ranged = false;
}
@ -954,9 +969,10 @@ function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
* {@link leftTop}から{@link rightBottom}の範囲内にあるセルを範囲選択状態にする
*/
function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
const safeBounds = getSafeAddressBounds({ leftTop, rightBottom });
const targetRows = cells.value.slice(safeBounds.leftTop.row, safeBounds.rightBottom.row + 1);
for (const row of targetRows) {
for (const cell of row.cells.slice(leftTop.col, rightBottom.col + 1)) {
for (const cell of row.cells.slice(safeBounds.leftTop.col, safeBounds.rightBottom.col + 1)) {
cell.ranged = true;
}
}
@ -989,10 +1005,10 @@ function applyRowRules(targetCells: GridCell[]) {
for (const group of rowGroups.filter(it => it.row.using)) {
const row = group.row;
const targetCols = group.cells.map(it => it.column);
const cells = _cells[group.row.index].cells;
const rowCells = _cells[group.row.index].cells;
const newStyles = rowSetting.styleRules
.filter(it => it.condition({ row, targetCols, cells }))
.filter(it => it.condition({ row, targetCols, cells: rowCells }))
.map(it => it.applyStyle);
if (JSON.stringify(newStyles) !== JSON.stringify(row.additionalStyles)) {
@ -1002,7 +1018,11 @@ function applyRowRules(targetCells: GridCell[]) {
}
function availableCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length;
const safeBounds = availableBounds.value;
return cellAddress.row >= safeBounds.leftTop.row &&
cellAddress.col >= safeBounds.leftTop.col &&
cellAddress.row <= safeBounds.rightBottom.row &&
cellAddress.col <= safeBounds.rightBottom.col;
}
function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean {
@ -1013,6 +1033,23 @@ function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col === -1;
}
function getSafeAddressBounds(
bounds: { leftTop: CellAddress, rightBottom: CellAddress },
): { leftTop: CellAddress, rightBottom: CellAddress } {
const available = availableBounds.value;
const safeLeftTop = {
col: Math.max(bounds.leftTop.col, available.leftTop.col),
row: Math.max(bounds.leftTop.row, available.leftTop.row),
};
const safeRightBottom = {
col: Math.min(bounds.rightBottom.col, available.rightBottom.col),
row: Math.min(bounds.rightBottom.row, available.rightBottom.row),
};
return { leftTop: safeLeftTop, rightBottom: safeRightBottom };
}
function registerMouseMove() {
unregisterMouseMove();
addEventListener('mousemove', onMouseMove);

View File

@ -1,7 +1,7 @@
import { CellValidator } from '@/components/grid/cell-validators.js';
import { Size, SizeStyle } from '@/components/grid/grid.js';
import { calcCellWidth } from '@/components/grid/grid-utils.js';
import { CellValue } from '@/components/grid/cell.js';
import { CellValue, GridCell } from '@/components/grid/cell.js';
import { GridRow } from '@/components/grid/row.js';
import { MenuItem } from '@/types/menu.js';
import { GridContext } from '@/components/grid/grid-event.js';
@ -23,6 +23,11 @@ export type GridColumnSetting = {
customValueEditor?: CustomValueEditor;
valueTransformer?: CellValueTransformer;
contextMenuFactory?: GridColumnContextMenuFactory;
events?: {
copy?: (value: CellValue) => string;
paste?: (text: string) => CellValue;
delete?: (cell: GridCell, context: GridContext) => void;
}
};
export type GridColumn = {

View File

@ -32,7 +32,6 @@ export type GridCellValueChangeEvent = {
type: 'cell-value-change';
column: GridColumn;
row: GridRow;
violation: ValidateViolation;
oldValue: CellValue;
newValue: CellValue;
};

View File

@ -1,6 +1,10 @@
import { SizeStyle } from '@/components/grid/grid.js';
import { CELL_ADDRESS_NONE, CellAddress } from '@/components/grid/cell.js';
import { GridRowSetting } from '@/components/grid/row.js';
import { isRef, Ref } from 'vue';
import { DataSource, SizeStyle } from '@/components/grid/grid.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { GridRow, GridRowSetting } from '@/components/grid/row.js';
import { GridContext } from '@/components/grid/grid-event.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { GridColumn, GridColumnSetting } from '@/components/grid/column.js';
export function isCellElement(elem: any): elem is HTMLTableCellElement {
return elem instanceof HTMLTableCellElement;
@ -65,3 +69,124 @@ export function equalCellAddress(a: CellAddress, b: CellAddress): boolean {
return a.row === b.row && a.col === b.col;
}
/**
*
*/
export function copyGridDataToClipboard(
gridItems: Ref<DataSource[]> | DataSource[],
context: GridContext,
) {
const items = isRef(gridItems) ? gridItems.value : gridItems;
const lines = Array.of<string>();
const bounds = context.randedBounds;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const rowItems = Array.of<string>();
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const { bindTo, events } = context.columns[col].setting;
const value = items[row][bindTo];
const transformValue = events?.copy
? events.copy(value)
: typeof value === 'object' || Array.isArray(value)
? JSON.stringify(value)
: value?.toString() ?? '';
rowItems.push(transformValue);
}
lines.push(rowItems.join('\t'));
}
const text = lines.join('\n');
copyToClipboard(text);
if (_DEV_) {
console.log(`Copied to clipboard: ${text}`);
}
}
/**
*
* 使
*/
export async function pasteToGridFromClipboard(
context: GridContext,
callback: (row: GridRow, col: GridColumn, parsedValue: CellValue) => void,
) {
function parseValue(value: string, setting: GridColumnSetting): CellValue {
if (setting.events?.paste) {
return setting.events.paste(value);
} else {
switch (setting.type) {
case 'number': {
return Number(value);
}
case 'boolean': {
return value === 'true';
}
default: {
return value;
}
}
}
}
const clipBoardText = await navigator.clipboard.readText();
if (_DEV_) {
console.log(`Paste from clipboard: ${clipBoardText}`);
}
const bounds = context.randedBounds;
const lines = clipBoardText.replace(/\r/g, '')
.split('\n')
.map(it => it.split('\t'));
if (lines.length === 1 && lines[0].length === 1) {
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
const ranges = context.rangedCells;
for (const cell of ranges) {
callback(cell.row, cell.column, parseValue(lines[0][0], cell.column.setting));
}
} else {
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
const offsetRow = bounds.leftTop.row;
const offsetCol = bounds.leftTop.col;
const { columns, rows } = context;
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;
}
callback(rows[row], columns[col], parseValue(items[colIdx], columns[col].setting));
}
}
}
}
/**
*
* 使
*/
export function removeDataFromGrid(
context: GridContext,
callback: (cell: GridCell) => void,
) {
for (const cell of context.rangedCells) {
const { editable, events } = cell.column.setting;
if (editable) {
if (events?.delete) {
events.delete(cell, context);
} else {
callback(cell);
}
}
}
}

View File

@ -1,150 +0,0 @@
import { Ref } from 'vue';
import { GridContext, GridKeyDownEvent } from '@/components/grid/grid-event.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { CellValue } from '@/components/grid/cell.js';
import { DataSource } from '@/components/grid/grid.js';
class OptInGridUtils {
async defaultKeyDownHandler(gridItems: Ref<DataSource[]>, event: GridKeyDownEvent, context: GridContext) {
const { ctrlKey, shiftKey, code } = event.event;
switch (true) {
case ctrlKey && shiftKey: {
break;
}
case ctrlKey: {
switch (code) {
case 'KeyC': {
this.copyToClipboard(gridItems, context);
break;
}
case 'KeyV': {
await this.pasteFromClipboard(gridItems, context);
break;
}
}
break;
}
case shiftKey: {
break;
}
default: {
switch (code) {
case 'Delete': {
this.deleteSelectionRange(gridItems, context);
break;
}
}
break;
}
}
}
copyToClipboard(gridItems: Ref<DataSource[]> | DataSource[], context: GridContext) {
const items = typeof gridItems === 'object' ? (gridItems as Ref<DataSource[]>).value : gridItems;
const lines = Array.of<string>();
const bounds = context.randedBounds;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const rowItems = Array.of<string>();
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const bindTo = context.columns[col].setting.bindTo;
const cell = items[row][bindTo];
const value = typeof cell === 'object' || Array.isArray(cell)
? JSON.stringify(cell)
: cell?.toString() ?? '';
rowItems.push(value);
}
lines.push(rowItems.join('\t'));
}
const text = lines.join('\n');
copyToClipboard(text);
if (_DEV_) {
console.log(`Copied to clipboard: ${text}`);
}
}
async pasteFromClipboard(
gridItems: Ref<DataSource[]>,
context: GridContext,
valueConverters?: { bindTo: string, converter: (value: string) => CellValue }[],
) {
const converterMap = new Map<string, (value: string) => CellValue>(valueConverters?.map(it => [it.bindTo, it.converter]) ?? []);
function parseValue(value: string, setting: GridColumnSetting): CellValue {
switch (setting.type) {
case 'number': {
return Number(value);
}
case 'boolean': {
return value === 'true';
}
default: {
return converterMap.has(setting.bindTo)
? converterMap.get(setting.bindTo)!(value)
: value;
}
}
}
const clipBoardText = await navigator.clipboard.readText();
if (_DEV_) {
console.log(`Paste from clipboard: ${clipBoardText}`);
}
const bounds = context.randedBounds;
const lines = clipBoardText.replace(/\r/g, '')
.split('\n')
.map(it => it.split('\t'));
if (lines.length === 1 && lines[0].length === 1) {
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
const ranges = context.rangedCells;
for (const cell of ranges) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting);
}
} else {
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
const offsetRow = bounds.leftTop.row;
const offsetCol = bounds.leftTop.col;
const columns = context.columns;
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][columns[col].setting.bindTo] = parseValue(items[colIdx], columns[col].setting);
}
}
}
}
deleteSelectionRange(gridItems: Ref<Record<string, any>[]>, context: GridContext) {
if (context.rangedRows.length > 0) {
const deletedIndexes = context.rangedRows.map(it => it.index);
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
} else {
const ranges = context.rangedCells;
for (const cell of ranges) {
if (cell.column.setting.editable) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
}
}
}
}
}
export const optInGridUtils = new OptInGridUtils();

View File

@ -10,6 +10,7 @@ export const defaultGridRowSetting: Required<GridRowSetting> = {
minimumDefinitionCount: 100,
styleRules: [],
contextMenuFactory: () => [],
events: {},
};
export type GridRowStyleRuleConditionParams = {
@ -31,6 +32,9 @@ export type GridRowSetting = {
minimumDefinitionCount?: number;
styleRules?: GridRowStyleRule[];
contextMenuFactory?: GridRowContextMenuFactory;
events?: {
delete?: (rows: GridRow[]) => void;
}
}
export type GridRow = {

View File

@ -196,25 +196,18 @@ import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { validators } from '@/components/grid/cell-validators.js';
import {
GridCellValidationEvent,
GridCellValueChangeEvent,
GridContext,
GridEvent,
GridKeyDownEvent,
} from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js';
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.local.logs.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { GridSetting } from '@/components/grid/grid.js';
import MkTagItem from '@/components/MkTagItem.vue';
import { MenuItem } from '@/types/menu.js';
import { CellValue } from '@/components/grid/cell.js';
import { selectFile } from '@/scripts/select-file.js';
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
type GridItem = {
checked: boolean;
@ -275,26 +268,33 @@ function setupGrid(): GridSetting {
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
action: () => copyGridDataToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
for (const row of context.rangedRows) {
gridItems.value[row.index].checked = true;
for (const rangedRow of context.rangedRows) {
gridItems.value[rangedRow.index].checked = true;
}
},
},
];
},
events: {
delete(rows) {
for (const row of rows) {
gridItems.value[row.index].checked = true;
}
},
},
},
cols: [
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
{
bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
customValueEditor: async (row, col, value, cellElement) => {
async customValueEditor(row, col, value, cellElement) {
const file = await selectFile(cellElement);
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;
@ -310,13 +310,13 @@ function setupGrid(): GridSetting {
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
{
bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140,
valueTransformer: (row) => {
valueTransformer(row) {
// IDID
return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction
.map((it) => it.name)
.join(',');
},
customValueEditor: async (row) => {
async customValueEditor(row) {
// ID使
const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction;
const result = await os.selectRole({
@ -334,27 +334,54 @@ function setupGrid(): GridSetting {
return transform;
},
events: {
paste(text) {
// idnameJSON
try {
const obj = JSON.parse(text);
if (!Array.isArray(obj)) {
return [];
}
if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
return [];
}
return obj.map(it => ({ id: it.id, name: it.name }));
} catch (ex) {
console.warn(ex);
return [];
}
},
delete(cell) {
// undefined
gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = [];
},
},
},
{ bindTo: 'updatedAt', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'publicUrl', type: 'text', editable: false, width: 180 },
{ bindTo: 'originalUrl', type: 'text', editable: false, width: 180 },
],
cells: {
contextMenuFactory: (col, row, value, context) => {
contextMenuFactory(col, row, value, context) {
return [
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => {
return optInGridUtils.copyToClipboard(gridItems, context);
return copyGridDataToClipboard(gridItems, context);
},
},
{
type: 'button',
text: '選択範囲を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, context),
action: () => {
removeDataFromGrid(context, (cell) => {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
});
},
},
{
type: 'button',
@ -567,7 +594,7 @@ async function onPageChanged(pageNumber: number) {
await refreshCustomEmojis();
}
function onGridEvent(event: GridEvent, currentState: GridContext) {
function onGridEvent(event: GridEvent) {
switch (event.type) {
case 'cell-validation':
onGridCellValidation(event);
@ -575,9 +602,6 @@ function onGridEvent(event: GridEvent, currentState: GridContext) {
case 'cell-value-change':
onGridCellValueChange(event);
break;
case 'keydown':
onGridKeyDown(event, currentState);
break;
}
}
@ -592,74 +616,6 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
}
}
async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
function roleIdConverter(value: string): CellValue {
try {
const obj = JSON.parse(value);
if (!Array.isArray(obj)) {
return [];
}
if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
return [];
}
return obj.map(it => ({ id: it.id, name: it.name }));
} catch (ex) {
return [];
}
}
const { ctrlKey, shiftKey, code } = event.event;
switch (true) {
case ctrlKey && shiftKey: {
break;
}
case ctrlKey: {
switch (code) {
case 'KeyC': {
optInGridUtils.copyToClipboard(gridItems, currentState);
break;
}
case 'KeyV': {
await optInGridUtils.pasteFromClipboard(
gridItems,
currentState,
[
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', converter: roleIdConverter },
],
);
break;
}
}
break;
}
case shiftKey: {
break;
}
default: {
switch (code) {
case 'Delete': {
if (currentState.rangedRows.length > 0) {
for (const row of currentState.rangedRows) {
gridItems.value[row.index].checked = true;
}
} else {
const ranges = currentState.rangedCells;
for (const cell of ranges) {
if (cell.column.setting.editable) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;
}
}
}
break;
}
}
break;
}
}
}
async function refreshCustomEmojis() {
const limit = 100;

View File

@ -87,18 +87,12 @@ import * as os from '@/os.js';
import { validators } from '@/components/grid/cell-validators.js';
import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
import { uploadFile } from '@/scripts/upload.js';
import {
GridCellValidationEvent,
GridCellValueChangeEvent,
GridContext,
GridEvent,
GridKeyDownEvent,
} from '@/components/grid/grid-event.js';
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.local.logs.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import { GridSetting } from '@/components/grid/grid.js';
import { CellValue } from '@/components/grid/cell.js';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
import { GridRow } from '@/components/grid/row.js';
const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
@ -124,6 +118,11 @@ function setupGrid(): GridSetting {
const required = validators.required();
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
function removeRows(rows: GridRow[]) {
const idxes = [...new Set(rows.map(it => it.index))];
gridItems.value = gridItems.value.filter((_, i) => !idxes.includes(i));
}
return {
row: {
showNumber: true,
@ -141,16 +140,21 @@ function setupGrid(): GridSetting {
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
action: () => copyGridDataToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, context),
action: () => removeRows(context.rangedRows),
},
];
},
events: {
delete(rows) {
removeRows(rows);
},
},
},
cols: [
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
@ -195,13 +199,13 @@ function setupGrid(): GridSetting {
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
action: () => copyGridDataToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, context),
action: () => removeRows(context.rangedCells.map(it => it.row)),
},
];
},
@ -359,7 +363,7 @@ async function onDriveSelectClicked() {
gridItems.value.push(...driveFiles.map(fromDriveFile));
}
function onGridEvent(event: GridEvent, currentState: GridContext) {
function onGridEvent(event: GridEvent) {
switch (event.type) {
case 'cell-validation':
onGridCellValidation(event);
@ -367,9 +371,6 @@ function onGridEvent(event: GridEvent, currentState: GridContext) {
case 'cell-value-change':
onGridCellValueChange(event);
break;
case 'keydown':
onGridKeyDown(event, currentState);
break;
}
}
@ -384,63 +385,6 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
}
}
async function onGridKeyDown(event: GridKeyDownEvent, context: GridContext) {
function roleIdConverter(value: string): CellValue {
try {
const obj = JSON.parse(value);
if (!Array.isArray(obj)) {
return [];
}
if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
return [];
}
return obj.map(it => ({ id: it.id, name: it.name }));
} catch (ex) {
return [];
}
}
const { ctrlKey, shiftKey, code } = event.event;
switch (true) {
case ctrlKey && shiftKey: {
break;
}
case ctrlKey: {
switch (code) {
case 'KeyC': {
optInGridUtils.copyToClipboard(gridItems, context);
break;
}
case 'KeyV': {
await optInGridUtils.pasteFromClipboard(
gridItems,
context,
[
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', converter: roleIdConverter },
],
);
break;
}
}
break;
}
case shiftKey: {
break;
}
default: {
switch (code) {
case 'Delete': {
optInGridUtils.deleteSelectionRange(gridItems, context);
break;
}
}
break;
}
}
}
function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
return {
fileId: it.id,

View File

@ -9,7 +9,6 @@
<MkGrid
:data="filteredLogs"
:settings="setupGrid()"
@event="onGridEvent"
/>
</div>
<div v-else>
@ -27,15 +26,10 @@
import { computed, ref, toRefs } from 'vue';
import { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
import {
GridContext,
GridEvent,
GridKeyDownEvent,
} from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { GridSetting } from '@/components/grid/grid.js';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
function setupGrid(): GridSetting {
return {
@ -48,7 +42,7 @@ function setupGrid(): GridSetting {
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(logs, context),
action: () => copyGridDataToClipboard(logs, context),
},
];
},
@ -66,7 +60,7 @@ function setupGrid(): GridSetting {
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(logs, context),
action: () => copyGridDataToClipboard(logs, context),
},
];
},
@ -86,18 +80,6 @@ const filteredLogs = computed(() => {
return logs.value.filter((log) => forceShowing || log.failed);
});
function onGridEvent(event: GridEvent, currentState: GridContext) {
switch (event.type) {
case 'keydown':
onGridKeyDown(event, currentState);
break;
}
}
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
optInGridUtils.defaultKeyDownHandler(logs, event, currentState);
}
</script>
<style module lang="scss">

View File

@ -117,9 +117,8 @@ import MkInput from '@/components/MkInput.vue';
import MkGrid from '@/components/grid/MkGrid.vue';
import { emptyStrToUndefined, RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
import { GridCellValueChangeEvent, GridContext, GridEvent, GridKeyDownEvent } from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkFolder from '@/components/MkFolder.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.local.logs.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import * as os from '@/os.js';
import { GridSetting } from '@/components/grid/grid.js';
import MkTagItem from '@/components/MkTagItem.vue';

View File

@ -117,16 +117,16 @@ export type KeyEventHandler = {
}
export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) {
function checkModifier(event: KeyboardEvent, modifiers? : KeyModifier[]) {
function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) {
if (modifiers) {
return modifiers.every(modifier => event.getModifierState(modifier));
return modifiers.every(modifier => ev.getModifierState(modifier));
}
return true;
}
function checkState(event: KeyboardEvent, states?: KeyState[]) {
function checkState(ev: KeyboardEvent, states?: KeyState[]) {
if (states) {
return states.every(state => event.getModifierState(state));
return states.every(state => ev.getModifierState(state));
}
return true;
}