wip
This commit is contained in:
parent
a2fcc81290
commit
8d1a5734cd
|
@ -98,7 +98,6 @@
|
|||
"@storybook/vue3": "7.6.10",
|
||||
"@storybook/vue3-vite": "7.6.10",
|
||||
"@testing-library/vue": "8.0.1",
|
||||
"@types/blueimp-load-image": "^5.16.6",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/matter-js": "0.19.6",
|
||||
|
|
|
@ -54,13 +54,15 @@ import {
|
|||
equalCellAddress,
|
||||
getCellAddress,
|
||||
GridCell,
|
||||
GridEventEmitter,
|
||||
GridEventEmitter, Size,
|
||||
} from '@/components/grid/types.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit:begin', sender: GridCell): void;
|
||||
(ev: 'edit:end', sender: GridCell): void;
|
||||
(ev: 'selection:move', sender: GridCell, next: CellAddress): void;
|
||||
(ev: 'operation:beginEdit', sender: GridCell): void;
|
||||
(ev: 'operation:endEdit', sender: GridCell): 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<{
|
||||
cell: GridCell,
|
||||
|
@ -87,10 +89,9 @@ const needsContentCentering = computed(() => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(cellWidth, updateContentSize);
|
||||
watch(() => [cell, cell.value.value], () => {
|
||||
// 中身がセットされた直後はサイズが分からないので、次のタイミングで更新する
|
||||
nextTick(updateContentSize);
|
||||
nextTick(emitContentSizeChanged);
|
||||
});
|
||||
watch(() => cell.value.selected, () => {
|
||||
if (cell.value.selected) {
|
||||
|
@ -116,44 +117,13 @@ function onOutsideMouseDown(ev: MouseEvent) {
|
|||
|
||||
function onCellKeyDown(ev: KeyboardEvent) {
|
||||
if (!editing.value) {
|
||||
ev.preventDefault();
|
||||
switch (ev.code) {
|
||||
case 'Enter':
|
||||
case 'F2': {
|
||||
beginEditing();
|
||||
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 {
|
||||
switch (ev.code) {
|
||||
|
@ -193,9 +163,10 @@ function beginEditing() {
|
|||
editingValue.value = cell.value.value;
|
||||
editing.value = true;
|
||||
registerOutsideMouseDown();
|
||||
emit('edit:begin', cell.value);
|
||||
emit('operation:beginEdit', cell.value);
|
||||
|
||||
nextTick(() => {
|
||||
// inputの展開後にフォーカスを当てたい
|
||||
if (inputAreaEl.value) {
|
||||
(inputAreaEl.value.querySelector('*') as HTMLElement).focus();
|
||||
}
|
||||
|
@ -204,7 +175,7 @@ function beginEditing() {
|
|||
}
|
||||
case 'boolean': {
|
||||
// とくに特殊なUIは設けず、トグルするだけ
|
||||
cell.value.value = !cell.value.value;
|
||||
emitValueChange(!cell.value.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -215,22 +186,28 @@ function endEditing(applyValue: boolean) {
|
|||
return;
|
||||
}
|
||||
|
||||
emit('edit:end', cell.value);
|
||||
emit('operation:endEdit', cell.value);
|
||||
unregisterOutsideMouseDown();
|
||||
|
||||
if (applyValue) {
|
||||
cell.value.value = editingValue.value;
|
||||
emitValueChange(editingValue.value);
|
||||
}
|
||||
|
||||
editingValue.value = undefined;
|
||||
editing.value = false;
|
||||
|
||||
rootEl.value?.focus();
|
||||
}
|
||||
|
||||
function updateContentSize() {
|
||||
cell.value.contentSize = {
|
||||
function emitValueChange(newValue: CellValue) {
|
||||
emit('change:value', cell.value, newValue);
|
||||
}
|
||||
|
||||
function emitContentSizeChanged() {
|
||||
emit('change:contentSize', cell.value, {
|
||||
width: contentAreaEl.value?.clientWidth ?? 0,
|
||||
height: contentAreaEl.value?.clientHeight ?? 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
@ -4,31 +4,35 @@
|
|||
:content="(row.index + 1).toString()"
|
||||
:selectable="true"
|
||||
:row="row"
|
||||
@selection:row="(sender) => emit('selection:row', sender)"
|
||||
@operation:selectionRow="(sender) => emit('operation:selectionRow', sender)"
|
||||
/>
|
||||
<MkDataCell
|
||||
v-for="cell in cells"
|
||||
:key="cell.address.col"
|
||||
:cell="cell"
|
||||
:bus="bus"
|
||||
@edit:begin="(sender) => emit('edit:begin', sender)"
|
||||
@edit:end="(sender) => emit('edit:end', sender)"
|
||||
@selection:move="(sender, next) => emit('selection:move', sender, next)"
|
||||
@operation:beginEdit="(sender) => emit('operation:beginEdit', sender)"
|
||||
@operation:endEdit="(sender) => emit('operation:endEdit', sender)"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 MkNumberCell from '@/components/grid/MkNumberCell.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'edit:begin', sender: GridCell): void;
|
||||
(ev: 'edit:end', sender: GridCell): void;
|
||||
(ev: 'selection:move', sender: GridCell, next: CellAddress): void;
|
||||
(ev: 'selection:row', sender: GridRow): void;
|
||||
(ev: 'operation:beginEdit', sender: GridCell): void;
|
||||
(ev: 'operation:endEdit', sender: GridCell): void;
|
||||
(ev: 'operation:selectionRow', 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<{
|
||||
row: GridRow,
|
||||
|
@ -37,7 +41,6 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const { cells } = toRefs(props);
|
||||
const last = computed(() => cells.value[cells.value.length - 1]);
|
||||
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
<table
|
||||
:class="$style.grid"
|
||||
@mousedown="onMouseDown"
|
||||
@mouseup="onMouseUp"
|
||||
@mousemove="onMouseMove"
|
||||
@keydown="onKeyDown"
|
||||
>
|
||||
<thead>
|
||||
<MkHeaderRow
|
||||
:columns="columns"
|
||||
:bus="bus"
|
||||
@width:beginChange="onHeaderCellWidthBeginChange"
|
||||
@width:endChange="onHeaderCellWidthEndChange"
|
||||
@width:changing="onHeaderCellWidthChanging"
|
||||
@width:largest="onHeaderCellWidthLargest"
|
||||
@selection:column="onSelectionColumn"
|
||||
@operation:beginWidthChange="onHeaderCellWidthBeginChange"
|
||||
@operation:endWidthChange="onHeaderCellWidthEndChange"
|
||||
@operation:widthLargest="onHeaderCellWidthLargest"
|
||||
@operation:selectionColumn="onSelectionColumn"
|
||||
@change:width="onHeaderCellChangeWidth"
|
||||
@change:contentSize="onHeaderCellChangeContentSize"
|
||||
/>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -23,10 +23,12 @@
|
|||
:row="row"
|
||||
:cells="cells[row.index]"
|
||||
:bus="bus"
|
||||
@edit:begin="onCellEditBegin"
|
||||
@edit:end="onCellEditEnd"
|
||||
@selection:move="onSelectionMove"
|
||||
@selection:row="onSelectionRow"
|
||||
@operation:beginEdit="onCellEditBegin"
|
||||
@operation:endEdit="onCellEditEnd"
|
||||
@operation:selectionMove="onSelectionMove"
|
||||
@operation:selectionRow="onSelectionRow"
|
||||
@change:value="onChangeCellValue"
|
||||
@change:contentSize="onChangeCellContentSize"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -38,6 +40,7 @@ import {
|
|||
calcCellWidth,
|
||||
CELL_ADDRESS_NONE,
|
||||
CellAddress,
|
||||
CellValue,
|
||||
ColumnSetting,
|
||||
DataSource,
|
||||
equalCellAddress,
|
||||
|
@ -47,7 +50,7 @@ import {
|
|||
GridEventEmitter,
|
||||
GridRow,
|
||||
GridState,
|
||||
isCellElement,
|
||||
Size,
|
||||
} from '@/components/grid/types.js';
|
||||
import MkDataRow from '@/components/grid/MkDataRow.vue';
|
||||
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
|
||||
|
@ -57,21 +60,24 @@ const props = defineProps<{
|
|||
data: DataSource[]
|
||||
}>();
|
||||
|
||||
const bus = new GridEventEmitter();
|
||||
|
||||
const { columnSettings, data } = toRefs(props);
|
||||
|
||||
const columns = ref<GridColumn[]>([]);
|
||||
const rows = ref<GridRow[]>([]);
|
||||
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 selected = cells.value.flat().filter(it => it.selected);
|
||||
return selected.length > 0 ? selected[0] : undefined;
|
||||
});
|
||||
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(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) {
|
||||
const cellAddress = getCellAddress(ev.target as HTMLElement);
|
||||
switch (state.value) {
|
||||
|
@ -91,20 +134,34 @@ function onMouseDown(ev: MouseEvent) {
|
|||
break;
|
||||
}
|
||||
case 'normal': {
|
||||
const cellAddress = getCellAddress(ev.target as HTMLElement);
|
||||
if (availableCellAddress(cellAddress)) {
|
||||
selectionCell(cellAddress);
|
||||
state.value = 'cellSelecting';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
switch (state.value) {
|
||||
case 'cellSelecting': {
|
||||
state.value = 'normal';
|
||||
previousCellAddress.value = CELL_ADDRESS_NONE;
|
||||
registerMouseUp();
|
||||
registerMouseMove();
|
||||
state.value = 'cellSelecting';
|
||||
} else if (isColumnHeaderCellAddress(cellAddress)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -129,19 +186,70 @@ function onMouseMove(ev: MouseEvent) {
|
|||
row: Math.max(targetCellAddress.row, selectedCellAddress.row),
|
||||
};
|
||||
|
||||
for (const cell of rangedCells.value) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
unSelectionOutOfRange(leftTop, rightBottom);
|
||||
expandRange(leftTop, rightBottom);
|
||||
previousCellAddress.value = targetCellAddress;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
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) {
|
||||
if (availableCellAddress(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) {
|
||||
case 'colResizing': {
|
||||
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) {
|
||||
switch (state.value) {
|
||||
case 'normal': {
|
||||
|
@ -231,6 +356,10 @@ function onSelectionRow(sender: GridRow) {
|
|||
}
|
||||
|
||||
function selectionCell(target: CellAddress) {
|
||||
if (!availableCellAddress(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unSelectionRange();
|
||||
|
||||
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) {
|
||||
const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
const bindToList = columnSettings.value.map(it => it.bindTo);
|
||||
if (new Set(bindToList).size !== columnSettings.value.length) {
|
||||
|
@ -316,6 +464,24 @@ function refreshData() {
|
|||
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();
|
||||
refreshData();
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.left"/>
|
||||
<div :class="$style.wrapper" @mouseup="onContentMouseUp">
|
||||
<div :class="$style.wrapper">
|
||||
<div ref="contentEl" :class="$style.contentArea">
|
||||
{{ text }}
|
||||
</div>
|
||||
|
@ -22,15 +22,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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<{
|
||||
// ヘッダのサイズ変更系イベントはセル全体の横幅設定に影響するので上位コンポーネントにリレーする必要あり
|
||||
(ev: 'width:begin-change', sender: GridColumn): void;
|
||||
(ev: 'width:end-change', sender: GridColumn): void;
|
||||
(ev: 'width:changing', sender: GridColumn, width: string): void;
|
||||
(ev: 'width:largest', sender: GridColumn): void;
|
||||
(ev: 'selection:column', sender: GridColumn): void;
|
||||
(ev: 'operation:beginWidthChange', sender: GridColumn): void;
|
||||
(ev: 'operation:endWidthChange', sender: GridColumn): void;
|
||||
(ev: 'operation:widthLargest', sender: GridColumn): void;
|
||||
(ev: 'change:width', sender: GridColumn, width: string): void;
|
||||
(ev: 'change:contentSize', sender: GridColumn, newSize: Size): void;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
column: GridColumn,
|
||||
|
@ -54,19 +53,10 @@ watch(column, () => {
|
|||
nextTick(updateContentSize);
|
||||
});
|
||||
|
||||
function onContentMouseUp(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'mouseup': {
|
||||
emit('selection:column', column.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onHandleDoubleClick(ev: MouseEvent) {
|
||||
switch (ev.type) {
|
||||
case 'dblclick': {
|
||||
emit('width:largest', column.value);
|
||||
emit('operation:widthLargest', column.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +69,7 @@ function onHandleMouseDown(ev: MouseEvent) {
|
|||
registerHandleMouseUp();
|
||||
registerHandleMouseMove();
|
||||
resizing.value = true;
|
||||
emit('width:begin-change', column.value);
|
||||
emit('operation:beginWidthChange', column.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -99,7 +89,7 @@ function onHandleMouseMove(ev: MouseEvent) {
|
|||
const clientWidth = rootEl.value.clientWidth;
|
||||
const clientRight = bounds.left + clientWidth;
|
||||
const nextWidth = clientWidth + (ev.clientX - clientRight);
|
||||
emit('width:changing', column.value, `${nextWidth}px`);
|
||||
emit('change:width', column.value, `${nextWidth}px`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -113,7 +103,7 @@ function onHandleMouseUp(ev: MouseEvent) {
|
|||
unregisterHandleMouseUp();
|
||||
unregisterHandleMouseMove();
|
||||
resizing.value = false;
|
||||
emit('width:end-change', column.value);
|
||||
emit('operation:endWidthChange', column.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -141,17 +131,17 @@ function unregisterHandleMouseUp() {
|
|||
function updateContentSize() {
|
||||
const clientWidth = contentEl.value?.clientWidth ?? 0;
|
||||
const clientHeight = contentEl.value?.clientHeight ?? 0;
|
||||
column.value.contentSize = {
|
||||
emit('change:contentSize', column.value, {
|
||||
// バーの横幅も考慮したいので、+3px
|
||||
width: clientWidth + 3 + 3,
|
||||
height: clientHeight,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
$handleWidth: 3px;
|
||||
$handleWidth: 5px;
|
||||
|
||||
.cell {
|
||||
border-left: solid 0.5px var(--divider);
|
||||
|
@ -180,6 +170,8 @@ $handleWidth: 3px;
|
|||
}
|
||||
|
||||
.left {
|
||||
// rightのぶんだけズレるのでそれを相殺するためのネガティブマージン
|
||||
margin-left: -$handleWidth;
|
||||
margin-right: auto;
|
||||
width: $handleWidth;
|
||||
min-width: $handleWidth;
|
||||
|
@ -187,9 +179,12 @@ $handleWidth: 3px;
|
|||
|
||||
.right {
|
||||
margin-left: auto;
|
||||
// 判定を罫線の上に重ねたいのでネガティブマージンを使う
|
||||
margin-right: -$handleWidth;
|
||||
width: $handleWidth;
|
||||
min-width: $handleWidth;
|
||||
cursor: w-resize;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,26 +10,27 @@
|
|||
:key="column.index"
|
||||
:column="column"
|
||||
:bus="bus"
|
||||
@width:beginChange="(sender) => emit('width:begin-change', sender)"
|
||||
@width:endChange="(sender) => emit('width:end-change', sender)"
|
||||
@width:changing="(sender, width) => emit('width:changing', sender, width)"
|
||||
@width:largest="(sender) => emit('width:largest', sender)"
|
||||
@selection:column="(sender) => emit('selection:column', sender)"
|
||||
@operation:beginWidthChange="(sender) => emit('operation:beginWidthChange', sender)"
|
||||
@operation:endWidthChange="(sender) => emit('operation:endWidthChange', sender)"
|
||||
@operation:widthLargest="(sender) => emit('operation:widthLargest', sender)"
|
||||
@change:width="(sender, width) => emit('change:width', sender, width)"
|
||||
@change:contentSize="(sender, newSize) => emit('change:contentSize', sender, newSize)"
|
||||
/>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<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 MkNumberCell from '@/components/grid/MkNumberCell.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'width:begin-change', sender: GridColumn): void;
|
||||
(ev: 'width:end-change', sender: GridColumn): void;
|
||||
(ev: 'width:changing', sender: GridColumn, width: string): void;
|
||||
(ev: 'width:largest', sender: GridColumn): void;
|
||||
(ev: 'selection:column', sender: GridColumn): void;
|
||||
(ev: 'operation:beginWidthChange', sender: GridColumn): void;
|
||||
(ev: 'operation:endWidthChange', sender: GridColumn): void;
|
||||
(ev: 'operation:widthLargest', sender: GridColumn): void;
|
||||
(ev: 'operation:selectionColumn', sender: GridColumn): void;
|
||||
(ev: 'change:width', sender: GridColumn, width: string): void;
|
||||
(ev: 'change:contentSize', sender: GridColumn, newSize: Size): void;
|
||||
}>();
|
||||
defineProps<{
|
||||
columns: GridColumn[],
|
||||
|
|
|
@ -1,36 +1,21 @@
|
|||
<template>
|
||||
<th :class="[$style.num, [top ? {} : $style.border]]" @mouseup="onMouseUp">
|
||||
<th :class="[$style.num, [top ? {} : $style.border]]">
|
||||
{{ content }}
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from 'vue';
|
||||
import { GridRow } from '@/components/grid/types.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'selection:row', sender: GridRow): void;
|
||||
}>();
|
||||
const emit = defineEmits<{}>();
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
content: string,
|
||||
row?: GridRow,
|
||||
selectable: 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>
|
||||
|
||||
<style module lang="scss">
|
||||
|
|
|
@ -4,7 +4,7 @@ export type CellValue = string | boolean | number | undefined | null
|
|||
|
||||
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'
|
||||
|
||||
|
|
|
@ -2,10 +2,39 @@
|
|||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkPageHeader/>
|
||||
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
<div class="_gaps" :class="$style.root">
|
||||
<MkGrid :data="convertedGridItems" :columnSettings="columnSettings"/>
|
||||
<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"/>
|
||||
</div>
|
||||
|
||||
<div :class="$style.pages">
|
||||
<button><<</button>
|
||||
<button><</button>
|
||||
|
||||
<button>1</button>
|
||||
<button>2</button>
|
||||
<button>3</button>
|
||||
<button>4</button>
|
||||
<button>5</button>
|
||||
<span>...</span>
|
||||
<button>10</button>
|
||||
|
||||
<button>></button>
|
||||
<button>>></button>
|
||||
</div>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
|
@ -18,6 +47,10 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||
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[] = [
|
||||
{ bindTo: 'url', title: '🎨', type: 'image', editable: false, width: 50 },
|
||||
|
@ -32,6 +65,10 @@ const columnSettings: ColumnSetting[] = [
|
|||
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
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 refreshCustomEmojis = async () => {
|
||||
|
@ -48,6 +85,29 @@ onMounted(async () => {
|
|||
await refreshCustomEmojis();
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -68,4 +128,26 @@ onMounted(async () => {
|
|||
padding: 16px;
|
||||
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>
|
||||
|
|
|
@ -905,9 +905,6 @@ importers:
|
|||
'@testing-library/vue':
|
||||
specifier: 8.0.1
|
||||
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':
|
||||
specifier: 0.0.3
|
||||
version: 0.0.3
|
||||
|
@ -7876,10 +7873,6 @@ packages:
|
|||
resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
|
||||
dev: true
|
||||
|
||||
/@types/blueimp-load-image@5.16.6:
|
||||
resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==}
|
||||
dev: true
|
||||
|
||||
/@types/body-parser@1.19.5:
|
||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in New Issue