diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index a799d30578..36b7d04d3b 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -48,7 +48,7 @@ import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell, resetC import { equalCellAddress, getCellAddress, getCellElement } from '@/components/grid/grid-utils.js'; import { MenuItem } from '@/types/menu.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 { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js'; @@ -59,7 +59,7 @@ type RowHolder = { } const emit = defineEmits<{ - (ev: 'event', event: GridEvent, current: GridCurrentState): void; + (ev: 'event', event: GridEvent, current: GridContext): void; }>(); const props = defineProps<{ @@ -76,6 +76,9 @@ const rowSetting: Required = { // non-reactive const columnSettings = props.settings.cols; +// non-reactive +const cellSettings = props.settings.cells ?? {}; + const { data } = toRefs(props); // #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}`); } + const context = createContext(); const menuItems = Array.of(); - - // 外でメニュー項目を挿してもらう - 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 }); + switch (true) { + case availableCellAddress(cellAddress): { + const cell = cells.value[cellAddress.row].cells[cellAddress.col]; + if (cell.setting.contextMenuFactory) { + menuItems.push(...cell.setting.contextMenuFactory(cell.column, cell.row, cell.value, context)); + } + break; + } + 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) { @@ -835,7 +853,7 @@ function calcLargestCellWidth(column: GridColumn) { * {@link emit}を使用してイベントを発行する。 */ function emitGridEvent(ev: GridEvent) { - const currentState: GridCurrentState = { + const currentState: GridContext = { selectedCell: selectedCell.value, rangedCells: rangedCells.value, rangedRows: rangedRows.value, @@ -1037,6 +1055,19 @@ function unregisterMouseUp() { 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() { if (_DEV_) { console.log('[grid][refresh-data][begin]'); @@ -1044,8 +1075,8 @@ function refreshData() { const _data: DataSource[] = data.value; const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount) - ? _data.map((_, index) => createRow(index, true)) - : Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length)); + ? _data.map((_, index) => createRow(index, true, rowSetting)) + : Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length, rowSetting)); const _cols: GridColumn[] = columns.value; // 行・列の定義から、元データの配列より値を取得してセルを作成する。 @@ -1053,11 +1084,11 @@ function refreshData() { const _cells: RowHolder[] = _rows.map(row => { const cells = row.using ? _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); return cell; }) - : _cols.map(col => createCell(col, row, undefined)); + : _cols.map(col => createCell(col, row, undefined, cellSettings)); 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++) { - const newRow = createRow(rowIdx, true); + const newRow = createRow(rowIdx, true, rowSetting); newRows.push(newRow); newCells.push({ 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], }); } diff --git a/packages/frontend/src/components/grid/cell.ts b/packages/frontend/src/components/grid/cell.ts index bb7c006433..bae4c92347 100644 --- a/packages/frontend/src/components/grid/cell.ts +++ b/packages/frontend/src/components/grid/cell.ts @@ -2,6 +2,8 @@ import { ValidateViolation } from '@/components/grid/cell-validators.js'; import { Size } from '@/components/grid/grid.js'; import { GridColumn } from '@/components/grid/column.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 @@ -23,13 +25,21 @@ export type GridCell = { selected: boolean; ranged: boolean; contentSize: Size; + setting: GridCellSetting; violation: ValidateViolation; } +export type GridCellContextMenuFactory = (col: GridColumn, row: GridRow, value: CellValue, context: GridContext) => MenuItem[]; + +export type GridCellSetting = { + contextMenuFactory?: GridCellContextMenuFactory; +} + export function createCell( column: GridColumn, row: GridRow, value: CellValue, + setting: GridCellSetting, ): GridCell { return { address: { row: row.index, col: column.index }, @@ -48,6 +58,7 @@ export function createCell( }, violations: [], }, + setting, }; } diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts index 42f855e17c..b70ee2fb6c 100644 --- a/packages/frontend/src/components/grid/column.ts +++ b/packages/frontend/src/components/grid/column.ts @@ -1,9 +1,16 @@ 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 { 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 CellValueConverter = (row: GridRow, col: GridColumn, value: CellValue) => CellValue; +export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[]; + export type GridColumnSetting = { bindTo: string; title?: string; @@ -12,6 +19,8 @@ export type GridColumnSetting = { width: SizeStyle; editable?: boolean; validators?: CellValidator[]; + valueConverter?: CellValueConverter; + contextMenuFactory?: GridColumnContextMenuFactory; }; export type GridColumn = { diff --git a/packages/frontend/src/components/grid/grid-event.ts b/packages/frontend/src/components/grid/grid-event.ts index 1278bf8770..ebb32fee2c 100644 --- a/packages/frontend/src/components/grid/grid-event.ts +++ b/packages/frontend/src/components/grid/grid-event.ts @@ -1,11 +1,10 @@ import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js'; import { GridState } from '@/components/grid/grid.js'; import { ValidateViolation } from '@/components/grid/cell-validators.js'; -import { MenuItem } from '@/types/menu.js'; import { GridColumn } from '@/components/grid/column.js'; import { GridRow } from '@/components/grid/row.js'; -export type GridCurrentState = { +export type GridContext = { selectedCell?: GridCell; rangedCells: GridCell[]; rangedRows: GridRow[]; @@ -26,10 +25,7 @@ export type GridEvent = GridCellValueChangeEvent | GridKeyDownEvent | GridMouseDownEvent | - GridCellValidationEvent | - GridCellContextMenuEvent | - GridRowContextMenuEvent | - GridColumnContextMenuEvent + GridCellValidationEvent ; export type GridCellValueChangeEvent = { @@ -57,21 +53,3 @@ export type GridMouseDownEvent = { 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[]; -}; diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts index 41a092f67e..708984f330 100644 --- a/packages/frontend/src/components/grid/grid.ts +++ b/packages/frontend/src/components/grid/grid.ts @@ -1,11 +1,12 @@ 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 { GridRowSetting } from '@/components/grid/row.js'; export type GridSetting = { row?: GridRowSetting; cols: GridColumnSetting[]; + cells?: GridCellSetting; }; export type DataSource = Record; diff --git a/packages/frontend/src/components/grid/optin-utils.ts b/packages/frontend/src/components/grid/optin-utils.ts index b349d23623..e730a5c695 100644 --- a/packages/frontend/src/components/grid/optin-utils.ts +++ b/packages/frontend/src/components/grid/optin-utils.ts @@ -1,12 +1,12 @@ 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 { 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, event: GridKeyDownEvent, currentState: GridCurrentState) { + async defaultKeyDownHandler(gridItems: Ref, event: GridKeyDownEvent, context: GridContext) { const { ctrlKey, shiftKey, code } = event.event; switch (true) { @@ -16,11 +16,11 @@ class OptInGridUtils { case ctrlKey: { switch (code) { case 'KeyC': { - this.copyToClipboard(gridItems, currentState); + this.copyToClipboard(gridItems, context); break; } case 'KeyV': { - await this.pasteFromClipboard(gridItems, currentState); + await this.pasteFromClipboard(gridItems, context); break; } } @@ -32,7 +32,7 @@ class OptInGridUtils { default: { switch (code) { case 'Delete': { - this.deleteSelectionRange(gridItems, currentState); + this.deleteSelectionRange(gridItems, context); break; } } @@ -41,14 +41,14 @@ class OptInGridUtils { } } - copyToClipboard(gridItems: Ref, currentState: GridCurrentState) { + copyToClipboard(gridItems: Ref, context: GridContext) { const lines = Array.of(); - const bounds = currentState.randedBounds; + const bounds = context.randedBounds; for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { const items = Array.of(); 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]; items.push(cell?.toString() ?? ''); } @@ -65,7 +65,7 @@ class OptInGridUtils { async pasteFromClipboard( gridItems: Ref, - currentState: GridCurrentState, + context: GridContext, ) { function parseValue(value: string, type: GridColumnSetting['type']): CellValue { switch (type) { @@ -86,14 +86,14 @@ class OptInGridUtils { console.log(`Paste from clipboard: ${clipBoardText}`); } - const bounds = currentState.randedBounds; + 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 = currentState.rangedCells; + 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.type); } @@ -101,7 +101,7 @@ class OptInGridUtils { // 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける const offsetRow = bounds.leftTop.row; const offsetCol = bounds.leftTop.col; - const columns = currentState.columns; + const columns = context.columns; for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { const rowIdx = row - offsetRow; if (lines.length <= rowIdx) { @@ -123,12 +123,12 @@ class OptInGridUtils { } } - deleteSelectionRange(gridItems: Ref, currentState: GridCurrentState) { - if (currentState.rangedRows.length > 0) { - const deletedIndexes = currentState.rangedRows.map(it => it.index); + deleteSelectionRange(gridItems: Ref, 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 = currentState.rangedCells; + const ranges = context.rangedCells; for (const cell of ranges) { if (cell.column.setting.editable) { gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined; diff --git a/packages/frontend/src/components/grid/row.ts b/packages/frontend/src/components/grid/row.ts index 32ae31c43d..4f6898ade8 100644 --- a/packages/frontend/src/components/grid/row.ts +++ b/packages/frontend/src/components/grid/row.ts @@ -1,12 +1,15 @@ import { AdditionalStyle } from '@/components/grid/grid.js'; import { GridCell } from '@/components/grid/cell.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 = { showNumber: true, selectable: true, minimumDefinitionCount: 100, styleRules: [], + contextMenuFactory: () => [], }; export type GridRowStyleRuleConditionParams = { @@ -20,25 +23,30 @@ export type GridRowStyleRule = { applyStyle: AdditionalStyle; } +export type GridRowContextMenuFactory = (row: GridRow, context: GridContext) => MenuItem[]; + export type GridRowSetting = { showNumber?: boolean; selectable?: boolean; minimumDefinitionCount?: number; styleRules?: GridRowStyleRule[]; + contextMenuFactory?: GridRowContextMenuFactory; } export type GridRow = { index: number; ranged: boolean; using: boolean; + setting: GridRowSetting; additionalStyles: AdditionalStyle[]; } -export function createRow(index: number, using: boolean): GridRow { +export function createRow(index: number, using: boolean, setting: GridRowSetting): GridRow { return { index, ranged: false, using: using, + setting, additionalStyles: [], }; } diff --git a/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue index 010c0559c7..306ead4e3a 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue @@ -137,13 +137,11 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import { validators } from '@/components/grid/cell-validators.js'; import { - GridCellContextMenuEvent, GridCellValidationEvent, GridCellValueChangeEvent, - GridCurrentState, + GridContext, GridEvent, GridKeyDownEvent, - GridRowContextMenuEvent, } from '@/components/grid/grid-event.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -209,6 +207,26 @@ function setupGrid(): GridSetting { 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: [ { 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: '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(); } -function onGridEvent(event: GridEvent, currentState: GridCurrentState) { +function onGridEvent(event: GridEvent, currentState: GridContext) { switch (event.type) { case 'cell-validation': onGridCellValidation(event); break; - case 'row-context-menu': - onGridRowContextMenu(event, currentState); - break; - case 'cell-context-menu': - onGridCellContextMenu(event, currentState); - break; case 'cell-value-change': onGridCellValueChange(event); break; @@ -431,54 +471,6 @@ function onGridCellValidation(event: GridCellValidationEvent) { 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) { const { row, column, newValue } = event; 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; switch (true) { diff --git a/packages/frontend/src/pages/admin/custom-emojis-grid.local.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-grid.local.logs.vue index 1a404300cf..28377dbe88 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-grid.local.logs.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-grid.local.logs.vue @@ -26,14 +26,11 @@