diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index affdcd98ad..f70ce26ccd 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -668,6 +668,7 @@ export const Paper = View.extend({ this.cloneOptions(); this.render(); this._setDimensions(); + this._startObservingElementSize(); this.startListening(); // Mouse wheel events buffer @@ -2253,6 +2254,7 @@ export const Paper = View.extend({ onRemove: function() { + this._stopObservingElementSize(); this.freeze(); this._updates.disabled = true; //clean up all DOM elements/views to prevent memory leaks @@ -2281,6 +2283,10 @@ export const Paper = View.extend({ this._setDimensions(); const computedSize = this.getComputedSize(); this.trigger('resize', computedSize.width, computedSize.height, data); + // Re-evaluate observer attachment in case the user toggled between + // explicit-size and CSS-relative modes. + this._stopObservingElementSize(); + this._startObservingElementSize(); }, _setDimensions: function() { @@ -2295,6 +2301,47 @@ export const Paper = View.extend({ }); }, + _elementResizeObserver: null, + _lastObservedSize: null, + + _startObservingElementSize: function() { + if (this._elementResizeObserver) return; + if (typeof ResizeObserver === 'undefined') return; + const { width, height } = this.options; + // Explicit-size mode: `setDimensions()` is the sole source of 'resize'. + if (isNumber(width) && isNumber(height)) return; + + const size = this.getComputedSize(); + this._lastObservedSize = { width: size.width, height: size.height }; + + this._elementResizeObserver = new ResizeObserver(() => { + if (!this.el || !this._elementResizeObserver) return; + // Re-read via getComputedSize() rather than the ResizeObserverEntry + // payload. `clientWidth/clientHeight` (the non-numeric branch of + // getComputedSize) is the padding box minus scrollbar — none of + // `contentBoxSize` / `borderBoxSize` / `contentRect` match exactly + // once the host has padding or border. Re-reading keeps the size + // we emit identical to every other `getComputedSize()` caller; the + // cost is one DOM read in a post-layout callback. + const next = this.getComputedSize(); + const prev = this._lastObservedSize; + if (prev && prev.width === next.width && prev.height === next.height) return; + this._lastObservedSize = { width: next.width, height: next.height }; + // The host already has its CSS-driven size; do not call + // `_setDimensions()` here — writing px values back would clobber CSS. + this.trigger('resize', next.width, next.height, { source: 'observer' }); + }); + this._elementResizeObserver.observe(this.el); + }, + + _stopObservingElementSize: function() { + if (this._elementResizeObserver) { + this._elementResizeObserver.disconnect(); + this._elementResizeObserver = null; + } + this._lastObservedSize = null; + }, + // Expand/shrink the paper to fit the content. // Alternatively signature function(opt) fitToContent: function(gridWidth, gridHeight, padding, opt) { diff --git a/packages/joint-core/test/jointjs/paper.js b/packages/joint-core/test/jointjs/paper.js index d35166fe7e..e5fd1db86e 100644 --- a/packages/joint-core/test/jointjs/paper.js +++ b/packages/joint-core/test/jointjs/paper.js @@ -95,6 +95,79 @@ QUnit.module('paper', function(hooks) { resizeCbSpy.resetHistory(); }); + QUnit.module('autoResizePaper', function() { + + // Waits for the ResizeObserver to deliver its async callback. + // Two rAFs cover the spec's "delivered before the next paint" plus + // browser implementation slack. + function afterResizeObserverDelivery(callback) { + requestAnimationFrame(function() { + requestAnimationFrame(callback); + }); + } + + QUnit.test('fires resize when host CSS size changes', function(assert) { + var done = assert.async(); + var paper = this.paper; + // '100%' / '100%' makes the paper fill the container while + // keeping options non-numeric (i.e. observer-eligible). + $container.css({ width: '200px', height: '100px' }); + paper.setDimensions('100%', '100%'); + var spy = sinon.spy(); + paper.on('resize', spy); + afterResizeObserverDelivery(function() { + spy.resetHistory(); + $container.css({ width: '300px', height: '150px' }); + afterResizeObserverDelivery(function() { + assert.ok(spy.called, 'resize fires for host size change'); + var args = spy.lastCall.args; + assert.equal(args[0], 300, 'width reports host clientWidth'); + assert.equal(args[1], 150, 'height reports host clientHeight'); + assert.deepEqual(args[2], { source: 'observer' }, 'data carries observer source'); + done(); + }); + }); + }); + + QUnit.test('numeric dimensions skip the observer', function(assert) { + var done = assert.async(); + var paper = this.paper; + paper.setDimensions(200, 100); + var spy = sinon.spy(); + paper.on('resize', spy); + $container.css({ width: '500px', height: '500px' }); + afterResizeObserverDelivery(function() { + assert.ok(spy.notCalled, 'host CSS resize ignored in explicit-size mode'); + done(); + }); + }); + + QUnit.test('disconnects on paper.remove()', function(assert) { + var paperEl = document.createElement('div'); + $container.append(paperEl); + var paper = new joint.dia.Paper({ + el: paperEl, + model: new joint.dia.Graph, + width: '100%', + height: '100%' + }); + assert.ok(paper._elementResizeObserver, 'observer attached on init'); + var disconnectSpy = sinon.spy(paper._elementResizeObserver, 'disconnect'); + paper.remove(); + assert.ok(disconnectSpy.calledOnce, 'disconnect called exactly once on remove'); + assert.notOk(paper._elementResizeObserver, 'observer reference cleared'); + }); + + QUnit.test('toggling between fixed and CSS-relative dimensions attaches/detaches observer', function(assert) { + var paper = this.paper; + assert.notOk(paper._elementResizeObserver, 'no observer with default numeric dims'); + paper.setDimensions('100%', '100%'); + assert.ok(paper._elementResizeObserver, 'observer attached after switching to CSS-relative'); + paper.setDimensions(100, 100); + assert.notOk(paper._elementResizeObserver, 'observer detached when both dims numeric again'); + }); + }); + }); QUnit.test('paper.addCell() number of sort()', function(assert) {