This commit is contained in:
samunohito 2024-01-26 22:16:17 +09:00
parent a2fcc81290
commit 8d1a5734cd
10 changed files with 357 additions and 156 deletions

View File

@ -98,7 +98,6 @@
"@storybook/vue3": "7.6.10", "@storybook/vue3": "7.6.10",
"@storybook/vue3-vite": "7.6.10", "@storybook/vue3-vite": "7.6.10",
"@testing-library/vue": "8.0.1", "@testing-library/vue": "8.0.1",
"@types/blueimp-load-image": "^5.16.6",
"@types/escape-regexp": "0.0.3", "@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/matter-js": "0.19.6", "@types/matter-js": "0.19.6",

View File

@ -54,13 +54,15 @@ import {
equalCellAddress, equalCellAddress,
getCellAddress, getCellAddress,
GridCell, GridCell,
GridEventEmitter, GridEventEmitter, Size,
} from '@/components/grid/types.js'; } from '@/components/grid/types.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'edit:begin', sender: GridCell): void; (ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'edit:end', sender: GridCell): void; (ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'selection:move', sender: GridCell, next: CellAddress): void; (ev: 'operation:selectionMove', sender: GridCell, next: CellAddress): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>(); }>();
const props = defineProps<{ const props = defineProps<{
cell: GridCell, cell: GridCell,
@ -87,10 +89,9 @@ const needsContentCentering = computed(() => {
} }
}); });
watch(cellWidth, updateContentSize);
watch(() => [cell, cell.value.value], () => { watch(() => [cell, cell.value.value], () => {
// //
nextTick(updateContentSize); nextTick(emitContentSizeChanged);
}); });
watch(() => cell.value.selected, () => { watch(() => cell.value.selected, () => {
if (cell.value.selected) { if (cell.value.selected) {
@ -116,44 +117,13 @@ function onOutsideMouseDown(ev: MouseEvent) {
function onCellKeyDown(ev: KeyboardEvent) { function onCellKeyDown(ev: KeyboardEvent) {
if (!editing.value) { if (!editing.value) {
ev.preventDefault();
switch (ev.code) { switch (ev.code) {
case 'Enter': case 'Enter':
case 'F2': { case 'F2': {
beginEditing(); beginEditing();
break; break;
} }
case 'ArrowRight': {
const next = {
col: cell.value.address.col + 1,
row: cell.value.address.row,
};
emit('selection:move', cell.value, next);
break;
}
case 'ArrowLeft': {
const next = {
col: cell.value.address.col - 1,
row: cell.value.address.row,
};
emit('selection:move', cell.value, next);
break;
}
case 'ArrowUp': {
const next = {
col: cell.value.address.col,
row: cell.value.address.row - 1,
};
emit('selection:move', cell.value, next);
break;
}
case 'ArrowDown': {
const next = {
col: cell.value.address.col,
row: cell.value.address.row + 1,
};
emit('selection:move', cell.value, next);
break;
}
} }
} else { } else {
switch (ev.code) { switch (ev.code) {
@ -193,9 +163,10 @@ function beginEditing() {
editingValue.value = cell.value.value; editingValue.value = cell.value.value;
editing.value = true; editing.value = true;
registerOutsideMouseDown(); registerOutsideMouseDown();
emit('edit:begin', cell.value); emit('operation:beginEdit', cell.value);
nextTick(() => { nextTick(() => {
// input
if (inputAreaEl.value) { if (inputAreaEl.value) {
(inputAreaEl.value.querySelector('*') as HTMLElement).focus(); (inputAreaEl.value.querySelector('*') as HTMLElement).focus();
} }
@ -204,7 +175,7 @@ function beginEditing() {
} }
case 'boolean': { case 'boolean': {
// UI // UI
cell.value.value = !cell.value.value; emitValueChange(!cell.value.value);
break; break;
} }
} }
@ -215,22 +186,28 @@ function endEditing(applyValue: boolean) {
return; return;
} }
emit('edit:end', cell.value); emit('operation:endEdit', cell.value);
unregisterOutsideMouseDown(); unregisterOutsideMouseDown();
if (applyValue) { if (applyValue) {
cell.value.value = editingValue.value; emitValueChange(editingValue.value);
} }
editingValue.value = undefined;
editing.value = false; editing.value = false;
rootEl.value?.focus(); rootEl.value?.focus();
} }
function updateContentSize() { function emitValueChange(newValue: CellValue) {
cell.value.contentSize = { emit('change:value', cell.value, newValue);
}
function emitContentSizeChanged() {
emit('change:contentSize', cell.value, {
width: contentAreaEl.value?.clientWidth ?? 0, width: contentAreaEl.value?.clientWidth ?? 0,
height: contentAreaEl.value?.clientHeight ?? 0, height: contentAreaEl.value?.clientHeight ?? 0,
}; });
} }
</script> </script>

View File

@ -4,31 +4,35 @@
:content="(row.index + 1).toString()" :content="(row.index + 1).toString()"
:selectable="true" :selectable="true"
:row="row" :row="row"
@selection:row="(sender) => emit('selection:row', sender)" @operation:selectionRow="(sender) => emit('operation:selectionRow', sender)"
/> />
<MkDataCell <MkDataCell
v-for="cell in cells" v-for="cell in cells"
:key="cell.address.col" :key="cell.address.col"
:cell="cell" :cell="cell"
:bus="bus" :bus="bus"
@edit:begin="(sender) => emit('edit:begin', sender)" @operation:beginEdit="(sender) => emit('operation:beginEdit', sender)"
@edit:end="(sender) => emit('edit:end', sender)" @operation:endEdit="(sender) => emit('operation:endEdit', sender)"
@selection:move="(sender, next) => emit('selection:move', sender, next)" @operation:selectionMove="(sender, next) => emit('operation:selectionMove', sender, next)"
@change:value="(sender, newValue) => emit('change:value', sender, newValue)"
@change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
/> />
</tr> </tr>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, toRefs } from 'vue'; import { computed, toRefs } from 'vue';
import { CellAddress, GridCell, GridEventEmitter, GridRow } from '@/components/grid/types.js'; import { CellAddress, CellValue, GridCell, GridEventEmitter, GridRow, Size } from '@/components/grid/types.js';
import MkDataCell from '@/components/grid/MkDataCell.vue'; import MkDataCell from '@/components/grid/MkDataCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue'; import MkNumberCell from '@/components/grid/MkNumberCell.vue';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'edit:begin', sender: GridCell): void; (ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'edit:end', sender: GridCell): void; (ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'selection:move', sender: GridCell, next: CellAddress): void; (ev: 'operation:selectionRow', sender: GridRow): void;
(ev: 'selection:row', sender: GridRow): void; (ev: 'operation:selectionMove', sender: GridCell, next: CellAddress): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>(); }>();
const props = defineProps<{ const props = defineProps<{
row: GridRow, row: GridRow,
@ -37,7 +41,6 @@ const props = defineProps<{
}>(); }>();
const { cells } = toRefs(props); const { cells } = toRefs(props);
const last = computed(() => cells.value[cells.value.length - 1]);
</script> </script>

View File

@ -2,18 +2,18 @@
<table <table
:class="$style.grid" :class="$style.grid"
@mousedown="onMouseDown" @mousedown="onMouseDown"
@mouseup="onMouseUp" @keydown="onKeyDown"
@mousemove="onMouseMove"
> >
<thead> <thead>
<MkHeaderRow <MkHeaderRow
:columns="columns" :columns="columns"
:bus="bus" :bus="bus"
@width:beginChange="onHeaderCellWidthBeginChange" @operation:beginWidthChange="onHeaderCellWidthBeginChange"
@width:endChange="onHeaderCellWidthEndChange" @operation:endWidthChange="onHeaderCellWidthEndChange"
@width:changing="onHeaderCellWidthChanging" @operation:widthLargest="onHeaderCellWidthLargest"
@width:largest="onHeaderCellWidthLargest" @operation:selectionColumn="onSelectionColumn"
@selection:column="onSelectionColumn" @change:width="onHeaderCellChangeWidth"
@change:contentSize="onHeaderCellChangeContentSize"
/> />
</thead> </thead>
<tbody> <tbody>
@ -23,10 +23,12 @@
:row="row" :row="row"
:cells="cells[row.index]" :cells="cells[row.index]"
:bus="bus" :bus="bus"
@edit:begin="onCellEditBegin" @operation:beginEdit="onCellEditBegin"
@edit:end="onCellEditEnd" @operation:endEdit="onCellEditEnd"
@selection:move="onSelectionMove" @operation:selectionMove="onSelectionMove"
@selection:row="onSelectionRow" @operation:selectionRow="onSelectionRow"
@change:value="onChangeCellValue"
@change:contentSize="onChangeCellContentSize"
/> />
</tbody> </tbody>
</table> </table>
@ -38,6 +40,7 @@ import {
calcCellWidth, calcCellWidth,
CELL_ADDRESS_NONE, CELL_ADDRESS_NONE,
CellAddress, CellAddress,
CellValue,
ColumnSetting, ColumnSetting,
DataSource, DataSource,
equalCellAddress, equalCellAddress,
@ -47,7 +50,7 @@ import {
GridEventEmitter, GridEventEmitter,
GridRow, GridRow,
GridState, GridState,
isCellElement, Size,
} from '@/components/grid/types.js'; } from '@/components/grid/types.js';
import MkDataRow from '@/components/grid/MkDataRow.vue'; import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
@ -57,21 +60,24 @@ const props = defineProps<{
data: DataSource[] data: DataSource[]
}>(); }>();
const bus = new GridEventEmitter();
const { columnSettings, data } = toRefs(props); const { columnSettings, data } = toRefs(props);
const columns = ref<GridColumn[]>([]); const columns = ref<GridColumn[]>([]);
const rows = ref<GridRow[]>([]); const rows = ref<GridRow[]>([]);
const cells = ref<GridCell[][]>([]); const cells = ref<GridCell[][]>([]);
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
const state = ref<GridState>('normal');
const selectedCell = computed(() => { const selectedCell = computed(() => {
const selected = cells.value.flat().filter(it => it.selected); const selected = cells.value.flat().filter(it => it.selected);
return selected.length > 0 ? selected[0] : undefined; return selected.length > 0 ? selected[0] : undefined;
}); });
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged)); const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const state = ref<GridState>('normal');
const bus = new GridEventEmitter();
watch(columnSettings, refreshColumnsSetting); watch(columnSettings, refreshColumnsSetting);
watch(data, refreshData); watch(data, refreshData);
@ -81,6 +87,43 @@ if (_DEV_) {
}); });
} }
function onKeyDown(ev: KeyboardEvent) {
switch (state.value) {
case 'normal': {
const selectedCellAddress = selectedCell.value?.address;
if (!selectedCellAddress) {
return;
}
let next: CellAddress;
switch (ev.code) {
case 'ArrowRight': {
next = { col: selectedCellAddress.col + 1, row: selectedCellAddress.row };
break;
}
case 'ArrowLeft': {
next = { col: selectedCellAddress.col - 1, row: selectedCellAddress.row };
break;
}
case 'ArrowUp': {
next = { col: selectedCellAddress.col, row: selectedCellAddress.row - 1 };
break;
}
case 'ArrowDown': {
next = { col: selectedCellAddress.col, row: selectedCellAddress.row + 1 };
break;
}
default: {
return;
}
}
selectionCell(next);
break;
}
}
}
function onMouseDown(ev: MouseEvent) { function onMouseDown(ev: MouseEvent) {
const cellAddress = getCellAddress(ev.target as HTMLElement); const cellAddress = getCellAddress(ev.target as HTMLElement);
switch (state.value) { switch (state.value) {
@ -91,20 +134,34 @@ function onMouseDown(ev: MouseEvent) {
break; break;
} }
case 'normal': { case 'normal': {
const cellAddress = getCellAddress(ev.target as HTMLElement);
if (availableCellAddress(cellAddress)) { if (availableCellAddress(cellAddress)) {
selectionCell(cellAddress); selectionCell(cellAddress);
state.value = 'cellSelecting';
}
break;
}
}
}
function onMouseUp() { registerMouseUp();
switch (state.value) { registerMouseMove();
case 'cellSelecting': { state.value = 'cellSelecting';
state.value = 'normal'; } else if (isColumnHeaderCellAddress(cellAddress)) {
previousCellAddress.value = CELL_ADDRESS_NONE; unSelectionRange();
const colCells = cells.value.map(row => row[cellAddress.col]);
selectionRange(...colCells.map(cell => cell.address));
registerMouseUp();
registerMouseMove();
firstSelectionColumnIdx.value = cellAddress.col;
state.value = 'colSelecting';
} else if (isRowNumberCellAddress(cellAddress)) {
unSelectionRange();
const rowCells = cells.value[cellAddress.row];
selectionRange(...rowCells.map(cell => cell.address));
registerMouseUp();
registerMouseMove();
firstSelectionRowIdx.value = cellAddress.row;
state.value = 'rowSelecting';
}
break; break;
} }
} }
@ -129,19 +186,70 @@ function onMouseMove(ev: MouseEvent) {
row: Math.max(targetCellAddress.row, selectedCellAddress.row), row: Math.max(targetCellAddress.row, selectedCellAddress.row),
}; };
for (const cell of rangedCells.value) { unSelectionOutOfRange(leftTop, rightBottom);
const outOfRangeCol = cell.address.col < leftTop.col || cell.address.col > rightBottom.col;
const outOfRangeRow = cell.address.row < leftTop.row || cell.address.row > rightBottom.row;
if (outOfRangeCol || outOfRangeRow) {
cell.ranged = false;
}
}
expandRange(leftTop, rightBottom); expandRange(leftTop, rightBottom);
previousCellAddress.value = targetCellAddress; previousCellAddress.value = targetCellAddress;
break; break;
} }
case 'colSelecting': {
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) {
return;
}
const leftTop = {
col: Math.min(targetCellAddress.col, firstSelectionColumnIdx.value),
row: 0,
};
const rightBottom = {
col: Math.max(targetCellAddress.col, firstSelectionColumnIdx.value),
row: cells.value.length - 1,
};
unSelectionOutOfRange(leftTop, rightBottom);
expandRange(leftTop, rightBottom);
previousCellAddress.value = targetCellAddress;
break;
}
case 'rowSelecting': {
const targetCellAddress = getCellAddress(ev.target as HTMLElement);
if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
return;
}
const leftTop = {
col: 0,
row: Math.min(targetCellAddress.row, firstSelectionRowIdx.value),
};
const rightBottom = {
col: Math.min(...cells.value.map(it => it.length - 1)),
row: Math.max(targetCellAddress.row, firstSelectionRowIdx.value),
};
unSelectionOutOfRange(leftTop, rightBottom);
expandRange(leftTop, rightBottom);
previousCellAddress.value = targetCellAddress;
break;
}
}
}
function onMouseUp(ev: MouseEvent) {
switch (state.value) {
case 'rowSelecting':
case 'colSelecting':
case 'cellSelecting': {
unregisterMouseUp();
unregisterMouseMove();
state.value = 'normal';
previousCellAddress.value = CELL_ADDRESS_NONE;
break;
}
} }
} }
@ -161,6 +269,14 @@ function onCellEditEnd() {
state.value = 'normal'; state.value = 'normal';
} }
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
cells.value[sender.address.row][sender.address.col].value = newValue;
}
function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
cells.value[sender.address.row][sender.address.col].contentSize = contentSize;
}
function onSelectionMove(_: GridCell, next: CellAddress) { function onSelectionMove(_: GridCell, next: CellAddress) {
if (availableCellAddress(next)) { if (availableCellAddress(next)) {
selectionCell(next); selectionCell(next);
@ -185,7 +301,7 @@ function onHeaderCellWidthEndChange(_: GridColumn) {
} }
} }
function onHeaderCellWidthChanging(sender: GridColumn, width: string) { function onHeaderCellChangeWidth(sender: GridColumn, width: string) {
switch (state.value) { switch (state.value) {
case 'colResizing': { case 'colResizing': {
const column = columns.value[sender.index]; const column = columns.value[sender.index];
@ -195,6 +311,15 @@ function onHeaderCellWidthChanging(sender: GridColumn, width: string) {
} }
} }
function onHeaderCellChangeContentSize(sender: GridColumn, newSize: Size) {
switch (state.value) {
case 'normal': {
columns.value[sender.index].contentSize = newSize;
break;
}
}
}
function onHeaderCellWidthLargest(sender: GridColumn) { function onHeaderCellWidthLargest(sender: GridColumn) {
switch (state.value) { switch (state.value) {
case 'normal': { case 'normal': {
@ -231,6 +356,10 @@ function onSelectionRow(sender: GridRow) {
} }
function selectionCell(target: CellAddress) { function selectionCell(target: CellAddress) {
if (!availableCellAddress(target)) {
return;
}
unSelectionRange(); unSelectionRange();
const _cells = cells.value; const _cells = cells.value;
@ -253,6 +382,17 @@ function unSelectionRange() {
} }
} }
function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
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;
if (outOfRangeCol || outOfRangeRow) {
cell.ranged = false;
}
}
}
function expandRange(leftTop: CellAddress, rightBottom: CellAddress) { function expandRange(leftTop: CellAddress, rightBottom: CellAddress) {
const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1); const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
for (const row of targetRows) { for (const row of targetRows) {
@ -266,6 +406,14 @@ function availableCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length; return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length;
} }
function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row === -1 && cellAddress.col >= 0;
}
function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col === -1;
}
function refreshColumnsSetting() { function refreshColumnsSetting() {
const bindToList = columnSettings.value.map(it => it.bindTo); const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) { if (new Set(bindToList).size !== columnSettings.value.length) {
@ -316,6 +464,24 @@ function refreshData() {
cells.value = _cells; cells.value = _cells;
} }
function registerMouseMove() {
unregisterMouseMove();
addEventListener('mousemove', onMouseMove);
}
function unregisterMouseMove() {
removeEventListener('mousemove', onMouseMove);
}
function registerMouseUp() {
unregisterMouseUp();
addEventListener('mouseup', onMouseUp);
}
function unregisterMouseUp() {
removeEventListener('mouseup', onMouseUp);
}
refreshColumnsSetting(); refreshColumnsSetting();
refreshData(); refreshData();

View File

@ -6,7 +6,7 @@
> >
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.left"/> <div :class="$style.left"/>
<div :class="$style.wrapper" @mouseup="onContentMouseUp"> <div :class="$style.wrapper">
<div ref="contentEl" :class="$style.contentArea"> <div ref="contentEl" :class="$style.contentArea">
{{ text }} {{ text }}
</div> </div>
@ -22,15 +22,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, ref, toRefs, watch } from 'vue'; import { computed, nextTick, ref, toRefs, watch } from 'vue';
import { GridColumn, GridEventEmitter } from '@/components/grid/types.js'; import { GridColumn, GridEventEmitter, Size } from '@/components/grid/types.js';
const emit = defineEmits<{ const emit = defineEmits<{
// (ev: 'operation:beginWidthChange', sender: GridColumn): void;
(ev: 'width:begin-change', sender: GridColumn): void; (ev: 'operation:endWidthChange', sender: GridColumn): void;
(ev: 'width:end-change', sender: GridColumn): void; (ev: 'operation:widthLargest', sender: GridColumn): void;
(ev: 'width:changing', sender: GridColumn, width: string): void; (ev: 'change:width', sender: GridColumn, width: string): void;
(ev: 'width:largest', sender: GridColumn): void; (ev: 'change:contentSize', sender: GridColumn, newSize: Size): void;
(ev: 'selection:column', sender: GridColumn): void;
}>(); }>();
const props = defineProps<{ const props = defineProps<{
column: GridColumn, column: GridColumn,
@ -54,19 +53,10 @@ watch(column, () => {
nextTick(updateContentSize); nextTick(updateContentSize);
}); });
function onContentMouseUp(ev: MouseEvent) {
switch (ev.type) {
case 'mouseup': {
emit('selection:column', column.value);
break;
}
}
}
function onHandleDoubleClick(ev: MouseEvent) { function onHandleDoubleClick(ev: MouseEvent) {
switch (ev.type) { switch (ev.type) {
case 'dblclick': { case 'dblclick': {
emit('width:largest', column.value); emit('operation:widthLargest', column.value);
break; break;
} }
} }
@ -79,7 +69,7 @@ function onHandleMouseDown(ev: MouseEvent) {
registerHandleMouseUp(); registerHandleMouseUp();
registerHandleMouseMove(); registerHandleMouseMove();
resizing.value = true; resizing.value = true;
emit('width:begin-change', column.value); emit('operation:beginWidthChange', column.value);
} }
break; break;
} }
@ -99,7 +89,7 @@ function onHandleMouseMove(ev: MouseEvent) {
const clientWidth = rootEl.value.clientWidth; const clientWidth = rootEl.value.clientWidth;
const clientRight = bounds.left + clientWidth; const clientRight = bounds.left + clientWidth;
const nextWidth = clientWidth + (ev.clientX - clientRight); const nextWidth = clientWidth + (ev.clientX - clientRight);
emit('width:changing', column.value, `${nextWidth}px`); emit('change:width', column.value, `${nextWidth}px`);
} }
break; break;
} }
@ -113,7 +103,7 @@ function onHandleMouseUp(ev: MouseEvent) {
unregisterHandleMouseUp(); unregisterHandleMouseUp();
unregisterHandleMouseMove(); unregisterHandleMouseMove();
resizing.value = false; resizing.value = false;
emit('width:end-change', column.value); emit('operation:endWidthChange', column.value);
} }
break; break;
} }
@ -141,17 +131,17 @@ function unregisterHandleMouseUp() {
function updateContentSize() { function updateContentSize() {
const clientWidth = contentEl.value?.clientWidth ?? 0; const clientWidth = contentEl.value?.clientWidth ?? 0;
const clientHeight = contentEl.value?.clientHeight ?? 0; const clientHeight = contentEl.value?.clientHeight ?? 0;
column.value.contentSize = { emit('change:contentSize', column.value, {
// +3px // +3px
width: clientWidth + 3 + 3, width: clientWidth + 3 + 3,
height: clientHeight, height: clientHeight,
}; });
} }
</script> </script>
<style module lang="scss"> <style module lang="scss">
$handleWidth: 3px; $handleWidth: 5px;
.cell { .cell {
border-left: solid 0.5px var(--divider); border-left: solid 0.5px var(--divider);
@ -180,6 +170,8 @@ $handleWidth: 3px;
} }
.left { .left {
// right
margin-left: -$handleWidth;
margin-right: auto; margin-right: auto;
width: $handleWidth; width: $handleWidth;
min-width: $handleWidth; min-width: $handleWidth;
@ -187,9 +179,12 @@ $handleWidth: 3px;
.right { .right {
margin-left: auto; margin-left: auto;
// 使
margin-right: -$handleWidth;
width: $handleWidth; width: $handleWidth;
min-width: $handleWidth; min-width: $handleWidth;
cursor: w-resize; cursor: w-resize;
z-index: 1;
} }
} }
</style> </style>

View File

@ -10,26 +10,27 @@
:key="column.index" :key="column.index"
:column="column" :column="column"
:bus="bus" :bus="bus"
@width:beginChange="(sender) => emit('width:begin-change', sender)" @operation:beginWidthChange="(sender) => emit('operation:beginWidthChange', sender)"
@width:endChange="(sender) => emit('width:end-change', sender)" @operation:endWidthChange="(sender) => emit('operation:endWidthChange', sender)"
@width:changing="(sender, width) => emit('width:changing', sender, width)" @operation:widthLargest="(sender) => emit('operation:widthLargest', sender)"
@width:largest="(sender) => emit('width:largest', sender)" @change:width="(sender, width) => emit('change:width', sender, width)"
@selection:column="(sender) => emit('selection:column', sender)" @change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
/> />
</tr> </tr>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { GridColumn, GridEventEmitter } from '@/components/grid/types.js'; import { GridColumn, GridEventEmitter, Size } from '@/components/grid/types.js';
import MkHeaderCell from '@/components/grid/MkHeaderCell.vue'; import MkHeaderCell from '@/components/grid/MkHeaderCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue'; import MkNumberCell from '@/components/grid/MkNumberCell.vue';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'width:begin-change', sender: GridColumn): void; (ev: 'operation:beginWidthChange', sender: GridColumn): void;
(ev: 'width:end-change', sender: GridColumn): void; (ev: 'operation:endWidthChange', sender: GridColumn): void;
(ev: 'width:changing', sender: GridColumn, width: string): void; (ev: 'operation:widthLargest', sender: GridColumn): void;
(ev: 'width:largest', sender: GridColumn): void; (ev: 'operation:selectionColumn', sender: GridColumn): void;
(ev: 'selection:column', sender: GridColumn): void; (ev: 'change:width', sender: GridColumn, width: string): void;
(ev: 'change:contentSize', sender: GridColumn, newSize: Size): void;
}>(); }>();
defineProps<{ defineProps<{
columns: GridColumn[], columns: GridColumn[],

View File

@ -1,36 +1,21 @@
<template> <template>
<th :class="[$style.num, [top ? {} : $style.border]]" @mouseup="onMouseUp"> <th :class="[$style.num, [top ? {} : $style.border]]">
{{ content }} {{ content }}
</th> </th>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { toRefs } from 'vue';
import { GridRow } from '@/components/grid/types.js'; import { GridRow } from '@/components/grid/types.js';
const emit = defineEmits<{ const emit = defineEmits<{}>();
(ev: 'selection:row', sender: GridRow): void;
}>();
const props = defineProps<{ defineProps<{
content: string, content: string,
row?: GridRow, row?: GridRow,
selectable: boolean, selectable: boolean,
top?: boolean, top?: boolean,
}>(); }>();
const { content, row, selectable } = toRefs(props);
function onMouseUp(ev: MouseEvent) {
switch (ev.type) {
case 'mouseup': {
if (selectable.value && row.value) {
emit('selection:row', row.value);
}
break;
}
}
}
</script> </script>
<style module lang="scss"> <style module lang="scss">

View File

@ -4,7 +4,7 @@ export type CellValue = string | boolean | number | undefined | null
export type DataSource = Record<string, CellValue>; export type DataSource = Record<string, CellValue>;
export type GridState = 'normal' | 'cellSelecting' | 'cellEditing' | 'colResizing' export type GridState = 'normal' | 'cellSelecting' | 'cellEditing' | 'colResizing' | 'colSelecting' | 'rowSelecting'
export type RowState = 'normal' | 'added' | 'deleted' export type RowState = 'normal' | 'added' | 'deleted'

View File

@ -2,11 +2,40 @@
<div> <div>
<MkStickyContainer> <MkStickyContainer>
<template #header> <template #header>
<MkPageHeader/> <MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
</template> </template>
<div class="_gaps" :class="$style.root"> <div class="_gaps" :class="$style.root">
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
<template #prefix><i class="ti ti-search"></i></template>
<template #label>{{ i18n.ts.search }}</template>
</MkInput>
<div :class="$style.controller">
<MkSelect v-model="limit">
<option value="100">100</option>
</MkSelect>
</div>
<div style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;">
<MkGrid :data="convertedGridItems" :columnSettings="columnSettings"/> <MkGrid :data="convertedGridItems" :columnSettings="columnSettings"/>
</div> </div>
<div :class="$style.pages">
<button>&lt;&lt;</button>
<button>&lt;</button>
<button>1</button>
<button>2</button>
<button>3</button>
<button>4</button>
<button>5</button>
<span>...</span>
<button>10</button>
<button>&gt;</button>
<button>&gt;&gt;</button>
</div>
</div>
</MkStickyContainer> </MkStickyContainer>
</div> </div>
</template> </template>
@ -18,6 +47,10 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/types.js'; import { ColumnSetting } from '@/components/grid/types.js';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const columnSettings: ColumnSetting[] = [ const columnSettings: ColumnSetting[] = [
{ bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 }, { bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 },
@ -32,6 +65,10 @@ const columnSettings: ColumnSetting[] = [
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]); const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
const gridItems = ref<GridItem[]>([]); const gridItems = ref<GridItem[]>([]);
const query = ref('');
const limit = ref(100);
const tab = ref('local');
const convertedGridItems = computed(() => gridItems.value.map(it => it.asRecord())); const convertedGridItems = computed(() => gridItems.value.map(it => it.asRecord()));
const refreshCustomEmojis = async () => { const refreshCustomEmojis = async () => {
@ -48,6 +85,29 @@ onMounted(async () => {
await refreshCustomEmojis(); await refreshCustomEmojis();
refreshGridItems(); refreshGridItems();
}); });
const headerTabs = computed(() => [{
key: 'local',
title: i18n.ts.local,
}, {
key: 'remote',
title: i18n.ts.remote,
}]);
const headerActions = computed(() => [{
asFullButton: true,
icon: 'ti ti-plus',
text: i18n.ts.addEmoji,
handler: () => {},
}, {
icon: 'ti ti-dots',
handler: () => {},
}]);
definePageMetadata(computed(() => ({
title: i18n.ts.customEmojis,
icon: 'ti ti-icons',
})));
</script> </script>
<style lang="scss"> <style lang="scss">
@ -68,4 +128,26 @@ onMounted(async () => {
padding: 16px; padding: 16px;
overflow: scroll; overflow: scroll;
} }
.controller {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 16px;
}
.pages {
display: flex;
justify-content: center;
align-items: center;
margin-top: 8px;
button {
background-color: var(--buttonBg);
border-radius: 9999px;
border: none;
margin: 0 4px;
padding: 8px;
}
}
</style> </style>

View File

@ -905,9 +905,6 @@ importers:
'@testing-library/vue': '@testing-library/vue':
specifier: 8.0.1 specifier: 8.0.1
version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.15) version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.15)
'@types/blueimp-load-image':
specifier: ^5.16.6
version: 5.16.6
'@types/escape-regexp': '@types/escape-regexp':
specifier: 0.0.3 specifier: 0.0.3
version: 0.0.3 version: 0.0.3
@ -7876,10 +7873,6 @@ packages:
resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
dev: true dev: true
/@types/blueimp-load-image@5.16.6:
resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==}
dev: true
/@types/body-parser@1.19.5: /@types/body-parser@1.19.5:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies: dependencies: