Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/joint-core/src/dia/Paper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2329,13 +2329,21 @@ export const Paper = View.extend({
const maxWidth = opt.maxWidth || Number.MAX_VALUE;
const maxHeight = opt.maxHeight || Number.MAX_VALUE;
const newOrigin = opt.allowNewOrigin;
// Shift the grid anchor so the first cell starts at (originX, originY)
// in paper-local coords. Default 0 reproduces the prior behavior.
const originX = opt.originX || 0;
const originY = opt.originY || 0;

const area = ('contentArea' in opt) ? new Rect(opt.contentArea) : this.getContentArea(opt);
const { sx, sy } = this.scale();
area.x *= sx;
area.y *= sy;
area.width *= sx;
area.height *= sy;
// Move the area into the origin-relative coordinate system.
// All grid math below then operates relative to (originX, originY).
area.x -= originX * sx;
area.y -= originY * sy;

let calcWidth = Math.ceil((area.width + area.x) / gridWidth);
let calcHeight = Math.ceil((area.height + area.y) / gridHeight);
Expand Down Expand Up @@ -2371,7 +2379,9 @@ export const Paper = View.extend({
calcWidth = Math.min(calcWidth, maxWidth);
calcHeight = Math.min(calcHeight, maxHeight);

return new Rect(-tx / sx, -ty / sy, calcWidth / sx, calcHeight / sy);
// Translate the returned rect back into the absolute coordinate
// system (undo the origin-relative shift applied to area.x/y).
return new Rect(-tx / sx + originX, -ty / sy + originY, calcWidth / sx, calcHeight / sy);
},

transformToFitContent: function(opt) {
Expand Down
49 changes: 49 additions & 0 deletions packages/joint-core/test/jointjs/dia/Paper.js
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,55 @@ QUnit.module('joint.dia.Paper', function(hooks) {
});
});

QUnit.module('fitToContent() > options > originX / originY', function() {

QUnit.test('default origin (0, 0) — backward compatible', function(assert) {
addCells(graph);
var area = paper.fitToContent({
useModelGeometry: true,
gridWidth: 100,
gridHeight: 100,
allowNewOrigin: 'any'
});
// Content spans (-100, -100) → (200, 300). Grid at 0:
// Right edge rounds to 200, Bottom to 300.
// New origin shifts negative content into view.
assert.deepEqual(area.toJSON(), { x: -100, y: -100, width: 300, height: 400 });
});

QUnit.test('non-zero origin shifts the grid anchor', function(assert) {
addCells(graph);
var area = paper.fitToContent({
useModelGeometry: true,
gridWidth: 100,
gridHeight: 100,
allowNewOrigin: 'any',
originX: 50,
originY: 25
});
// Origin shifted by (50, 25): the returned rect's
// top-left moves by the same amount; dimensions are
// unchanged because content/grid relationship hasn't
// changed in origin-relative space.
assert.deepEqual(area.toJSON(), { x: -50, y: -75, width: 300, height: 400 });
});

QUnit.test('origin without allowNewOrigin still offsets the rect', function(assert) {
addCells(graph);
var area = paper.fitToContent({
useModelGeometry: true,
gridWidth: 100,
gridHeight: 100,
originX: 50,
originY: 25
});
// Without allowNewOrigin the negative content is
// ignored; rect origin = (originX, originY).
assert.deepEqual(area.x, 50);
assert.deepEqual(area.y, 25);
});
});

QUnit.module('fitToContent() > options > useModelGeometry', function() {

[0.5, 1, 2].forEach(function(scale) {
Expand Down
6 changes: 6 additions & 0 deletions packages/joint-core/types/dia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,12 @@ export namespace Paper {
maxHeight?: number;
useModelGeometry?: boolean;
contentArea?: BBox;
/**
* Shifts the grid anchor so the first cell starts at
* (`originX`, `originY`) in paper-local coordinates. Default `0`.
*/
originX?: number;
originY?: number;
}

interface EventMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ export function RenderItemDecorator(
return (
<div style={{ width: '100%', height: 450 }}>
<GraphProvider initialCells={properties.cells ?? testCells}>
<Paper
height={450}
<Paper style={{ height: 450 }}
className={PAPER_CLASSNAME}
renderElement={properties.renderElement}
renderLink={properties.renderLink}
Expand All @@ -105,7 +104,7 @@ export function RenderGraphViewWithChildren(properties: Readonly<{ children: JSX
return (
<div style={{ width: '100%', height: 350 }}>
<SimpleGraphProviderDecorator>
<Paper height={350} className={PAPER_CLASSNAME} renderElement={RenderSimpleRectElement}>
<Paper style={{ height: 350 }} className={PAPER_CLASSNAME} renderElement={RenderSimpleRectElement}>
{properties.children}
</Paper>
</SimpleGraphProviderDecorator>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Paper', () => {
});
return (
<GraphProvider initialCells={CELLS}>
<Paper ref={ref} width={100} height={100} renderElement={renderRectElement} />
<Paper style={{ width: 100, height: 100 }} ref={ref} renderElement={renderRectElement} />
</GraphProvider>
);
}
Expand Down
20 changes: 1 addition & 19 deletions packages/joint-react/src/components/paper/paper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,6 @@ import { PaperStoreContext } from '../../context';
import { useCreatePortalPaper } from '../../hooks/use-create-portal-paper';
import type { PaperProps } from './paper.types';

/**
* Resolves a CSS dimension value to a JointJS Paper dimension.
* @param dimension - The CSS width or height value.
* @returns The resolved dimension or undefined.
*/
function resolveStyleDimension(
dimension: React.CSSProperties['width'] | React.CSSProperties['height']
): dia.Paper.Dimension | undefined {
if (dimension === undefined) {
return undefined;
}
return dimension;
}

/**
* Internal Paper implementation used by forwarded `Paper` component.
* @param props - Paper component props.
Expand All @@ -29,17 +15,13 @@ function PaperBase(
props: Readonly<PaperProps>,
forwardedRef: React.ForwardedRef<dia.Paper | null>
) {
const { className, style, children, width, height, paper: externalPaper } = props;
const resolvedWidth = width ?? resolveStyleDimension(style?.width);
const resolvedHeight = height ?? resolveStyleDimension(style?.height);
const { className, style, children, paper: externalPaper } = props;
const paperHTMLElementRef = useRef<HTMLDivElement | null>(null);
const reactId = useId();
const id = props.id ?? `paper-${reactId}`;
const isExternalPaper = !!externalPaper;
const { paperRef, paperStore, isReady, content } = useCreatePortalPaper({
...props,
width: resolvedWidth,
height: resolvedHeight,
elementRef: isExternalPaper ? undefined : paperHTMLElementRef,
id,
style,
Expand Down
45 changes: 10 additions & 35 deletions packages/joint-react/src/components/paper/paper.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ export interface PortalPaperOptions {
/** Unique identifier used by joint-react to track the paper instance. */
readonly id?: string;

// ── Sizing & appearance ──────────────────────────────────────────────────
readonly width?: dia.Paper.Options['width'];
readonly height?: dia.Paper.Options['height'];
// ── Appearance ───────────────────────────────────────────────────────────
// Note: sizing is intentionally NOT exposed. Paper is sized exclusively
// by host CSS (`className` / `style`); `paper.getComputedSize()` falls
// back to `el.clientWidth/clientHeight` when `options.width/height` are
// not numeric (see `dia.Paper.getComputedSize`).
readonly drawGrid?: dia.Paper.Options['drawGrid'];
readonly drawGridSize?: dia.Paper.Options['drawGridSize'];
readonly gridSize?: dia.Paper.Options['gridSize'];
Expand Down Expand Up @@ -213,30 +215,6 @@ export type RenderLink<LinkData = unknown> = (data: LinkData) => ReactNode;
* @see https://docs.jointjs.com/api/dia/Paper
*/
export interface PaperProps extends PortalPaperOptions, PropsWithChildren {
/**
* Width of the paper host element.
*
* Precedence for width is:
* 1. `width` prop
* 2. `style.width`
* 3. CSS width from `className`
*
* When this prop is omitted, the Paper component falls back to `style.width`.
* If both are omitted, width is left unset so host CSS can size the paper.
*/
readonly width?: dia.Paper.Dimension;
/**
* Height of the paper host element.
*
* Precedence for height is:
* 1. `height` prop
* 2. `style.height`
* 3. CSS height from `className`
*
* When this prop is omitted, the Paper component falls back to `style.height`.
* If both are omitted, height is left unset so host CSS can size the paper.
*/
readonly height?: dia.Paper.Dimension;
/**
* A function that renders the element.
*
Expand Down Expand Up @@ -292,17 +270,14 @@ export interface PaperProps extends PortalPaperOptions, PropsWithChildren {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly renderLink?: RenderLink<any>;
/**
* Inline styles applied to the paper host element.
*
* For sizing, `style.width` and `style.height` are used only when the matching
* `width` / `height` props are not provided.
* Inline styles applied to the paper host element. Use `style.width` and
* `style.height` (or CSS via `className`) to size the paper — Paper does
* not expose dedicated width/height props.
*/
readonly style?: CSSProperties;
/**
* CSS classes applied to the paper host element.
*
* Class-based sizing is lowest priority and is used only when the matching
* `width` / `height` prop and `style.width` / `style.height` are omitted.
* CSS classes applied to the paper host element. Combine with width /
* height rules to size the paper.
*/
readonly className?: string;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('paper-element-item exports', () => {
let capturedPaper: unknown = null;
const { rerender, container } = render(
<GraphProvider initialCells={CELLS}>
<Paper width={100} height={100} renderElement={() => <rect />}>
<Paper style={{ width: 100, height: 100 }} renderElement={() => <rect />}>
<StoreCapture
onCapture={(graph, paper) => {
capturedGraph = graph;
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('paper-element-item exports', () => {
let capturedPaper: unknown = null;
const { rerender, container } = render(
<GraphProvider initialCells={CELLS}>
<Paper width={100} height={100} useHTMLOverlay renderElement={() => <rect />}>
<Paper style={{ width: 100, height: 100 }} useHTMLOverlay renderElement={() => <rect />}>
<StoreCapture
onCapture={(graph, paper) => {
capturedGraph = graph;
Expand Down Expand Up @@ -117,7 +117,7 @@ describe('paper-element-item exports', () => {
let capturedPaper: unknown = null;
const { rerender, container } = render(
<GraphProvider initialCells={CELLS}>
<Paper width={100} height={100} useHTMLOverlay renderElement={() => <rect />}>
<Paper style={{ width: 100, height: 100 }} useHTMLOverlay renderElement={() => <rect />}>
<StoreCapture
onCapture={(graph, paper) => {
capturedGraph = graph;
Expand Down Expand Up @@ -167,7 +167,7 @@ describe('paper-element-item exports', () => {
let capturedPaper: unknown = null;
const { rerender, container } = render(
<GraphProvider initialCells={CELLS}>
<Paper width={100} height={100} renderElement={() => <rect />}>
<Paper style={{ width: 100, height: 100 }} renderElement={() => <rect />}>
<StoreCapture
onCapture={(graph, paper) => {
capturedGraph = graph;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ describe('Paper with useHTMLOverlay', () => {
it('renders elements through the HTML overlay container with positioned wrappers', async () => {
const { container } = render(
<GraphProvider initialCells={HTML_CELLS}>
<Paper
width={300}
height={300}
<Paper style={{ width: 300, height: 300 }}
useHTMLOverlay
renderElement={({ label }: { label: string }) => (
<div data-testid={`label-${label}`}>{label}</div>
Expand Down Expand Up @@ -63,9 +61,7 @@ describe('Paper with useHTMLOverlay', () => {
it('keeps the HTML overlay positioned correctly while paper is mounted', async () => {
const { container } = render(
<GraphProvider initialCells={HTML_CELLS}>
<Paper
width={200}
height={200}
<Paper style={{ width: 200, height: 200 }}
useHTMLOverlay
renderElement={() => <span>x</span>}
/>
Expand Down
1 change: 1 addition & 0 deletions packages/joint-react/src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@

.jj-paper {
background-color: var(--jj-paper-color);
height: stretch;
}

/* ── Elements ────────────────────────────────────────────────────────── */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('useCreateFeature — paper target lifecycle', () => {
}));
render(
<GraphProvider initialCells={initialCells}>
<Paper id="features-paper" width={100} height={100} renderElement={noopRender}>
<Paper style={{ width: 100, height: 100 }} id="features-paper" renderElement={noopRender}>
<FeaturesProvider target="paper" id="paper-feat-1" onAddFeature={onAdd}>
<div>paper-child</div>
</FeaturesProvider>
Expand All @@ -55,7 +55,7 @@ describe('useCreateFeature — paper target lifecycle', () => {
}));
const { unmount } = render(
<GraphProvider initialCells={initialCells}>
<Paper id="features-cleanup-paper" width={100} height={100} renderElement={noopRender}>
<Paper style={{ width: 100, height: 100 }} id="features-cleanup-paper" renderElement={noopRender}>
<FeaturesProvider target="paper" id="paper-feat-cleanup" onAddFeature={onAdd}>
<div>cleanup-child</div>
</FeaturesProvider>
Expand All @@ -76,7 +76,7 @@ describe('useCreateFeature — paper target lifecycle', () => {
}));
render(
<GraphProvider initialCells={initialCells}>
<Paper id="features-onload-paper" width={100} height={100} renderElement={noopRender}>
<Paper style={{ width: 100, height: 100 }} id="features-onload-paper" renderElement={noopRender}>
<FeaturesProvider
target="paper"
id="paper-feat-onload"
Expand Down Expand Up @@ -106,7 +106,7 @@ describe('useCreateFeature — paper target lifecycle', () => {
function App({ value }: Readonly<{ value: number }>) {
return (
<GraphProvider initialCells={initialCells}>
<Paper id="features-update-paper" width={100} height={100} renderElement={noopRender}>
<Paper style={{ width: 100, height: 100 }} id="features-update-paper" renderElement={noopRender}>
<FeaturesProvider
target="paper"
id="paper-feat-update"
Expand Down Expand Up @@ -144,10 +144,8 @@ describe('useCreateFeature — paper target lifecycle', () => {
render(
<GraphProvider initialCells={initialCells}>
<FeaturesProvider target="paper" id="paper-feat-deferred" onAddFeature={onAdd}>
<Paper
<Paper style={{ width: 100, height: 100 }}
id="features-deferred-paper"
width={100}
height={100}
renderElement={noopRender}
>
<div>deferred-child</div>
Expand Down
Loading