fix cell re-render bugs
This commit is contained in:
parent
ff48c77827
commit
295440a347
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue