diff --git a/.changeset/paper-content-area-filter.md b/.changeset/paper-content-area-filter.md new file mode 100644 index 0000000000..f67d91444f --- /dev/null +++ b/.changeset/paper-content-area-filter.md @@ -0,0 +1,5 @@ +--- +"@joint/core": minor +--- + +dia.Paper - support `useModelGeometry: { filter }` in `getContentArea()` and the fit-to-content methods diff --git a/packages/joint-core/src/dia/Paper.mjs b/packages/joint-core/src/dia/Paper.mjs index a6c3e8b496..1e3504489d 100644 --- a/packages/joint-core/src/dia/Paper.mjs +++ b/packages/joint-core/src/dia/Paper.mjs @@ -2596,7 +2596,15 @@ export const Paper = View.extend({ getContentArea: function(opt) { if (opt && opt.useModelGeometry) { - return this.model.getBBox() || new Rect(); + const { model: graph } = this; + // `useModelGeometry: true` or `useModelGeometry: { filter }` + const { filter } = isObject(opt.useModelGeometry) ? opt.useModelGeometry : {}; + if (typeof filter !== 'function') { + return graph.getBBox() || new Rect(); + } + // Only the cells passing the `filter` are considered. + const cells = graph.getCells().filter(cell => filter(cell)); + return graph.getCellsBBox(cells) || new Rect(); } const graphLayerViews = this.getGraphLayerViews(); diff --git a/packages/joint-core/test/jointjs/dia/Paper.js b/packages/joint-core/test/jointjs/dia/Paper.js index 61c7cef349..c6d3101d0f 100644 --- a/packages/joint-core/test/jointjs/dia/Paper.js +++ b/packages/joint-core/test/jointjs/dia/Paper.js @@ -278,6 +278,67 @@ QUnit.module('joint.dia.Paper', function(hooks) { }); }); + QUnit.module('getContentArea() > options > useModelGeometry > filter', function() { + + QUnit.test('filter', function(assert) { + addCells(graph); + var [rect1, rect2] = graph.getElements(); + var filter = sinon.spy((cell) => cell.isElement()); + var area = paper.getContentArea({ useModelGeometry: { filter }}); + assert.deepEqual(area.toJSON(), { x: -100, y: -100, width: 300, height: 300 }, 'filtered cells are excluded'); + assert.equal(filter.callCount, 3, 'filter called for every cell'); + assert.ok(filter.getCalls().every(call => call.args.length === 1), 'filter called with the cell only'); + + area = paper.getContentArea({ useModelGeometry: { filter: (cell) => cell === rect2 }}); + assert.deepEqual(area.toJSON(), { x: 100, y: 100, width: 100, height: 100 }); + + area = paper.getContentArea({ useModelGeometry: { filter: () => false }}); + assert.ok(area instanceof g.Rect); + assert.deepEqual(area.toJSON(), { x: 0, y: 0, width: 0, height: 0 }, 'empty area when no cell passes the filter'); + + area = paper.getContentBBox({ useModelGeometry: { filter: (cell) => cell === rect1 }}); + assert.deepEqual(area.toJSON(), { x: -100, y: -100, width: 100, height: 100 }, 'getContentBBox() accepts the filter'); + }); + + QUnit.test('no filter', function(assert) { + addCells(graph); + var area = paper.getContentArea({ useModelGeometry: {}}); + assert.deepEqual(area.toJSON(), { x: -100, y: -100, width: 300, height: 400 }, 'an object without filter measures every cell'); + }); + }); + + QUnit.module('fitToContent() > options > useModelGeometry > filter', function() { + + QUnit.test('filter', function(assert) { + addCells(graph); + var area = paper.fitToContent({ + useModelGeometry: { filter: (cell) => cell.isElement() }, + allowNewOrigin: 'any' + }); + assert.deepEqual(area.toJSON(), { x: -100, y: -100, width: 300, height: 300 }); + assert.deepEqual(paper.getArea().toJSON(), { x: -100, y: -100, width: 300, height: 300 }); + + area = paper.getFitToContentArea({ + useModelGeometry: { filter: (cell) => cell.isElement() && cell.position().x > 0 }, + allowNewOrigin: 'any' + }); + assert.deepEqual(area.toJSON(), { x: 100, y: 100, width: 100, height: 100 }, 'getFitToContentArea() accepts the filter'); + }); + }); + + QUnit.module('transformToFitContent() > options > useModelGeometry > filter', function() { + + QUnit.test('filter', function(assert) { + addCells(graph); + paper.setDimensions(200, 200); + paper.transformToFitContent({ + useModelGeometry: { filter: (cell) => cell.isElement() && cell.position().x > 0 } + }); + assert.deepEqual(paper.scale(), { sx: 2, sy: 2 }); + assert.deepEqual(paper.translate(), { tx: -200, ty: -200 }); + }); + }); + QUnit.module('fitToContent() > options', function() { [{ diff --git a/packages/joint-core/test/ts/index.test.ts b/packages/joint-core/test/ts/index.test.ts index 810ebd5cd9..a7e8c40bef 100644 --- a/packages/joint-core/test/ts/index.test.ts +++ b/packages/joint-core/test/ts/index.test.ts @@ -179,6 +179,13 @@ const paper = new joint.dia.Paper({ paper.fitToContent({ padding: { top: 10 }, allowNewOrigin: false }); +// Measure the content only from the cells that pass the `filter`. +const isVisibleCell = (cell: joint.dia.Cell) => !cell.get('hidden'); +paper.fitToContent({ useModelGeometry: { filter: isVisibleCell }}); +paper.transformToFitContent({ useModelGeometry: { filter: isVisibleCell }}); +paper.getContentArea({ useModelGeometry: { filter: isVisibleCell }}).inflate(5); +paper.getContentBBox({ useModelGeometry: true }).inflate(5); + const eventOptions: joint.dia.Paper.Options[] = [ // `guard` is called without a view when the event did not hit a cell view. { guard: (_evt, view) => view?.model.isElement() ?? false } diff --git a/packages/joint-core/types/dia.d.ts b/packages/joint-core/types/dia.d.ts index 2a05c30055..1ce4b4f9e9 100644 --- a/packages/joint-core/types/dia.d.ts +++ b/packages/joint-core/types/dia.d.ts @@ -1882,7 +1882,23 @@ export namespace Paper { disposeHidden?: boolean; } - interface TransformToFitContentOptions { + interface ModelGeometryOptions { + /** + * Only the cells for which the function returns `true` contribute + * to the content area. + */ + filter?: (cell: Cell) => boolean; + } + + interface ContentAreaOptions { + /** + * Measure the content from the model geometry instead of the + * rendered views. Pass an object to configure the measurement. + */ + useModelGeometry?: boolean | ModelGeometryOptions; + } + + interface TransformToFitContentOptions extends ContentAreaOptions { padding?: Padding; preserveAspectRatio?: boolean; minScale?: number; @@ -1892,7 +1908,6 @@ export namespace Paper { maxScaleX?: number; maxScaleY?: number; scaleGrid?: number; - useModelGeometry?: boolean; fittingBBox?: BBox; contentArea?: BBox; verticalAlign?: 'top' | 'middle' | 'bottom'; @@ -1904,7 +1919,7 @@ export namespace Paper { */ type ScaleContentOptions = TransformToFitContentOptions; - interface FitToContentOptions { + interface FitToContentOptions extends ContentAreaOptions { gridWidth?: number; gridHeight?: number; padding?: Padding; @@ -1914,7 +1929,6 @@ export namespace Paper { minHeight?: number; maxWidth?: number; maxHeight?: number; - useModelGeometry?: boolean; contentArea?: BBox; /** * Shifts the grid anchor so the first cell starts at @@ -2149,9 +2163,9 @@ export class Paper extends mvc.View { getRestrictedArea(): g.Rect | null; getRestrictedArea(elementView: ElementView, x: number, y: number): g.Rect | null | Paper.PointConstraintCallback; - getContentArea(opt?: { useModelGeometry: boolean }): g.Rect; + getContentArea(opt?: Paper.ContentAreaOptions): g.Rect; - getContentBBox(opt?: { useModelGeometry: boolean }): g.Rect; + getContentBBox(opt?: Paper.ContentAreaOptions): g.Rect; findView(element: mvc.$SVGElement): T;