fix cell re-render bugs

This commit is contained in:
samunohito 2024-02-03 11:17:16 +09:00
parent ff48c77827
commit 295440a347
6 changed files with 79 additions and 27 deletions

View File

@ -10,7 +10,7 @@
<div <div
:class="[ :class="[
$style.root, $style.root,
[(cell.validation.valid || cell.selected) ? {} : $style.error], [(cell.violation.valid || cell.selected) ? {} : $style.error],
[cell.selected ? $style.selected : {}], [cell.selected ? $style.selected : {}],
// //
[(cell.ranged && !cell.row.ranged) ? $style.ranged : {}], [(cell.ranged && !cell.row.ranged) ? $style.ranged : {}],
@ -50,7 +50,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'; import {
computed,
defineAsyncComponent,
getCurrentInstance,
nextTick,
onMounted,
onUnmounted,
ref,
shallowRef,
toRefs,
watch,
} from 'vue';
import { GridEventEmitter, Size } from '@/components/grid/grid.js'; import { GridEventEmitter, Size } from '@/components/grid/grid.js';
import { useTooltip } from '@/scripts/use-tooltip.js'; import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -61,7 +72,6 @@ import { cellValidation, ValidateViolation } from '@/components/grid/cell-valida
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void; (ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'operation:endEdit', sender: GridCell): void; (ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'operation:validation', sender: GridCell, violation: ValidateViolation): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void; (ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void; (ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>(); }>();
@ -101,7 +111,7 @@ watch(() => cell.value.selected, () => {
} }
}); });
watch(() => cell.value.value, (newValue, oldValue) => { watch(() => cell.value.value, (newValue) => {
emitValueChange(newValue); emitValueChange(newValue);
}); });
@ -213,8 +223,7 @@ function endEditing(applyValue: boolean) {
function emitValueChange(newValue: CellValue) { function emitValueChange(newValue: CellValue) {
const _cell = cell.value; const _cell = cell.value;
const violation = cellValidation(_cell, newValue); emit('change:value', _cell, newValue);
emit('operation:validation', _cell, violation);
} }
function emitContentSizeChanged() { function emitContentSizeChanged() {
@ -225,11 +234,11 @@ function emitContentSizeChanged() {
} }
useTooltip(rootEl, (showing) => { useTooltip(rootEl, (showing) => {
if (cell.value.validation.valid) { if (cell.value.violation.valid) {
return; return;
} }
const content = cell.value.validation.violations.map(it => it.result.message).join('\n'); const content = cell.value.violation.violations.map(it => it.result.message).join('\n');
os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), { os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), {
showing, showing,
content, content,

View File

@ -19,7 +19,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { toRefs } from 'vue'; import { getCurrentInstance, toRefs, watch } from 'vue';
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js'; import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.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';

View File

@ -37,11 +37,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue'; import { computed, getCurrentInstance, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { DataSource, GridEventEmitter, GridSetting, GridState, Size } from '@/components/grid/grid.js'; import { DataSource, GridEventEmitter, GridSetting, GridState, Size } from '@/components/grid/grid.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';
import { ValidateViolation } from '@/components/grid/cell-validators.js'; import { cellValidation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js'; import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js'; import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
@ -177,8 +177,8 @@ const rangedRows = computed(() => rows.value.filter(it => it.ranged));
// endregion // endregion
// #endregion // #endregion
watch(columnSettings, refreshColumnsSetting, { immediate: true }); watch(columnSettings, refreshColumnsSetting);
watch(data, refreshData, { immediate: true, deep: true }); watch(data, patchData, { deep: true });
if (_DEV_) { if (_DEV_) {
watch(state, (value, oldValue) => { watch(state, (value, oldValue) => {
@ -632,11 +632,6 @@ function onCellEditEnd() {
state.value = 'normal'; state.value = 'normal';
} }
function onCellValidation(sender: GridCell, violation: ValidateViolation) {
sender.validation = violation;
emitGridEvent({ type: 'cell-validation', violation });
}
function onChangeCellValue(sender: GridCell, newValue: CellValue) { function onChangeCellValue(sender: GridCell, newValue: CellValue) {
emitCellValue(sender, newValue); emitCellValue(sender, newValue);
} }
@ -756,11 +751,19 @@ function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
const cellAddress = 'address' in sender ? sender.address : sender; const cellAddress = 'address' in sender ? sender.address : sender;
const cell = cells.value[cellAddress.row][cellAddress.col]; const cell = cells.value[cellAddress.row][cellAddress.col];
const violation = cellValidation(cell, newValue);
cell.violation = violation;
emitGridEvent({
type: 'cell-validation',
violation: violation,
});
cell.value = newValue;
emitGridEvent({ emitGridEvent({
type: 'cell-value-change', type: 'cell-value-change',
column: cell.column, column: cell.column,
row: cell.row, row: cell.row,
violation: cell.validation, violation: violation,
oldValue: cell.value, oldValue: cell.value,
newValue: newValue, newValue: newValue,
}); });
@ -908,7 +911,7 @@ function refreshData() {
} }
const _data: DataSource[] = data.value; const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => createRow(index)); const _rows: GridRow[] = _data.map((it, index) => createRow(index, it));
const _cols: GridColumn[] = columnSettings.value.map(createColumn); const _cols: GridColumn[] = columnSettings.value.map(createColumn);
// //
@ -928,10 +931,47 @@ function refreshData() {
cells.value = _cells; cells.value = _cells;
} }
/**
* セル値を部分更新するこの関数は外部起因でデータが変更された場合に呼ばれる
*
* 外部起因でデータが変更された場合は{@link data}の値が変更されるが何処の番地がどのように変わったのかまでは検知できない
* セルをすべて作り直せばいいがその手法だと以下のデメリットがある
* - 描画負荷がかかる
* - 各セルが持つ個別の状態選択中状態やバリデーション結果などが失われる
*
* そこで新しい値とセルが持つ値を突き合わせ変更があった場合のみセルの値を更新することでセルを使いまわしつつ値を最新化する
*/
function patchData(newItems: DataSource[]) {
const oldRows = cells.value;
if (oldRows.length !== newItems.length) {
//
//
refreshData();
return;
}
const _cols = columns.value;
for (let rowIdx = 0; rowIdx < oldRows.length; rowIdx++) {
const oldCells = oldRows[rowIdx];
const newItem = newItems[rowIdx];
for (let colIdx = 0; colIdx < oldCells.length; colIdx++) {
const _col = _cols[colIdx];
const oldCell = oldCells[colIdx];
const newValue = newItem[_col.setting.bindTo];
if (oldCell.value !== newValue) {
oldCell.value = newValue;
}
}
}
}
// endregion // endregion
// #endregion // #endregion
onMounted(() => { onMounted(() => {
refreshColumnsSetting();
if (rootEl.value) { if (rootEl.value) {
resizeObserver.observe(rootEl.value); resizeObserver.observe(rootEl.value);

View File

@ -23,7 +23,7 @@ export type GridCell = {
selected: boolean; selected: boolean;
ranged: boolean; ranged: boolean;
contentSize: Size; contentSize: Size;
validation: ValidateViolation; violation: ValidateViolation;
} }
export function createCell( export function createCell(
@ -39,7 +39,7 @@ export function createCell(
selected: false, selected: false,
ranged: false, ranged: false,
contentSize: { width: 0, height: 0 }, contentSize: { width: 0, height: 0 },
validation: { violation: {
valid: true, valid: true,
params: { params: {
column, column,

View File

@ -1,11 +1,15 @@
import { DataSource } from '@/components/grid/grid.js';
export type GridRow = { export type GridRow = {
index: number; index: number;
ranged: boolean; ranged: boolean;
origin: DataSource;
} }
export function createRow(index: number): GridRow { export function createRow(index: number, origin: DataSource): GridRow {
return { return {
index, index,
ranged: false, ranged: false,
origin: origin,
}; };
} }

View File

@ -86,7 +86,7 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { onMounted, ref } from 'vue'; import { getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js'; import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
@ -296,7 +296,7 @@ async function onDrop(ev: DragEvent) {
.replace(/\/[^/]+$/, '') .replace(/\/[^/]+$/, '')
.replace(droppedFile.file.name, ''); .replace(droppedFile.file.name, '');
} }
gridItems.value.push(item); gridItems.value.push(reactive(item));
} }
} }
@ -358,8 +358,7 @@ function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: Grid
} }
function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) { function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) {
const item = gridItems.value[event.row.index]; gridItems.value[event.row.index][event.column.setting.bindTo] = event.newValue;
item[event.column.setting.bindTo] = event.newValue;
} }
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) { function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {