This commit is contained in:
samunohito 2024-02-10 11:52:34 +09:00
parent e3240c556a
commit 171b596ac7
14 changed files with 171 additions and 71 deletions

View File

@ -4,8 +4,8 @@
:class="$style.cell"
:style="{ maxWidth: cellWidth, minWidth: cellWidth }"
:tabindex="-1"
@dblclick="onCellDoubleClick"
@keydown="onCellKeyDown"
@dblclick="onCellDoubleClick"
>
<div
:class="[
@ -53,11 +53,13 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import { GridEventEmitter, Size } from '@/components/grid/grid.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
import { CellValue, GridCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
import { GridRowSetting } from '@/components/grid/row.js';
import { selectFile } from '@/scripts/select-file.js';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;
@ -67,7 +69,7 @@ const emit = defineEmits<{
}>();
const props = defineProps<{
cell: GridCell,
gridSetting: GridSetting,
gridSetting: GridRowSetting,
bus: GridEventEmitter,
}>();
@ -162,7 +164,7 @@ function unregisterOutsideMouseDown() {
removeEventListener('mousedown', onOutsideMouseDown);
}
function beginEditing() {
async function beginEditing() {
if (editing.value || !cell.value.column.setting.editable) {
return;
}
@ -174,7 +176,7 @@ function beginEditing() {
registerOutsideMouseDown();
emit('operation:beginEdit', cell.value);
nextTick(() => {
await nextTick(() => {
// input
if (inputAreaEl.value) {
(inputAreaEl.value.querySelector('*') as HTMLElement).focus();
@ -187,6 +189,13 @@ function beginEditing() {
emitValueChange(!cell.value.value);
break;
}
case 'image': {
const file = await selectFile(rootEl.value);
if (file) {
emitValueChange(JSON.stringify(file));
}
break;
}
}
}
@ -229,7 +238,7 @@ useTooltip(rootEl, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), {
showing,
content,
targetElement: rootEl.value,
targetElement: rootEl.value!,
}, {}, 'closed');
});

View File

@ -1,7 +1,16 @@
<template>
<tr :class="[$style.row, [row.ranged ? $style.ranged : {}]]">
<tr
:class="[
$style.row,
row.ranged ? $style.ranged : {},
row.additionalStyle?.className ? row.additionalStyle.className : {},
]"
:style="[
row.additionalStyle?.style ? row.additionalStyle.style : {},
]"
>
<MkNumberCell
v-if="gridSetting.rowNumberVisible"
v-if="gridSetting.showNumber"
:content="(row.index + 1).toString()"
:row="row"
/>
@ -20,11 +29,11 @@
</template>
<script setup lang="ts">
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import { GridEventEmitter, Size } from '@/components/grid/grid.js';
import MkDataCell from '@/components/grid/MkDataCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
import { CellValue, GridCell } from '@/components/grid/cell.js';
import { GridRow } from '@/components/grid/row.js';
import { GridRow, GridRowSetting } from '@/components/grid/row.js';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;
@ -35,7 +44,7 @@ const emit = defineEmits<{
defineProps<{
row: GridRow,
cells: GridCell[],
gridSetting: GridSetting,
gridSetting: GridRowSetting,
bus: GridEventEmitter,
}>();

View File

@ -40,14 +40,7 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
import {
DataSource,
defaultGridSetting,
GridEventEmitter,
GridSetting,
GridState,
Size,
} from '@/components/grid/grid.js';
import { DataSource, GridEventEmitter, GridState, Size } from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import { cellValidation } from '@/components/grid/cell-validators.js';
@ -56,8 +49,8 @@ import { equalCellAddress, getCellAddress, getCellElement } from '@/components/g
import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
import { ColumnSetting, createColumn, GridColumn } from '@/components/grid/column.js';
import { createRow, GridRow, resetRow } from '@/components/grid/row.js';
import { createColumn, GridColumn, GridColumnSetting } from '@/components/grid/column.js';
import { createRow, defaultGridSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js';
type RowHolder = {
row: GridRow,
@ -70,13 +63,13 @@ const emit = defineEmits<{
}>();
const props = defineProps<{
gridSetting?: GridSetting,
columnSettings: ColumnSetting[],
gridSetting?: GridRowSetting,
columnSettings: GridColumnSetting[],
data: DataSource[]
}>();
// non-reactive
const gridSetting: Required<GridSetting> = {
const gridSetting: Required<GridRowSetting> = {
...props.gridSetting,
...defaultGridSetting,
};
@ -856,8 +849,7 @@ function emitGridEvent(ev: GridEvent) {
emit(
'event',
ev,
//
JSON.parse(JSON.stringify(currentState)),
currentState,
);
}
@ -980,7 +972,7 @@ function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
* {@link top}から{@link bottom}までの行を範囲選択状態にする
*/
function expandRowRange(top: number, bottom: number) {
if (!gridSetting.rowSelectable) {
if (!gridSetting.selectable) {
return;
}
@ -1026,9 +1018,9 @@ function refreshData() {
}
const _data: DataSource[] = data.value;
const _rows: GridRow[] = (_data.length > gridSetting.rowMinimumDefinitionCount)
const _rows: GridRow[] = (_data.length > gridSetting.minimumDefinitionCount)
? _data.map((_, index) => createRow(index, true))
: Array.from({ length: gridSetting.rowMinimumDefinitionCount }, (_, index) => createRow(index, index < _data.length));
: Array.from({ length: gridSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length));
const _cols: GridColumn[] = columns.value;
//
@ -1036,11 +1028,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], col.setting.cellSetting ?? {});
cell.violation = cellValidation(cell, cell.value);
return cell;
})
: _cols.map(col => createCell(col, row, undefined));
: _cols.map(col => createCell(col, row, undefined, col.setting.cellSetting ?? {}));
return { row, cells, origin: _data[row.index] };
});
@ -1080,7 +1072,7 @@ function patchData(newItems: DataSource[]) {
newRows.push(newRow);
newCells.push({
row: newRow,
cells: _cols.map(col => createCell(col, newRow, newItems[rowIdx][col.setting.bindTo])),
cells: _cols.map(col => createCell(col, newRow, newItems[rowIdx][col.setting.bindTo], col.setting.cellSetting ?? {})),
origin: newItems[rowIdx],
});
}

View File

@ -1,7 +1,7 @@
<template>
<tr :class="$style.header">
<MkNumberCell
v-if="gridSetting.rowNumberVisible"
v-if="gridSetting.showNumber"
content="#"
:top="true"
/>
@ -20,10 +20,11 @@
</template>
<script setup lang="ts">
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import { GridEventEmitter, Size } from '@/components/grid/grid.js';
import MkHeaderCell from '@/components/grid/MkHeaderCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
import { GridColumn } from '@/components/grid/column.js';
import { GridRowSetting } from '@/components/grid/row.js';
const emit = defineEmits<{
(ev: 'operation:beginWidthChange', sender: GridColumn): void;
@ -35,7 +36,7 @@ const emit = defineEmits<{
}>();
defineProps<{
columns: GridColumn[],
gridSetting: GridSetting,
gridSetting: GridRowSetting,
bus: GridEventEmitter,
}>();

View File

@ -1,7 +1,8 @@
import { ValidateViolation, ValidateViolationItem } from '@/components/grid/cell-validators.js';
import { Size } from '@/components/grid/grid.js';
import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { AdditionalStyle, EventOptions, 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';
export type CellValue = string | boolean | number | undefined | null

View File

@ -1,10 +1,12 @@
import { CellValidator } from '@/components/grid/cell-validators.js';
import { Size, SizeStyle } from '@/components/grid/grid.js';
import { AdditionalStyle, EventOptions, Size, SizeStyle } from '@/components/grid/grid.js';
import { calcCellWidth } from '@/components/grid/grid-utils.js';
import { CellValue, GridCell, GridCellSetting } from '@/components/grid/cell.js';
import { GridRow } from '@/components/grid/row.js';
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
export type ColumnSetting = {
export type GridColumnSetting = {
bindTo: string;
title?: string;
icon?: string;
@ -12,16 +14,25 @@ export type ColumnSetting = {
width: SizeStyle;
editable?: boolean;
validators?: CellValidator[];
valueConverter?: GridColumnValueConverter;
cellSetting?: GridCellSetting;
};
export type GridColumn = {
index: number;
setting: ColumnSetting;
setting: GridColumnSetting;
width: string;
contentSize: Size;
}
export function createColumn(setting: ColumnSetting, index: number): GridColumn {
export type GridColumnValueConverter = (row: GridRow, col: GridColumn, value: CellValue) => CellValue;
export type GridColumnEventArgs = {
col: GridColumn;
cells: GridCell[];
} & EventOptions;
export function createColumn(setting: GridColumnSetting, index: number): GridColumn {
return {
index,
setting,
@ -29,3 +40,4 @@ export function createColumn(setting: ColumnSetting, index: number): GridColumn
contentSize: { width: 0, height: 0 },
};
}

View File

@ -1,5 +1,6 @@
import { GridSetting, SizeStyle } from '@/components/grid/grid.js';
import { SizeStyle } from '@/components/grid/grid.js';
import { CELL_ADDRESS_NONE, CellAddress } from '@/components/grid/cell.js';
import { GridRowSetting } from '@/components/grid/row.js';
export function isCellElement(elem: any): elem is HTMLTableCellElement {
return elem instanceof HTMLTableCellElement;
@ -21,7 +22,7 @@ export function calcCellWidth(widthSetting: SizeStyle): string {
}
}
export function getCellAddress(elem: HTMLElement, gridSetting: GridSetting, parentNodeCount = 10): CellAddress {
export function getCellAddress(elem: HTMLElement, gridSetting: GridRowSetting, parentNodeCount = 10): CellAddress {
let node = elem;
for (let i = 0; i < parentNodeCount; i++) {
if (isCellElement(node) && isRowElement(node.parentElement)) {
@ -29,7 +30,7 @@ export function getCellAddress(elem: HTMLElement, gridSetting: GridSetting, pare
// ヘッダ行ぶんを除く
row: node.parentElement.rowIndex - 1,
// 数値列ぶんを除く
col: gridSetting.rowNumberVisible ? node.cellIndex - 1 : node.cellIndex,
col: gridSetting.showNumber ? node.cellIndex - 1 : node.cellIndex,
};
}

View File

@ -1,16 +1,11 @@
import { EventEmitter } from 'eventemitter3';
import { CellValue } from '@/components/grid/cell.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { GridRowSetting } from '@/components/grid/row.js';
export type GridSetting = {
rowNumberVisible?: boolean;
rowSelectable?: boolean;
rowMinimumDefinitionCount?: number;
}
export const defaultGridSetting: Required<GridSetting> = {
rowNumberVisible: true,
rowSelectable: true,
rowMinimumDefinitionCount: 100,
row: GridRowSetting;
cols: GridColumnSetting[];
};
export type DataSource = Record<string, CellValue>;
@ -32,6 +27,16 @@ export type Size = {
export type SizeStyle = number | 'auto' | undefined;
export type EventOptions = {
preventDefault?: boolean;
stopPropagation?: boolean;
}
export type AdditionalStyle = {
className?: string;
style?: Record<string, string | number>;
}
export class GridEventEmitter extends EventEmitter<{
'forceRefreshContentSize': void;
}> {

View File

@ -1,7 +1,7 @@
import { Ref } from 'vue';
import { GridCurrentState, GridKeyDownEvent } from '@/components/grid/grid-event.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { CellValue } from '@/components/grid/cell.js';
import { DataSource } from '@/components/grid/grid.js';
@ -67,7 +67,7 @@ class OptInGridUtils {
gridItems: Ref<DataSource[]>,
currentState: GridCurrentState,
) {
function parseValue(value: string, type: ColumnSetting['type']): CellValue {
function parseValue(value: string, type: GridColumnSetting['type']): CellValue {
switch (type) {
case 'number': {
return Number(value);

View File

@ -1,7 +1,22 @@
import { AdditionalStyle } from '@/components/grid/grid.js';
export const defaultGridSetting: Required<GridRowSetting> = {
showNumber: true,
selectable: true,
minimumDefinitionCount: 100,
};
export type GridRowSetting = {
showNumber?: boolean;
selectable?: boolean;
minimumDefinitionCount?: number;
}
export type GridRow = {
index: number;
ranged: boolean;
using: boolean;
additionalStyle?: AdditionalStyle;
}
export function createRow(index: number, using: boolean): GridRow {
@ -15,4 +30,6 @@ export function createRow(index: number, using: boolean): GridRow {
export function resetRow(row: GridRow): void {
row.ranged = false;
row.using = false;
row.additionalStyle = undefined;
}

View File

@ -113,7 +113,7 @@ import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { validators } from '@/components/grid/cell-validators.js';
import {
GridCellContextMenuEvent,
@ -125,13 +125,13 @@ import {
GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js';
import { GridSetting } from '@/components/grid/grid.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { GridRowSetting } from '@/components/grid/row.js';
type GridItem = {
checked: boolean;
@ -145,16 +145,17 @@ type GridItem = {
isSensitive: boolean;
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
fileId?: string;
}
const gridSetting: GridSetting = {
rowNumberVisible: true,
rowSelectable: false,
const gridSetting: GridRowSetting = {
showNumber: true,
selectable: false,
};
const required = validators.required();
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
const columnSettings: ColumnSetting[] = [
const columnSettings: GridColumnSetting[] = [
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required, regex] },
@ -228,6 +229,7 @@ async function onUpdateButtonClicked() {
isSensitive: item.isSensitive,
localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
fileId: item.fileId,
})
.then(() => ({ item, success: true, err: undefined }))
.catch(err => ({ item, success: false, err })),
@ -390,14 +392,32 @@ function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: Gr
function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
gridItems.value[row.index][column.setting.bindTo] = newValue;
if (column.setting.bindTo === 'url') {
const file = JSON.parse(newValue as string) as Misskey.entities.DriveFile;
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;
} else {
gridItems.value[row.index][column.setting.bindTo] = newValue;
}
const originItem = originGridItems.value[row.index][column.setting.bindTo];
if (originItem !== newValue) {
row.additionalStyle = {
className: 'editedRow',
};
} else {
row.additionalStyle = undefined;
}
}
}
async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
const { ctrlKey, code } = event.event;
const { ctrlKey, shiftKey, code } = event.event;
switch (true) {
case ctrlKey && shiftKey: {
break;
}
case ctrlKey: {
switch (code) {
case 'KeyC': {
@ -411,6 +431,37 @@ async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentS
}
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;
const originItem = originGridItems.value[cell.row.index][cell.column.setting.bindTo];
if (originItem !== undefined) {
cell.row.additionalStyle = {
className: 'editedRow',
};
} else {
cell.row.additionalStyle = undefined;
}
}
}
}
break;
}
}
break;
}
}
}
@ -430,8 +481,6 @@ async function refreshCustomEmojis() {
hostType: 'local',
};
console.log(queryUpdatedAtTo.value);
if (JSON.stringify(query) !== previousQuery.value) {
currentPage.value = 1;
}
@ -481,6 +530,10 @@ onMounted(async () => {
</script>
<style lang="scss">
.editedRow {
background-color: var(--infoBg);
}
.row1 {
grid-row: 1 / 2;
}

View File

@ -27,7 +27,7 @@
<script setup lang="ts">
import { computed, ref, toRefs } from 'vue';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import {
GridCellContextMenuEvent,
@ -40,7 +40,7 @@ import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import MkSwitch from '@/components/MkSwitch.vue';
const columnSettings: ColumnSetting[] = [
const columnSettings: GridColumnSetting[] = [
{ bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 },
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 },

View File

@ -100,7 +100,7 @@ import {
GridKeyDownEvent,
GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridColumnSetting } from '@/components/grid/column.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-grid.local.logs.vue';
@ -127,7 +127,7 @@ type GridItem = {
const required = validators.required();
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
const columnSettings: ColumnSetting[] = [
const columnSettings: GridColumnSetting[] = [
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required, regex] },
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },

View File

@ -51,7 +51,7 @@ import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import {
GridCellContextMenuEvent,
@ -74,7 +74,7 @@ type GridItem = {
host: string;
}
const columnSettings: ColumnSetting[] = [
const columnSettings: GridColumnSetting[] = [
{ bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 },
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },