Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba4ddb06d3 | ||
|
|
5bd7b5d724 |
No files matched your search
@@ -108,6 +108,7 @@ import { VaFilterService } from '../services/va-filter.service'
|
||||
import { UploadFileResponse } from '../models/UploadFile'
|
||||
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
||||
import { ParseResult } from '../models/ParseResult.interface'
|
||||
import { observeGridContainerResize } from './utils/refreshGridAfterContainerResize'
|
||||
|
||||
@Component({
|
||||
selector: 'app-editor',
|
||||
@@ -3801,22 +3802,16 @@ export class EditorComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-run height+render when the grid container settles.
|
||||
* Re-measure the grid whenever its container settles. The observer itself
|
||||
* lives in editor/utils/refreshGridAfterContainerResize.ts so the wiring is
|
||||
* covered by a spec.
|
||||
*/
|
||||
private observeGridResize() {
|
||||
const el = this.hotInstance?.rootElement?.parentElement
|
||||
if (!el) return
|
||||
|
||||
this.gridResizeObserver?.disconnect()
|
||||
this.gridResizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const hot = this.hotInstance
|
||||
if (!hot || hot.isDestroyed) return
|
||||
this.updateSettingsSortSafe({ height: this.hotTable.height }, false)
|
||||
hot.render()
|
||||
})
|
||||
})
|
||||
this.gridResizeObserver.observe(el)
|
||||
this.gridResizeObserver = observeGridContainerResize(
|
||||
this.hotInstance,
|
||||
this.hotInstance?.rootElement?.parentElement
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import { HyperFormula } from 'hyperformula'
|
||||
import {
|
||||
observeGridContainerResize,
|
||||
refreshGridAfterContainerResize
|
||||
} from './refreshGridAfterContainerResize'
|
||||
|
||||
/**
|
||||
* The editor refreshes its grid when the container changes size (sidebar
|
||||
* toggle, modal open/close). That path must not touch grid settings: any
|
||||
* `updateSettings()` while a multi-column sort is active corrupts formula
|
||||
* references into `#REF!` (the bug `updateSettingsSortSafe()` works around),
|
||||
* and its workaround - clear the sort, apply, re-apply - re-sorts the data
|
||||
* underneath an open cell editor.
|
||||
*
|
||||
* This pins that contract against a real Handsontable instance, using the same
|
||||
* technique as the other editor integration specs (editor.component.ts has no
|
||||
* TestBed harness).
|
||||
*/
|
||||
describe('refreshGridAfterContainerResize', () => {
|
||||
let container: HTMLDivElement
|
||||
let hot: Handsontable
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
hot = new Handsontable(container, {
|
||||
data: [
|
||||
[3, 10, '=A1*B1'],
|
||||
[1, 20, '=A2*B2'],
|
||||
[2, 30, '=A3*B3']
|
||||
],
|
||||
columns: [{ data: 0 }, { data: 1 }, { data: 2, readOnly: true }],
|
||||
formulas: { engine: HyperFormula, licenseKey: 'gpl-v3' },
|
||||
multiColumnSorting: true,
|
||||
licenseKey: 'non-commercial-and-evaluation'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (hot && !hot.isDestroyed) hot.destroy()
|
||||
hot = undefined as any
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('re-measures without changing settings, so an active sort survives', () => {
|
||||
const sorting = hot.getPlugin('multiColumnSorting') as any
|
||||
sorting.sort([{ column: 0, sortOrder: 'asc' }])
|
||||
const sortBefore = JSON.parse(JSON.stringify(sorting.getSortConfig()))
|
||||
|
||||
const updateSettings = spyOn(hot, 'updateSettings').and.callThrough()
|
||||
const clearSort = spyOn(sorting, 'clearSort').and.callThrough()
|
||||
const sort = spyOn(sorting, 'sort').and.callThrough()
|
||||
const refreshDimensions = spyOn(hot, 'refreshDimensions').and.callThrough()
|
||||
|
||||
refreshGridAfterContainerResize(hot)
|
||||
|
||||
expect(refreshDimensions).toHaveBeenCalledTimes(1)
|
||||
expect(updateSettings).not.toHaveBeenCalled()
|
||||
expect(clearSort).not.toHaveBeenCalled()
|
||||
expect(sort).not.toHaveBeenCalled()
|
||||
expect(JSON.parse(JSON.stringify(sorting.getSortConfig()))).toEqual(
|
||||
sortBefore
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves formula values intact across a resize', () => {
|
||||
const valuesBefore = hot.getData()
|
||||
|
||||
refreshGridAfterContainerResize(hot)
|
||||
|
||||
expect(hot.getData()).toEqual(valuesBefore)
|
||||
const flat = (hot.getData() as any[][]).flat()
|
||||
expect(flat.some((value) => String(value).includes('#REF'))).toBeFalse()
|
||||
})
|
||||
|
||||
it('ignores a missing or destroyed instance', () => {
|
||||
expect(() => refreshGridAfterContainerResize(null)).not.toThrow()
|
||||
expect(() => refreshGridAfterContainerResize(undefined)).not.toThrow()
|
||||
|
||||
hot.destroy()
|
||||
expect(() => refreshGridAfterContainerResize(hot)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The observer is what makes the helper fire, and it is the wiring - not the
|
||||
* helper body - that regressed into `updateSettings()` in the first place. A
|
||||
* helper-only spec passes even if the observer reapplies settings, so this one
|
||||
* drives a real container resize through the observer and fails on any
|
||||
* settings call.
|
||||
*/
|
||||
describe('observeGridContainerResize', () => {
|
||||
it('re-measures on a container resize without touching grid settings', async () => {
|
||||
const container = document.createElement('div')
|
||||
container.style.width = '500px'
|
||||
document.body.appendChild(container)
|
||||
|
||||
const hot = new Handsontable(container, {
|
||||
data: [
|
||||
[3, 10, '=A1*B1'],
|
||||
[1, 20, '=A2*B2']
|
||||
],
|
||||
colHeaders: true,
|
||||
width: '100%',
|
||||
height: 300,
|
||||
licenseKey: undefined
|
||||
})
|
||||
|
||||
const settingsSpy = spyOn(hot, 'updateSettings').and.callThrough()
|
||||
const refreshSpy = spyOn(hot, 'refreshDimensions').and.callThrough()
|
||||
|
||||
const observer = observeGridContainerResize(hot, container)
|
||||
expect(observer).toBeDefined()
|
||||
|
||||
container.style.width = '320px'
|
||||
await new Promise((resolve) => setTimeout(resolve, 400))
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalled()
|
||||
expect(settingsSpy).not.toHaveBeenCalled()
|
||||
|
||||
observer?.disconnect()
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('returns nothing when the grid or container is missing', () => {
|
||||
const container = document.createElement('div')
|
||||
|
||||
expect(observeGridContainerResize(null, container)).toBeUndefined()
|
||||
expect(observeGridContainerResize(undefined, undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import Handsontable from 'handsontable'
|
||||
|
||||
/**
|
||||
* Re-measures a grid after its container changed size (sidebar toggle, modal
|
||||
* open/close, window resize).
|
||||
*
|
||||
* Deliberately does NOT call `updateSettings()`. Any settings call while a
|
||||
* multi-column sort is active corrupts formula cell references into `#REF!` -
|
||||
* that is the bug `updateSettingsSortSafe()` exists for - and the workaround
|
||||
* (clear the sort, apply, re-apply the sort) re-sorts the data underneath an
|
||||
* open cell editor, so typing can land on a different row than the one being
|
||||
* edited. A container resize has no business changing settings at all:
|
||||
* `refreshDimensions()` measures `rootElement.getBoundingClientRect()` and
|
||||
* compares it with the last measured size, re-running `adjustElementsSize()`
|
||||
* and a render only when the box actually moved. Both grids take their height
|
||||
* from a viewport-derived `calc()` setting (editor.component.ts:203), so a
|
||||
* container resize that matters to the grid is a width change or a viewport
|
||||
* change - both of which move that box. It also fires
|
||||
* `beforeRefreshDimensions` / `afterRefreshDimensions` for anything that needs
|
||||
* to react.
|
||||
*/
|
||||
export function refreshGridAfterContainerResize(
|
||||
hot: Handsontable | null | undefined
|
||||
): void {
|
||||
if (!hot || hot.isDestroyed) return
|
||||
hot.refreshDimensions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches a grid's container and re-measures the grid whenever its box changes
|
||||
* (sidebar toggle, modal open/close, window resize).
|
||||
*
|
||||
* The observer lives here rather than in the component so the wiring is
|
||||
* covered by a spec: what matters is not that `refreshDimensions()` is called
|
||||
* but that a resize never reaches `updateSettings()`.
|
||||
*
|
||||
* Returns the observer so the caller can disconnect it on destroy.
|
||||
*/
|
||||
export function observeGridContainerResize(
|
||||
hot: Handsontable | null | undefined,
|
||||
container: HTMLElement | null | undefined
|
||||
): ResizeObserver | undefined {
|
||||
if (!hot || !container || typeof ResizeObserver === 'undefined') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => refreshGridAfterContainerResize(hot))
|
||||
})
|
||||
observer.observe(container)
|
||||
|
||||
return observer
|
||||
}
|
||||
Reference in new issue
Block a user