diff --git a/docs/scriptappy.json b/docs/scriptappy.json index d361aafda..1a5c59729 100644 --- a/docs/scriptappy.json +++ b/docs/scriptappy.json @@ -4517,6 +4517,12 @@ "description": "left, right, top or bottom", "optional": true, "type": "string" + }, + "overlap": { + "description": "If true, the component is anchored at the center boundary and does not consume layout space", + "optional": true, + "defaultValue": false, + "type": "boolean" } } }, diff --git a/packages/picasso.js/src/core/chart/index.js b/packages/picasso.js/src/core/chart/index.js index d8f57dede..48e2304b3 100644 --- a/packages/picasso.js/src/core/chart/index.js +++ b/packages/picasso.js/src/core/chart/index.js @@ -102,6 +102,7 @@ import componentCollectionFn from './component-collection'; * @property {number} [layout.prioOrder = 0] * @property {string | {width: string, height: string}} [layout.minimumLayoutMode] Refer to layout sizes defined by layoutModes in `strategy` * @property {string} [layout.dock] left, right, top or bottom + * @property {boolean} [layout.overlap = false] If true, the component is anchored at the center boundary and does not consume layout space * @property {boolean} [show = true] If the component should be rendered * @property {string} [scale] Named scale. Will be provided to the component if it asks for it. * @property {string} [formatter] Named formatter. Fallback to create formatter from scale. Will be provided to the component if it asks for it. diff --git a/packages/picasso.js/src/core/component/component-factory.js b/packages/picasso.js/src/core/component/component-factory.js index 4426fcab1..b304a4094 100644 --- a/packages/picasso.js/src/core/component/component-factory.js +++ b/packages/picasso.js/src/core/component/component-factory.js @@ -166,6 +166,7 @@ function createDockDefinition(settings, preferredSize, logger) { def.dock = getLayoutProperty('dock'); def.prioOrder = getLayoutProperty('prioOrder'); def.minimumLayoutMode = getLayoutProperty('minimumLayoutMode'); + def.overlap = getLayoutProperty('overlap'); // move layout properties to layout object settings.layout = settings.layout || {}; @@ -174,6 +175,7 @@ function createDockDefinition(settings, preferredSize, logger) { settings.layout.prioOrder = typeof def.prioOrder !== 'undefined' ? def.prioOrder : settings.layout.prioOrder; settings.layout.dock = def.dock || settings.layout.dock; settings.layout.minimumLayoutMode = def.minimumLayoutMode || settings.layout.minimumLayoutMode; + settings.layout.overlap = typeof def.overlap !== 'undefined' ? def.overlap : settings.layout.overlap; // not directly a dock layout property def.show = settings.show; diff --git a/packages/picasso.js/src/core/layout/dock/__tests__/dock-layout.spec.js b/packages/picasso.js/src/core/layout/dock/__tests__/dock-layout.spec.js index 1de190d91..95c6a5d71 100644 --- a/packages/picasso.js/src/core/layout/dock/__tests__/dock-layout.spec.js +++ b/packages/picasso.js/src/core/layout/dock/__tests__/dock-layout.spec.js @@ -11,6 +11,7 @@ describe('Dock Layout', () => { minimumLayoutMode, size = 0, key, + overlap = false, } = {}) { const dummy = {}; dummy.key = key; @@ -19,6 +20,7 @@ describe('Dock Layout', () => { displayOrder, prioOrder, minimumLayoutMode, + overlap, }); dummy.preferredSize = () => ({ width: size, height: size, edgeBleed }); @@ -398,6 +400,283 @@ describe('Dock Layout', () => { }); }); + describe('Overlap', () => { + let rect; + let dl; + + beforeEach(() => { + rect = createRect(0, 0, 1000, 1000); + dl = dockLayout(); + }); + + it('should position two overlap top components at the same anchor and reduce center by max height', () => { + // compA: normal top, 30px — reduces center to y=30 + // compB: overlap top, 20px — max(30, 20)=30, center stays at y=30 + // compC: overlap top, 15px — max stays 20, center stays at y=30 + // all overlap bottoms align with compA's bottom (y=30) + const compA = componentMock({ dock: 'top', size: 30 }); + const compB = componentMock({ dock: 'top', size: 20, overlap: true }); + const compC = componentMock({ dock: 'top', size: 15, overlap: true }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, center]); + + // center: max(30, 20) = 30 + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 30, width: 1000, height: 970 }); + + // compA: normal top, y=0, h=30, bottom=30 + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 30 }); + + // compB: overlap top, anchor=30, y=30-20=10, bottom=30 (same as compA) + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 0, y: 10, width: 1000, height: 20 }); + + // compC: overlap top, anchor=30, y=30-15=15, bottom=30 (same as compA) + expect(compC.rect, 'compC rect incorrect').to.deep.include({ x: 0, y: 15, width: 1000, height: 15 }); + }); + + it('should position overlap bottom components at the same anchor and reduce center by max height', () => { + const compA = componentMock({ dock: 'bottom', size: 40 }); + const compB = componentMock({ dock: 'bottom', size: 25, overlap: true }); + const compC = componentMock({ dock: 'bottom', size: 10, overlap: true }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, center]); + + // center: max(1000-40=960, 1000-25=975) → bottom stays at 960, height=960 + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 960 }); + + // compA: normal bottom, y=960, h=40 (inner edge at y=960) + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 960, width: 1000, height: 40 }); + + // compB: overlap bottom, anchor=960, y=960, h=25 — inner edge aligns with compA + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 0, y: 960, width: 1000, height: 25 }); + + // compC: overlap bottom, anchor=960, y=960, h=10 — overlaps compB + expect(compC.rect, 'compC rect incorrect').to.deep.include({ x: 0, y: 960, width: 1000, height: 10 }); + }); + + it('should position overlap left components at the same anchor and reduce center by max width', () => { + const compA = componentMock({ dock: 'left', size: 40 }); + const compB = componentMock({ dock: 'left', size: 25, overlap: true }); + const compC = componentMock({ dock: 'left', size: 10, overlap: true }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, center]); + + // center: max(40, 25) → left boundary stays at x=40, width=960 + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 40, y: 0, width: 960, height: 1000 }); + + // compA: normal left, x=0, w=40 (inner edge at x=40) + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 0, width: 40, height: 1000 }); + + // compB: overlap left, anchor=40, x=15, w=25 — inner edge aligns with compA + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 15, y: 0, width: 25, height: 1000 }); + + // compC: overlap left, anchor=40, x=30, w=10 — overlaps compB + expect(compC.rect, 'compC rect incorrect').to.deep.include({ x: 30, y: 0, width: 10, height: 1000 }); + }); + + it('should position overlap right components at the same anchor and reduce center by max width', () => { + const compA = componentMock({ dock: 'right', size: 40 }); + const compB = componentMock({ dock: 'right', size: 25, overlap: true }); + const compC = componentMock({ dock: 'right', size: 10, overlap: true }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, center]); + + // center: min(1000-40=960, 1000-25=975) → right boundary stays at x=960, width=960 + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 0, width: 960, height: 1000 }); + + // compA: normal right, x=960, w=40 (inner edge at x=960) + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 960, y: 0, width: 40, height: 1000 }); + + // compB: overlap right, anchor=960, x=960, w=25 — inner edge aligns with compA + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 960, y: 0, width: 25, height: 1000 }); + + // compC: overlap right, anchor=960, x=960, w=10 — overlaps compB + expect(compC.rect, 'compC rect incorrect').to.deep.include({ x: 960, y: 0, width: 10, height: 1000 }); + }); + it('should expand center when overlap exceeds sum of normal components', () => { + // A=30 normal, B=20 normal, C=70 overlap → center = max(50, 70) = 70 + // vRect starts at reducedRect.y=70: A (innermost) placed at y=40, B at y=20 + // A's inner edge (bottom=70) aligns with center boundary and C's inner edge + const compA = componentMock({ dock: 'top', size: 30 }); + const compB = componentMock({ dock: 'top', size: 20 }); + const compC = componentMock({ dock: 'top', size: 70, overlap: true }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, center]); + + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 70, width: 1000, height: 930 }); + + // compA: innermost normal, bottom=70 (aligns with center and C's inner edge) + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 40, width: 1000, height: 30 }); + // compB: outermost normal + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 0, y: 20, width: 1000, height: 20 }); + // compC: overlap, anchor=70, bottom=70 (same as compA's bottom) + expect(compC.rect, 'compC rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 70 }); + }); + + it('should not affect center when there are no non-overlap components on the same edge', () => { + // Only overlap top components — center should still be reduced by max overlap size + const compA = componentMock({ dock: 'top', size: 30, overlap: true }); + const compB = componentMock({ dock: 'top', size: 20, overlap: true }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, center]); + + // No normal top: only overlap max=30 reduces center + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 30, width: 1000, height: 970 }); + + // Both anchored at y=30 + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 30 }); + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 0, y: 10, width: 1000, height: 20 }); + }); + + it('should correctly position components docked at overlap components via @ syntax', () => { + // A: normal top 30px → center at y=30 + // B: overlap top 20px → max(30,20)=30, center stays at y=30 + // C: overlap top 15px → max stays 20, center stays at y=30 + // D: @A, E: @B, F: @C — inherit refs' rects + const compA = componentMock({ dock: 'top', size: 30, key: 'A' }); + const compB = componentMock({ dock: 'top', size: 20, overlap: true, key: 'B' }); + const compC = componentMock({ dock: 'top', size: 15, overlap: true, key: 'C' }); + const compD = componentMock({ dock: '@A' }); + const compE = componentMock({ dock: '@B' }); + const compF = componentMock({ dock: '@C' }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, compD, compE, compF, center]); + + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 30, width: 1000, height: 970 }); + + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 30 }); + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 0, y: 10, width: 1000, height: 20 }); + expect(compC.rect, 'compC rect incorrect').to.deep.include({ x: 0, y: 15, width: 1000, height: 15 }); + + // @-docked components inherit the rect of their referenced component + expect(compD.rect, 'compD (@A) rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 30 }); + expect(compE.rect, 'compE (@B) rect incorrect').to.deep.include({ x: 0, y: 10, width: 1000, height: 20 }); + expect(compF.rect, 'compF (@C) rect incorrect').to.deep.include({ x: 0, y: 15, width: 1000, height: 15 }); + }); + + it('should accumulate edgeBleed from overlap components', () => { + // overlap top component has edgeBleed.left=50, which should reduce the center x + const compA = componentMock({ dock: 'top', size: 30, overlap: true, edgeBleed: { left: 50 } }); + const center = componentMock(); + + dl.layout(rect, [compA, center]); + + // center.y pushed to 30 by overlap max-size, center.x pushed to 50 by edgeBleed + expect(center.rect, 'center rect should account for overlap edgeBleed').to.deep.include({ + x: 50, + y: 30, + width: 950, + height: 970, + }); + }); + + it('should hide lowest-priority overlap component when its size would violate center minHeightRatio', () => { + // layout: 1000x1000, minHeightRatio=0.6 → minCenterHeight=600 + // overlap top A (prioOrder=0, higher prio) size=500 → center height=500 < 600 → invalid + // overlap top B (prioOrder=1, lower prio) size=500 → shed first + // After shedding B: only A remains, center height=500 < 600 → invalid → shed A too + // After shedding both: center height=1000 ≥ 600 → valid + const settings = { center: { minHeightRatio: 0.6 } }; + dl.settings(settings); + + const compA = componentMock({ dock: 'top', size: 500, overlap: true, prioOrder: 0 }); + const compB = componentMock({ dock: 'top', size: 500, overlap: true, prioOrder: 1 }); + const center = componentMock(); + + const { visible, hidden } = dl.layout(rect, [compA, compB, center]); + + expect(hidden).to.include(compB); + expect(hidden).to.include(compA); + expect(visible).to.include(center); + expect(center.rect, 'center rect should be full height').to.deep.include({ y: 0, height: 1000 }); + }); + + it('should keep higher-priority overlap component when shedding lower-priority one is sufficient', () => { + // layout: 1000x1000, minHeightRatio=0.6 → minCenterHeight=600 + // compA (prioOrder=0, higher prio) size=300 → alone: center height=700 ≥ 600 → valid + // compB (prioOrder=1, lower prio) size=500 → together: center height=500 < 600 → shed B first + // After shedding B: only A, center height=700 ≥ 600 → valid + const settings = { center: { minHeightRatio: 0.6 } }; + dl.settings(settings); + + const compA = componentMock({ dock: 'top', size: 300, overlap: true, prioOrder: 0 }); + const compB = componentMock({ dock: 'top', size: 500, overlap: true, prioOrder: 1 }); + const center = componentMock(); + + const { visible, hidden } = dl.layout(rect, [compA, compB, center]); + + expect(hidden).to.include(compB); + expect(visible).to.include(compA); + expect(visible).to.include(center); + expect(center.rect, 'center rect should be reduced by compA only').to.deep.include({ y: 300, height: 700 }); + }); + + it('should clamp center rect dimensions to non-negative when overlap exceeds layout size', () => { + // overlap top size=1200 on a 1000px tall layout — would yield negative height without guard + const compA = componentMock({ dock: 'top', size: 1200, overlap: true, prioOrder: 0 }); + const center = componentMock(); + + const { hidden } = dl.layout(rect, [compA, center]); + + // compA is hidden because its constraint is too large to satisfy minHeightRatio + expect(hidden).to.include(compA); + // center dimensions must never be negative + expect(center.rect.width, 'center width must be non-negative').to.be.at.least(0); + expect(center.rect.height, 'center height must be non-negative').to.be.at.least(0); + }); + + it('should support chained @ references through overlap components', () => { + // A: normal top 30px → placed at y=20 (inner), center top at y=50 + // B: normal top 20px → stacks above A, placed at y=0 + // C: @A → same rect as A (overlaps A) + // D: @B → same rect as B (overlaps B) + // E: @A → same rect as A + // F: @B → same rect as B + // G: @C → same rect as C (= A) + // H: @D → same rect as D (= B) + const compA = componentMock({ dock: 'top', size: 30, key: 'A' }); + const compB = componentMock({ dock: 'top', size: 20, key: 'B' }); + const compC = componentMock({ dock: '@A', key: 'C' }); + const compD = componentMock({ dock: '@B', key: 'D' }); + const compE = componentMock({ dock: '@A' }); + const compF = componentMock({ dock: '@B' }); + const compG = componentMock({ dock: '@C' }); + const compH = componentMock({ dock: '@D' }); + const center = componentMock(); + + dl.layout(rect, [compA, compB, compC, compD, compE, compF, compG, compH, center]); + + // center reduced by 30+20=50 + expect(center.rect, 'center rect incorrect').to.deep.include({ x: 0, y: 50, width: 1000, height: 950 }); + + // A: innermost top, y = 50-30 = 20 + expect(compA.rect, 'compA rect incorrect').to.deep.include({ x: 0, y: 20, width: 1000, height: 30 }); + // B: stacks above A, y = 20-20 = 0 + expect(compB.rect, 'compB rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 20 }); + + // C overlaps A (same rect) + expect(compC.rect, 'compC (@A) rect incorrect').to.deep.include({ x: 0, y: 20, width: 1000, height: 30 }); + // D overlaps B (same rect) + expect(compD.rect, 'compD (@B) rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 20 }); + + // E also overlaps A + expect(compE.rect, 'compE (@A) rect incorrect').to.deep.include({ x: 0, y: 20, width: 1000, height: 30 }); + // F also overlaps B + expect(compF.rect, 'compF (@B) rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 20 }); + + // G chains through C → same rect as A + expect(compG.rect, 'compG (@C) rect incorrect').to.deep.include({ x: 0, y: 20, width: 1000, height: 30 }); + // H chains through D → same rect as B + expect(compH.rect, 'compH (@D) rect incorrect').to.deep.include({ x: 0, y: 0, width: 1000, height: 20 }); + }); + }); + describe('Settings', () => { let settings; let container; diff --git a/packages/picasso.js/src/core/layout/dock/config.js b/packages/picasso.js/src/core/layout/dock/config.js index eae8366c2..c1aacf741 100644 --- a/packages/picasso.js/src/core/layout/dock/config.js +++ b/packages/picasso.js/src/core/layout/dock/config.js @@ -21,6 +21,7 @@ function create(settings = {}, callbackContext = {}) { preferredSize = 0, minimumLayoutMode, show = true, + overlap = false, } = settings; // avoid empty string dock @@ -142,6 +143,21 @@ function create(settings = {}, callbackContext = {}) { } return typeof show === 'function' ? show(callbackContext) : show; }, + + /** + * Set whether this component overlaps other components at the same dock position. + * When true, the component is positioned at the same edge as non-overlapping components + * at that dock position and does not affect the layout space of other components. + * @param {boolean} [val=false] - Toggle overlap + * @returns {this|boolean} The current context or overlap + */ + overlap(val) { + if (typeof val !== 'undefined') { + overlap = val; + return this; + } + return typeof overlap === 'function' ? overlap(callbackContext) : overlap; + }, }; return dockConfig; diff --git a/packages/picasso.js/src/core/layout/dock/docker.js b/packages/picasso.js/src/core/layout/dock/docker.js index e918defda..78817b016 100644 --- a/packages/picasso.js/src/core/layout/dock/docker.js +++ b/packages/picasso.js/src/core/layout/dock/docker.js @@ -157,6 +157,11 @@ function reduceLayoutRect({ layoutRect, visible, hidden, settings }) { const c = sortedComponents[i]; cacheSize(c, reducedRect, layoutRect); + if (c.overlap) { + addEdgeBleed(edgeBleed, c); + continue; + } + if (!reduceSingleLayoutRect(layoutRect, reducedRect, edgeBleed, c, settings)) { hidden.push(sortedComponents.splice(i, 1)[0]); --i; @@ -169,6 +174,75 @@ function reduceLayoutRect({ layoutRect, visible, hidden, settings }) { visible.length = 0; visible.push(...filteredUnsortedComps); reduceEdgeBleed(layoutRect, reducedRect, edgeBleed); + + // Ensure center accommodates the size of overlap components. + // Process from lowest to highest priority: hide overlap components whose size + // constraints would make the center rect invalid, then apply the valid set. + const directionalOverlapComps = sortedComponents + .filter((c) => c.overlap && ['top', 'bottom', 'left', 'right'].includes(c.config.dock())) + .sort((a, b) => b.config.prioOrder() - a.config.prioOrder()); // lowest priority first + + const applyOverlapConstraints = (rect, comps) => { + const maxSizes = {}; + for (const c of comps) { + const d = c.config.dock(); + maxSizes[d] = Math.max(maxSizes[d] || 0, c.cachedSize); + } + const r = extend({}, rect); + if (maxSizes.top) { + const minY = layoutRect.y + maxSizes.top; + if (r.y < minY) { + r.height -= minY - r.y; + r.y = minY; + } + } + if (maxSizes.bottom) { + const maxBottom = layoutRect.y + layoutRect.height - maxSizes.bottom; + const currentBottom = r.y + r.height; + if (currentBottom > maxBottom) { + r.height -= currentBottom - maxBottom; + } + } + if (maxSizes.left) { + const minX = layoutRect.x + maxSizes.left; + if (r.x < minX) { + r.width -= minX - r.x; + r.x = minX; + } + } + if (maxSizes.right) { + const maxRight = layoutRect.x + layoutRect.width - maxSizes.right; + const currentRight = r.x + r.width; + if (currentRight > maxRight) { + r.width -= currentRight - maxRight; + } + } + return r; + }; + + let remainingOverlaps = directionalOverlapComps.slice(); + while (remainingOverlaps.length > 0) { + const candidate = applyOverlapConstraints(reducedRect, remainingOverlaps); + if (validateReduceRect(layoutRect, candidate, settings)) { + reducedRect.x = candidate.x; + reducedRect.y = candidate.y; + reducedRect.width = candidate.width; + reducedRect.height = candidate.height; + break; + } + // Hide the lowest-priority overlap component and retry with the remaining set + const toHide = remainingOverlaps.shift(); + const idx = visible.indexOf(toHide); + if (idx !== -1) { + visible.splice(idx, 1); + } + hidden.push(toHide); + } + + // Guard against negative dimensions in case overlap sizes exceed the layout rect + reducedRect.width = Math.max(0, reducedRect.width); + reducedRect.height = Math.max(0, reducedRect.height); + return reducedRect; } @@ -217,9 +291,18 @@ function boundingBox(rects) { } function positionComponents({ visible, layoutRect, reducedRect, containerRect, translation }) { + // vRect/hRect start from the center boundary (reducedRect) so all components align outward from there const vRect = createRect(reducedRect.x, reducedRect.y, reducedRect.width, reducedRect.height); const hRect = createRect(reducedRect.x, reducedRect.y, reducedRect.width, reducedRect.height); + // Anchors for overlap components: same center boundary + const overlapAnchors = { + top: reducedRect.y, + bottom: reducedRect.y + reducedRect.height, + left: reducedRect.x, + right: reducedRect.x + reducedRect.width, + }; + const referencedComponents = {}; const referenceArray = visible.slice(); const elementOrder = referenceArray.slice().sort((a, b) => a.config.displayOrder() - b.config.displayOrder()); @@ -251,41 +334,53 @@ function positionComponents({ visible, layoutRect, reducedRect, containerRect, t rect.width = vRect.width; outerRect.x = layoutRect.x; rect.x = vRect.x; - outerRect.y = rect.y = vRect.y - c.cachedSize; - - vRect.y -= c.cachedSize; - vRect.height += c.cachedSize; + if (c.overlap) { + outerRect.y = rect.y = overlapAnchors.top - c.cachedSize; + } else { + outerRect.y = rect.y = vRect.y - c.cachedSize; + vRect.y -= c.cachedSize; + vRect.height += c.cachedSize; + } break; case 'bottom': outerRect.x = layoutRect.x; rect.x = vRect.x; - outerRect.y = rect.y = vRect.y + vRect.height; outerRect.width = layoutRect.width; rect.width = vRect.width; outerRect.height = rect.height = c.cachedSize; - - vRect.height += c.cachedSize; + if (c.overlap) { + outerRect.y = rect.y = overlapAnchors.bottom; + } else { + outerRect.y = rect.y = vRect.y + vRect.height; + vRect.height += c.cachedSize; + } break; case 'left': - outerRect.x = rect.x = hRect.x - c.cachedSize; outerRect.y = layoutRect.y; rect.y = hRect.y; outerRect.width = rect.width = c.cachedSize; outerRect.height = layoutRect.height; rect.height = hRect.height; - - hRect.x -= c.cachedSize; - hRect.width += c.cachedSize; + if (c.overlap) { + outerRect.x = rect.x = overlapAnchors.left - c.cachedSize; + } else { + outerRect.x = rect.x = hRect.x - c.cachedSize; + hRect.x -= c.cachedSize; + hRect.width += c.cachedSize; + } break; case 'right': - outerRect.x = rect.x = hRect.x + hRect.width; outerRect.y = layoutRect.y; rect.y = hRect.y; outerRect.width = rect.width = c.cachedSize; outerRect.height = layoutRect.height; rect.height = hRect.height; - - hRect.width += c.cachedSize; + if (c.overlap) { + outerRect.x = rect.x = overlapAnchors.right; + } else { + outerRect.x = rect.x = hRect.x + hRect.width; + hRect.width += c.cachedSize; + } break; case 'center': outerRect.x = rect.x = reducedRect.x; @@ -366,12 +461,14 @@ function filterComponents(components, settings, rect) { const key = comp.key; const d = config.dock(); const referencedDocks = /@/.test(d) ? d.split(',').map((s) => s.replace(/^\s*@/, '')) : []; + const overlap = config.overlap ? config.overlap() : false; if (checkShowSettings(settings, config, rect)) { visible.push({ comp, key, config, referencedDocks, + overlap, }); } else { hidden.push({ @@ -379,6 +476,7 @@ function filterComponents(components, settings, rect) { key, config, referencedDocks, + overlap, }); } } diff --git a/packages/picasso.js/types/index.d.ts b/packages/picasso.js/types/index.d.ts index a55bc0e29..60864100b 100644 --- a/packages/picasso.js/types/index.d.ts +++ b/packages/picasso.js/types/index.d.ts @@ -1117,6 +1117,7 @@ declare namespace picassojs { prioOrder?: number; minimumLayoutMode?: string | Object; dock?: string; + overlap?: boolean; }; show?: boolean; scale?: string;