diff --git a/common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json b/common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json new file mode 100644 index 0000000000..cb398d7203 --- /dev/null +++ b/common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-editors", + "comment": "fix: safely remount and reposition input editor when editing restarts", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-editors", + "email": "892739385@qq.com" +} diff --git a/packages/vtable-editors/src/input-editor.ts b/packages/vtable-editors/src/input-editor.ts index ece3c60398..9d190491bd 100644 --- a/packages/vtable-editors/src/input-editor.ts +++ b/packages/vtable-editors/src/input-editor.ts @@ -98,8 +98,20 @@ export class InputEditor implements IEditor { this.eventHandlers.push({ type: 'paste', handler: pasteHandler }); } - setValue(value: string) { - this.element.value = typeof value !== 'undefined' ? value : ''; + protected ensureElementMounted(container: HTMLElement) { + if (!this.element) { + this.createElement(); + return; + } + + if (!container.contains(this.element)) { + this.element.parentElement?.removeChild(this.element); + container.appendChild(this.element); + } + } + + setValue(value: string | null | undefined) { + this.element.value = value ?? ''; } getValue() { @@ -118,14 +130,7 @@ export class InputEditor implements IEditor { if (selectCell.col !== this.col || selectCell.row !== this.row) { return; } - if (!this.element) { - this.createElement(); - } else { - if (!container.contains(this.element)) { - this.element.parentElement.removeChild(this.element); - this.container.appendChild(this.element); - } - } + this.ensureElementMounted(container); this.element.style.opacity = '0'; //这个pointerEvents = 'none'很重要,如果没有的话会引起vtable.getElement()元素和这里的element元素的focus和blur的切换, //也会引起mouseleave_table mouseleave_cell和mouseenter的切换 @@ -141,19 +146,10 @@ export class InputEditor implements IEditor { this.table = table; this.col = col; this.row = row; - if (!this.element) { - this.createElement(); - if (referencePosition?.rect) { - this.adjustPosition(referencePosition.rect); - } - } else { - if (!container.contains(this.element)) { - this.element.parentElement.removeChild(this.element); - this.container.appendChild(this.element); - } - } - if (value !== undefined && value !== null) { - this.setValue(value); + this.ensureElementMounted(container); + this.setValue(value); + if (referencePosition?.rect) { + this.adjustPosition(referencePosition.rect); } //防止调用过prepareEdit 后,元素的显示和可操作性被影响 this.element.style.opacity = '1'; diff --git a/packages/vtable-editors/src/textArea-editor.ts b/packages/vtable-editors/src/textArea-editor.ts index 4eede430ca..78612cc6ab 100644 --- a/packages/vtable-editors/src/textArea-editor.ts +++ b/packages/vtable-editors/src/textArea-editor.ts @@ -57,26 +57,33 @@ export class TextAreaEditor implements IEditor { }); } - setValue(value: string) { - this.element.value = typeof value !== 'undefined' ? value : ''; + setValue(value: string | null | undefined) { + this.element.value = value ?? ''; } getValue() { return this.element?.value; } - onStart({ value, referencePosition, container, endEdit }: EditContext) { - this.container = container; - this.successCallback = endEdit; + protected ensureElementMounted(container: HTMLElement) { if (!this.element) { this.createElement(); + return; + } - if (value !== undefined && value !== null) { - this.setValue(value); - } - if (referencePosition?.rect) { - this.adjustPosition(referencePosition.rect); - } + if (!container.contains(this.element)) { + this.element.parentElement?.removeChild(this.element); + container.appendChild(this.element); + } + } + + onStart({ value, referencePosition, container, endEdit }: EditContext) { + this.container = container; + this.successCallback = endEdit; + this.ensureElementMounted(container); + this.setValue(value); + if (referencePosition?.rect) { + this.adjustPosition(referencePosition.rect); } this.element.focus(); // do nothing diff --git a/packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts b/packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts new file mode 100644 index 0000000000..0ca7934ad5 --- /dev/null +++ b/packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts @@ -0,0 +1,73 @@ +import * as VTable from '../../src'; +import { InputEditor } from '@visactor/vtable-editors'; + +const CONTAINER_ID = 'vTable'; +const inputEditor = new InputEditor({}); +VTable.register.editor('issue4810-input', inputEditor); + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue4810Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check double edit" to verify issue #4810.'; + + const button = document.createElement('button'); + button.textContent = 'Check double edit'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkDoubleEdit(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const checkDoubleEdit = () => { + const tableInstance = (window as any).tableInstance as VTable.ListTable; + const status = document.getElementById('issue4810Status')!; + + try { + tableInstance.startEditCell(0, tableInstance.columnHeaderLevelCount); + inputEditor.getInputElement()?.remove(); + (tableInstance.editorManager as any).editingEditor = null; + tableInstance.startEditCell(1, tableInstance.columnHeaderLevelCount); + } catch (err) { + status.textContent = `FAIL | ${(err as Error).message}`; + return status.textContent; + } + + const inputElement = inputEditor.getInputElement(); + const tableElement = tableInstance.getElement(); + const pass = + !!inputElement && + tableElement.contains(inputElement) && + inputElement.style.opacity === '1' && + inputElement.style.pointerEvents === 'auto' && + inputElement.style.left !== ''; + + status.textContent = `${pass ? 'PASS' : 'FAIL'} | mounted=${ + !!inputElement && tableElement.contains(inputElement) + }, left=${inputElement?.style.left}, top=${inputElement?.style.top}`; + return status.textContent; +}; + +export function createTable() { + const option: VTable.ListTableConstructorOptions = { + records: [ + { name: 'Alice', age: 20 }, + { name: 'Bob', age: 21 } + ], + columns: [ + { field: 'name', title: 'Name', width: 180, editor: 'issue4810-input' }, + { field: 'age', title: 'Age', width: 120, editor: 'issue4810-input' } + ], + editCellTrigger: 'doubleclick', + editor: 'issue4810-input', + widthMode: 'standard', + defaultRowHeight: 36 + }; + + createStatusBar(); + const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option); + (window as any).tableInstance = tableInstance; + (window as any).issue4810Run = checkDoubleEdit; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index f0eacdaba4..8253e772fc 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -54,6 +54,10 @@ export const menus = [ path: 'debug', name: 'issue-5213-row-series-number-aggregation' }, + { + path: 'debug', + name: 'issue-4810-edit-cell-double-click-blank' + }, { path: 'debug', name: 'issue-4816-functional-icons-theme'