refine context menu setting

This commit is contained in:
samunohito 2024-02-16 07:41:29 +09:00
parent 369d5971d4
commit c0f941689b
11 changed files with 243 additions and 255 deletions

View file

@ -48,7 +48,7 @@ import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell, resetC
import { equalCellAddress, getCellAddress, getCellElement } from '@/components/grid/grid-utils.js'; import { equalCellAddress, getCellAddress, getCellElement } from '@/components/grid/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'; import { GridContext, GridEvent } from '@/components/grid/grid-event.js';
import { createColumn, GridColumn } from '@/components/grid/column.js'; import { createColumn, GridColumn } from '@/components/grid/column.js';
import { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js'; import { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js';
@ -59,7 +59,7 @@ type RowHolder = {
} }
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'event', event: GridEvent, current: GridCurrentState): void; (ev: 'event', event: GridEvent, current: GridContext): void;
}>(); }>();
const props = defineProps<{ const props = defineProps<{
@ -76,6 +76,9 @@ const rowSetting: Required<GridRowSetting> = {
// non-reactive // non-reactive
const columnSettings = props.settings.cols; const columnSettings = props.settings.cols;
// non-reactive
const cellSettings = props.settings.cells ?? {};
const { data } = toRefs(props); const { data } = toRefs(props);
// #region Event Definitions // #region Event Definitions
@ -700,15 +703,30 @@ 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}`);
} }
const context = createContext();
const menuItems = Array.of<MenuItem>(); const menuItems = Array.of<MenuItem>();
switch (true) {
// case availableCellAddress(cellAddress): {
if (availableCellAddress(cellAddress)) { const cell = cells.value[cellAddress.row].cells[cellAddress.col];
emitGridEvent({ type: 'cell-context-menu', event: ev, menuItems }); if (cell.setting.contextMenuFactory) {
} else if (isRowNumberCellAddress(cellAddress)) { menuItems.push(...cell.setting.contextMenuFactory(cell.column, cell.row, cell.value, context));
emitGridEvent({ type: 'row-context-menu', event: ev, menuItems }); }
} else if (isColumnHeaderCellAddress(cellAddress)) { break;
emitGridEvent({ type: 'column-context-menu', event: ev, menuItems }); }
case isColumnHeaderCellAddress(cellAddress): {
const col = columns.value[cellAddress.col];
if (col.setting.contextMenuFactory) {
menuItems.push(...col.setting.contextMenuFactory(col, context));
}
break;
}
case isRowNumberCellAddress(cellAddress): {
const row = rows.value[cellAddress.row];
if (row.setting.contextMenuFactory) {
menuItems.push(...row.setting.contextMenuFactory(row, context));
}
break;
}
} }
if (menuItems.length > 0) { if (menuItems.length > 0) {
@ -835,7 +853,7 @@ function calcLargestCellWidth(column: GridColumn) {
* {@link emit}を使用してイベントを発行する * {@link emit}を使用してイベントを発行する
*/ */
function emitGridEvent(ev: GridEvent) { function emitGridEvent(ev: GridEvent) {
const currentState: GridCurrentState = { const currentState: GridContext = {
selectedCell: selectedCell.value, selectedCell: selectedCell.value,
rangedCells: rangedCells.value, rangedCells: rangedCells.value,
rangedRows: rangedRows.value, rangedRows: rangedRows.value,
@ -1037,6 +1055,19 @@ function unregisterMouseUp() {
removeEventListener('mouseup', onMouseUp); removeEventListener('mouseup', onMouseUp);
} }
function createContext(): GridContext {
return {
selectedCell: selectedCell.value,
rangedCells: rangedCells.value,
rangedRows: rangedRows.value,
randedBounds: rangedBounds.value,
availableBounds: availableBounds.value,
state: state.value,
rows: rows.value,
columns: columns.value,
};
}
function refreshData() { function refreshData() {
if (_DEV_) { if (_DEV_) {
console.log('[grid][refresh-data][begin]'); console.log('[grid][refresh-data][begin]');
@ -1044,8 +1075,8 @@ function refreshData() {
const _data: DataSource[] = data.value; const _data: DataSource[] = data.value;
const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount) const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount)
? _data.map((_, index) => createRow(index, true)) ? _data.map((_, index) => createRow(index, true, rowSetting))
: Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length)); : Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length, rowSetting));
const _cols: GridColumn[] = columns.value; const _cols: GridColumn[] = columns.value;
// //
@ -1053,11 +1084,11 @@ function refreshData() {
const _cells: RowHolder[] = _rows.map(row => { const _cells: RowHolder[] = _rows.map(row => {
const cells = row.using const cells = row.using
? _cols.map(col => { ? _cols.map(col => {
const cell = createCell(col, row, _data[row.index][col.setting.bindTo]); const cell = createCell(col, row, _data[row.index][col.setting.bindTo], cellSettings);
cell.violation = cellValidation(cell, cell.value); cell.violation = cellValidation(cell, cell.value);
return cell; return cell;
}) })
: _cols.map(col => createCell(col, row, undefined)); : _cols.map(col => createCell(col, row, undefined, cellSettings));
return { row, cells, origin: _data[row.index] }; return { row, cells, origin: _data[row.index] };
}); });
@ -1095,11 +1126,11 @@ function patchData(newItems: DataSource[]) {
// //
for (let rowIdx = rows.value.length; rowIdx < newItems.length; rowIdx++) { for (let rowIdx = rows.value.length; rowIdx < newItems.length; rowIdx++) {
const newRow = createRow(rowIdx, true); const newRow = createRow(rowIdx, true, rowSetting);
newRows.push(newRow); newRows.push(newRow);
newCells.push({ newCells.push({
row: newRow, row: newRow,
cells: _cols.map(col => createCell(col, newRow, newItems[rowIdx][col.setting.bindTo], col.setting.cellSetting ?? {})), cells: _cols.map(col => createCell(col, newRow, newItems[rowIdx][col.setting.bindTo], cellSettings)),
origin: newItems[rowIdx], origin: newItems[rowIdx],
}); });
} }

View file

@ -2,6 +2,8 @@ import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { Size } from '@/components/grid/grid.js'; import { Size } from '@/components/grid/grid.js';
import { GridColumn } from '@/components/grid/column.js'; import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js'; import { GridRow } from '@/components/grid/row.js';
import { MenuItem } from '@/types/menu.js';
import { GridContext } from '@/components/grid/grid-event.js';
export type CellValue = string | boolean | number | undefined | null export type CellValue = string | boolean | number | undefined | null
@ -23,13 +25,21 @@ export type GridCell = {
selected: boolean; selected: boolean;
ranged: boolean; ranged: boolean;
contentSize: Size; contentSize: Size;
setting: GridCellSetting;
violation: ValidateViolation; violation: ValidateViolation;
} }
export type GridCellContextMenuFactory = (col: GridColumn, row: GridRow, value: CellValue, context: GridContext) => MenuItem[];
export type GridCellSetting = {
contextMenuFactory?: GridCellContextMenuFactory;
}
export function createCell( export function createCell(
column: GridColumn, column: GridColumn,
row: GridRow, row: GridRow,
value: CellValue, value: CellValue,
setting: GridCellSetting,
): GridCell { ): GridCell {
return { return {
address: { row: row.index, col: column.index }, address: { row: row.index, col: column.index },
@ -48,6 +58,7 @@ export function createCell(
}, },
violations: [], violations: [],
}, },
setting,
}; };
} }

View file

@ -1,9 +1,16 @@
import { CellValidator } from '@/components/grid/cell-validators.js'; import { CellValidator } from '@/components/grid/cell-validators.js';
import { Size, SizeStyle } from '@/components/grid/grid.js'; import { Size, SizeStyle } from '@/components/grid/grid.js';
import { calcCellWidth } from '@/components/grid/grid-utils.js'; import { calcCellWidth } from '@/components/grid/grid-utils.js';
import { CellValue } 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';
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden'; export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden';
export type CellValueConverter = (row: GridRow, col: GridColumn, value: CellValue) => CellValue;
export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[];
export type GridColumnSetting = { export type GridColumnSetting = {
bindTo: string; bindTo: string;
title?: string; title?: string;
@ -12,6 +19,8 @@ export type GridColumnSetting = {
width: SizeStyle; width: SizeStyle;
editable?: boolean; editable?: boolean;
validators?: CellValidator[]; validators?: CellValidator[];
valueConverter?: CellValueConverter;
contextMenuFactory?: GridColumnContextMenuFactory;
}; };
export type GridColumn = { export type GridColumn = {

View file

@ -1,11 +1,10 @@
import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { GridState } from '@/components/grid/grid.js'; import { GridState } from '@/components/grid/grid.js';
import { ValidateViolation } from '@/components/grid/cell-validators.js'; import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { MenuItem } from '@/types/menu.js';
import { GridColumn } from '@/components/grid/column.js'; import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js'; import { GridRow } from '@/components/grid/row.js';
export type GridCurrentState = { export type GridContext = {
selectedCell?: GridCell; selectedCell?: GridCell;
rangedCells: GridCell[]; rangedCells: GridCell[];
rangedRows: GridRow[]; rangedRows: GridRow[];
@ -26,10 +25,7 @@ export type GridEvent =
GridCellValueChangeEvent | GridCellValueChangeEvent |
GridKeyDownEvent | GridKeyDownEvent |
GridMouseDownEvent | GridMouseDownEvent |
GridCellValidationEvent | GridCellValidationEvent
GridCellContextMenuEvent |
GridRowContextMenuEvent |
GridColumnContextMenuEvent
; ;
export type GridCellValueChangeEvent = { export type GridCellValueChangeEvent = {
@ -57,21 +53,3 @@ export type GridMouseDownEvent = {
event: MouseEvent; event: MouseEvent;
clickedCellAddress: CellAddress; 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,11 +1,12 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { CellValue } from '@/components/grid/cell.js'; import { CellValue, GridCellSetting } from '@/components/grid/cell.js';
import { GridColumnSetting } from '@/components/grid/column.js'; import { GridColumnSetting } from '@/components/grid/column.js';
import { GridRowSetting } from '@/components/grid/row.js'; import { GridRowSetting } from '@/components/grid/row.js';
export type GridSetting = { export type GridSetting = {
row?: GridRowSetting; row?: GridRowSetting;
cols: GridColumnSetting[]; cols: GridColumnSetting[];
cells?: GridCellSetting;
}; };
export type DataSource = Record<string, CellValue>; export type DataSource = Record<string, CellValue>;

View file

@ -1,12 +1,12 @@
import { Ref } from 'vue'; import { Ref } from 'vue';
import { GridCurrentState, GridKeyDownEvent } from '@/components/grid/grid-event.js'; import { GridContext, GridKeyDownEvent } from '@/components/grid/grid-event.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { GridColumnSetting } from '@/components/grid/column.js'; import { GridColumnSetting } from '@/components/grid/column.js';
import { CellValue } from '@/components/grid/cell.js'; import { CellValue } from '@/components/grid/cell.js';
import { DataSource } from '@/components/grid/grid.js'; import { DataSource } from '@/components/grid/grid.js';
class OptInGridUtils { class OptInGridUtils {
async defaultKeyDownHandler(gridItems: Ref<DataSource[]>, event: GridKeyDownEvent, currentState: GridCurrentState) { async defaultKeyDownHandler(gridItems: Ref<DataSource[]>, event: GridKeyDownEvent, context: GridContext) {
const { ctrlKey, shiftKey, code } = event.event; const { ctrlKey, shiftKey, code } = event.event;
switch (true) { switch (true) {
@ -16,11 +16,11 @@ class OptInGridUtils {
case ctrlKey: { case ctrlKey: {
switch (code) { switch (code) {
case 'KeyC': { case 'KeyC': {
this.copyToClipboard(gridItems, currentState); this.copyToClipboard(gridItems, context);
break; break;
} }
case 'KeyV': { case 'KeyV': {
await this.pasteFromClipboard(gridItems, currentState); await this.pasteFromClipboard(gridItems, context);
break; break;
} }
} }
@ -32,7 +32,7 @@ class OptInGridUtils {
default: { default: {
switch (code) { switch (code) {
case 'Delete': { case 'Delete': {
this.deleteSelectionRange(gridItems, currentState); this.deleteSelectionRange(gridItems, context);
break; break;
} }
} }
@ -41,14 +41,14 @@ class OptInGridUtils {
} }
} }
copyToClipboard(gridItems: Ref<DataSource[]>, currentState: GridCurrentState) { copyToClipboard(gridItems: Ref<DataSource[]>, context: GridContext) {
const lines = Array.of<string>(); const lines = Array.of<string>();
const bounds = currentState.randedBounds; const bounds = context.randedBounds;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const items = Array.of<string>(); const items = Array.of<string>();
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) { for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const bindTo = currentState.columns[col].setting.bindTo; const bindTo = context.columns[col].setting.bindTo;
const cell = gridItems.value[row][bindTo]; const cell = gridItems.value[row][bindTo];
items.push(cell?.toString() ?? ''); items.push(cell?.toString() ?? '');
} }
@ -65,7 +65,7 @@ class OptInGridUtils {
async pasteFromClipboard( async pasteFromClipboard(
gridItems: Ref<DataSource[]>, gridItems: Ref<DataSource[]>,
currentState: GridCurrentState, context: GridContext,
) { ) {
function parseValue(value: string, type: GridColumnSetting['type']): CellValue { function parseValue(value: string, type: GridColumnSetting['type']): CellValue {
switch (type) { switch (type) {
@ -86,14 +86,14 @@ class OptInGridUtils {
console.log(`Paste from clipboard: ${clipBoardText}`); console.log(`Paste from clipboard: ${clipBoardText}`);
} }
const bounds = currentState.randedBounds; const bounds = context.randedBounds;
const lines = clipBoardText.replace(/\r/g, '') const lines = clipBoardText.replace(/\r/g, '')
.split('\n') .split('\n')
.map(it => it.split('\t')); .map(it => it.split('\t'));
if (lines.length === 1 && lines[0].length === 1) { if (lines.length === 1 && lines[0].length === 1) {
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける // 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
const ranges = currentState.rangedCells; const ranges = context.rangedCells;
for (const cell of ranges) { for (const cell of ranges) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type); gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type);
} }
@ -101,7 +101,7 @@ class OptInGridUtils {
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける // 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
const offsetRow = bounds.leftTop.row; const offsetRow = bounds.leftTop.row;
const offsetCol = bounds.leftTop.col; const offsetCol = bounds.leftTop.col;
const columns = currentState.columns; const columns = context.columns;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const rowIdx = row - offsetRow; const rowIdx = row - offsetRow;
if (lines.length <= rowIdx) { if (lines.length <= rowIdx) {
@ -123,12 +123,12 @@ class OptInGridUtils {
} }
} }
deleteSelectionRange(gridItems: Ref<DataSource[]>, currentState: GridCurrentState) { deleteSelectionRange(gridItems: Ref<DataSource[]>, context: GridContext) {
if (currentState.rangedRows.length > 0) { if (context.rangedRows.length > 0) {
const deletedIndexes = currentState.rangedRows.map(it => it.index); const deletedIndexes = context.rangedRows.map(it => it.index);
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index)); gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));
} else { } else {
const ranges = currentState.rangedCells; const ranges = context.rangedCells;
for (const cell of ranges) { for (const cell of ranges) {
if (cell.column.setting.editable) { if (cell.column.setting.editable) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined; gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined;

View file

@ -1,12 +1,15 @@
import { AdditionalStyle } from '@/components/grid/grid.js'; import { AdditionalStyle } from '@/components/grid/grid.js';
import { GridCell } from '@/components/grid/cell.js'; import { GridCell } from '@/components/grid/cell.js';
import { GridColumn } from '@/components/grid/column.js'; import { GridColumn } from '@/components/grid/column.js';
import { MenuItem } from '@/types/menu.js';
import { GridContext } from '@/components/grid/grid-event.js';
export const defaultGridRowSetting: Required<GridRowSetting> = { export const defaultGridRowSetting: Required<GridRowSetting> = {
showNumber: true, showNumber: true,
selectable: true, selectable: true,
minimumDefinitionCount: 100, minimumDefinitionCount: 100,
styleRules: [], styleRules: [],
contextMenuFactory: () => [],
}; };
export type GridRowStyleRuleConditionParams = { export type GridRowStyleRuleConditionParams = {
@ -20,25 +23,30 @@ export type GridRowStyleRule = {
applyStyle: AdditionalStyle; applyStyle: AdditionalStyle;
} }
export type GridRowContextMenuFactory = (row: GridRow, context: GridContext) => MenuItem[];
export type GridRowSetting = { export type GridRowSetting = {
showNumber?: boolean; showNumber?: boolean;
selectable?: boolean; selectable?: boolean;
minimumDefinitionCount?: number; minimumDefinitionCount?: number;
styleRules?: GridRowStyleRule[]; styleRules?: GridRowStyleRule[];
contextMenuFactory?: GridRowContextMenuFactory;
} }
export type GridRow = { export type GridRow = {
index: number; index: number;
ranged: boolean; ranged: boolean;
using: boolean; using: boolean;
setting: GridRowSetting;
additionalStyles: AdditionalStyle[]; additionalStyles: AdditionalStyle[];
} }
export function createRow(index: number, using: boolean): GridRow { export function createRow(index: number, using: boolean, setting: GridRowSetting): GridRow {
return { return {
index, index,
ranged: false, ranged: false,
using: using, using: using,
setting,
additionalStyles: [], additionalStyles: [],
}; };
} }

View file

@ -137,13 +137,11 @@ import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { validators } from '@/components/grid/cell-validators.js'; import { validators } from '@/components/grid/cell-validators.js';
import { import {
GridCellContextMenuEvent,
GridCellValidationEvent, GridCellValidationEvent,
GridCellValueChangeEvent, GridCellValueChangeEvent,
GridCurrentState, GridContext,
GridEvent, GridEvent,
GridKeyDownEvent, GridKeyDownEvent,
GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js'; } from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -209,6 +207,26 @@ function setupGrid(): GridSetting {
applyStyle: { className: 'violationRow' }, applyStyle: { className: 'violationRow' },
}, },
], ],
contextMenuFactory: (row, context) => {
return [
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
for (const row of context.rangedRows) {
gridItems.value[row.index].checked = true;
}
},
},
];
},
}, },
cols: [ cols: [
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, { bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
@ -224,6 +242,34 @@ function setupGrid(): GridSetting {
{ bindTo: 'publicUrl', type: 'text', editable: false, width: 180 }, { bindTo: 'publicUrl', type: 'text', editable: false, width: 180 },
{ bindTo: 'originalUrl', type: 'text', editable: false, width: 180 }, { bindTo: 'originalUrl', type: 'text', editable: false, width: 180 },
], ],
cells: {
contextMenuFactory: (col, row, value, context) => {
return [
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択範囲を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, context),
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
for (const rowIdx of [...new Set(context.rangedCells.map(it => it.row.index)).values()]) {
gridItems.value[rowIdx].checked = true;
}
},
},
];
},
},
}; };
} }
@ -407,17 +453,11 @@ async function onPageChanged(pageNumber: number) {
await refreshCustomEmojis(); await refreshCustomEmojis();
} }
function onGridEvent(event: GridEvent, currentState: GridCurrentState) { function onGridEvent(event: GridEvent, currentState: GridContext) {
switch (event.type) { switch (event.type) {
case 'cell-validation': case 'cell-validation':
onGridCellValidation(event); onGridCellValidation(event);
break; break;
case 'row-context-menu':
onGridRowContextMenu(event, currentState);
break;
case 'cell-context-menu':
onGridCellContextMenu(event, currentState);
break;
case 'cell-value-change': case 'cell-value-change':
onGridCellValueChange(event); onGridCellValueChange(event);
break; break;
@ -431,54 +471,6 @@ function onGridCellValidation(event: GridCellValidationEvent) {
updateButtonDisabled.value = event.all.filter(it => !it.valid).length > 0; updateButtonDisabled.value = event.all.filter(it => !it.valid).length > 0;
} }
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
for (const row of currentState.rangedRows) {
gridItems.value[row.index].checked = true;
}
},
},
);
}
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
},
{
type: 'button',
text: '選択範囲を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
},
{
type: 'button',
text: '選択行を削除対象とする',
icon: 'ti ti-trash',
action: () => {
for (const rowIdx of [...new Set(currentState.rangedCells.map(it => it.row.index)).values()]) {
gridItems.value[rowIdx].checked = true;
}
},
},
);
}
function onGridCellValueChange(event: GridCellValueChangeEvent) { function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event; const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
@ -492,7 +484,7 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
} }
} }
async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) { async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
const { ctrlKey, shiftKey, code } = event.event; const { ctrlKey, shiftKey, code } = event.event;
switch (true) { switch (true) {

View file

@ -26,14 +26,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, toRefs } from 'vue'; import { computed, ref, toRefs } from 'vue';
import { GridColumnSetting } from '@/components/grid/column.js';
import { RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import { import {
GridCellContextMenuEvent, GridContext,
GridCurrentState,
GridEvent, GridEvent,
GridKeyDownEvent, GridKeyDownEvent,
GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js'; } from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
@ -45,6 +42,16 @@ function setupGrid(): GridSetting {
row: { row: {
showNumber: false, showNumber: false,
selectable: false, selectable: false,
contextMenuFactory: (row, context) => {
return [
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(logs, context),
},
];
},
}, },
cols: [ cols: [
{ bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 }, { bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 },
@ -52,6 +59,18 @@ function setupGrid(): GridSetting {
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 }, { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 },
{ bindTo: 'error', title: 'log', type: 'text', editable: false, width: 'auto' }, { bindTo: 'error', title: 'log', type: 'text', editable: false, width: 'auto' },
], ],
cells: {
contextMenuFactory: (col, row, value, context) => {
return [
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(logs, context),
},
];
},
},
}; };
} }
@ -67,55 +86,15 @@ const filteredLogs = computed(() => {
return logs.value.filter((log) => forceShowing || log.failed); return logs.value.filter((log) => forceShowing || log.failed);
}); });
function onGridEvent(event: GridEvent, currentState: GridCurrentState) { function onGridEvent(event: GridEvent, currentState: GridContext) {
switch (event.type) { switch (event.type) {
case 'row-context-menu':
onGridRowContextMenu(event, currentState);
break;
case 'cell-context-menu':
onGridCellContextMenu(event, currentState);
break;
case 'keydown': case 'keydown':
onGridKeyDown(event, currentState); onGridKeyDown(event, currentState);
break; break;
} }
} }
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) { function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
event.menuItems.push(
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(logs, currentState),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(logs, currentState),
},
);
}
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(logs, currentState),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(logs, currentState),
},
);
}
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
optInGridUtils.defaultKeyDownHandler(logs, event, currentState); optInGridUtils.defaultKeyDownHandler(logs, event, currentState);
} }

View file

@ -88,13 +88,11 @@ import { validators } 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 { import {
GridCellContextMenuEvent,
GridCellValidationEvent, GridCellValidationEvent,
GridCellValueChangeEvent, GridCellValueChangeEvent,
GridCurrentState, GridContext,
GridEvent, GridEvent,
GridKeyDownEvent, GridKeyDownEvent,
GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js'; } from '@/components/grid/grid-event.js';
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js'; import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
@ -136,6 +134,22 @@ function setupGrid(): GridSetting {
applyStyle: { className: 'violationRow' }, applyStyle: { className: 'violationRow' },
}, },
], ],
contextMenuFactory: (row, context) => {
return [
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, context),
},
];
},
}, },
cols: [ cols: [
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] }, { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
@ -147,6 +161,24 @@ function setupGrid(): GridSetting {
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 100 }, { bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 100 },
], ],
cells: {
contextMenuFactory: (col, row, value, context) => {
return [
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, context),
},
];
},
},
}; };
} }
@ -300,17 +332,11 @@ async function onDriveSelectClicked() {
gridItems.value.push(...driveFiles.map(fromDriveFile)); gridItems.value.push(...driveFiles.map(fromDriveFile));
} }
function onGridEvent(event: GridEvent, currentState: GridCurrentState) { function onGridEvent(event: GridEvent, currentState: GridContext) {
switch (event.type) { switch (event.type) {
case 'cell-validation': case 'cell-validation':
onGridCellValidation(event); onGridCellValidation(event);
break; break;
case 'row-context-menu':
onGridRowContextMenu(event, currentState);
break;
case 'cell-context-menu':
onGridCellContextMenu(event, currentState);
break;
case 'cell-value-change': case 'cell-value-change':
onGridCellValueChange(event); onGridCellValueChange(event);
break; break;
@ -324,40 +350,6 @@ function onGridCellValidation(event: GridCellValidationEvent) {
registerButtonDisabled.value = event.all.filter(it => !it.valid).length > 0; registerButtonDisabled.value = event.all.filter(it => !it.valid).length > 0;
} }
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択行をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
},
);
}
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
},
{
type: 'button',
text: '選択行を削除',
icon: 'ti ti-trash',
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
},
);
}
function onGridCellValueChange(event: GridCellValueChangeEvent) { function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event; const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
@ -365,7 +357,7 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
} }
} }
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) { function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState); optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState);
} }

View file

@ -51,16 +51,8 @@ import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { GridColumnSetting } from '@/components/grid/column.js';
import { RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import { import { GridCellValueChangeEvent, GridContext, GridEvent, GridKeyDownEvent } from '@/components/grid/grid-event.js';
GridCellContextMenuEvent,
GridCellValueChangeEvent,
GridCurrentState,
GridEvent,
GridKeyDownEvent,
GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue'; import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue';
@ -77,12 +69,42 @@ type GridItem = {
function setupGrid(): GridSetting { function setupGrid(): GridSetting {
return { return {
row: {
contextMenuFactory: (row, context) => {
return [
{
type: 'button',
text: '選択行をインポート',
icon: 'ti ti-download',
action: async () => {
const targets = context.rangedRows.map(it => gridItems.value[it.index]);
await importEmojis(targets);
},
},
];
},
},
cols: [ cols: [
{ bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 }, { bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 },
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' }, { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' }, { bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
], ],
cells: {
contextMenuFactory: (col, row, value, context) => {
return [
{
type: 'button',
text: '選択範囲の行をインポート',
icon: 'ti ti-download',
action: async () => {
const targets = context.rangedCells.map(it => gridItems.value[it.row.index]);
await importEmojis(targets);
},
},
];
},
},
}; };
} }
@ -113,14 +135,8 @@ async function onImportClicked() {
await importEmojis(targets); await importEmojis(targets);
} }
function onGridEvent(event: GridEvent, currentState: GridCurrentState) { function onGridEvent(event: GridEvent, currentState: GridContext) {
switch (event.type) { switch (event.type) {
case 'row-context-menu':
onGridRowContextMenu(event, currentState);
break;
case 'cell-context-menu':
onGridCellContextMenu(event, currentState);
break;
case 'cell-value-change': case 'cell-value-change':
onGridCellValueChange(event); onGridCellValueChange(event);
break; break;
@ -130,35 +146,6 @@ function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
} }
} }
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択行をインポート',
icon: 'ti ti-download',
action: async () => {
const targets = currentState.rangedRows.map(it => gridItems.value[it.index]);
console.log(targets);
await importEmojis(targets);
},
},
);
}
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
event.menuItems.push(
{
type: 'button',
text: '選択された絵文字をインポート',
icon: 'ti ti-download',
action: async () => {
const targets = [...new Set(currentState.rangedCells.map(it => it.row)).values()].map(it => gridItems.value[it.index]);
await importEmojis(targets);
},
},
);
}
function onGridCellValueChange(event: GridCellValueChangeEvent) { function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event; const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
@ -166,7 +153,7 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
} }
} }
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) { function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState); optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState);
} }