Path: blob/main/src/vs/base/browser/overlayLayoutElement.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getComputedStyle, setParentFlowTo } from './dom.js';6import { IDisposable } from '../common/lifecycle.js';7import { generateUuid } from '../common/uuid.js';89/**10* If the element already has an `anchor-name` style, return it.11* Otherwise generate a fresh `--overlay-anchor-<uuid>` name, assign it, and return it.12*/13function getOrCreateAnchorName(element: HTMLElement): string {14const existing = element.style.getPropertyValue('anchor-name');15if (existing) {16return existing;17}18const name = `--overlay-anchor-${generateUuid()}`;19element.style.setProperty('anchor-name', name);20return name;21}2223/**24* Positions an element over another element anywhere in the dom using absolute positioning.25*26* This is useful for cases where a dom node cannot be re-parented without losing its state, such as a iframe.27*28* Call {@link setAnchorElement} each time the layout is recalculated. When the29* same anchor element is passed again the call is a no-op (the browser keeps them in sync).30*/31export class OverlayLayoutElement implements IDisposable {3233private _currentAnchor?: { readonly element: HTMLElement; readonly name: string };34private _clippingAnchor?: { readonly element: HTMLElement; readonly name: string };3536/**37* The root element that contains the overlay element.38*39* This also provides clipping support for the overlay element. Clipping is needed when the anchor is40* scrollable and may scroll and be hidden by overflow from its parent container.41*/42private readonly _root: HTMLElement;4344constructor() {45this.content = document.createElement('div');46this.content.style.position = 'absolute';47this.content.style.overflow = 'hidden';4849this._root = document.createElement('div');50this._root.appendChild(this.content);5152this.reapplyLayoutStyles();53}5455public reapplyLayoutStyles(): void {56this.content.style.position = 'fixed';57this.content.style.top = 'anchor(top)';58this.content.style.left = 'anchor(left)';59this.content.style.width = 'anchor-size(width)';60this.content.style.height = 'anchor-size(height)';61this.content.style.pointerEvents = 'auto';6263this._root.style.position = 'absolute';64this._root.style.pointerEvents = 'none';65}6667public dispose(): void {68this.root.remove();69}7071/**72* The outermost element. This is what should be appended to the actual dom hierarchy, typically near to73* the document root node.74*/75public get root(): HTMLElement {76return this._root;77}7879/**80* The actual element that is positioned over the anchor.81*/82public readonly content: HTMLElement;8384/**85* Position the content over `anchorElement`.86*87* This only needs to be called when the anchor element or the clipping container changes.88*/89public setAnchorElement(90anchorElement: HTMLElement,91options?: {92readonly clippingContainer?: HTMLElement;93},94): void {95if (this._currentAnchor?.element !== anchorElement) {96const name = getOrCreateAnchorName(anchorElement);97this.content.style.setProperty('position-anchor', name);98setParentFlowTo(this.content, anchorElement);99this._currentAnchor = { element: anchorElement, name };100}101102this._updateClipping(options?.clippingContainer);103this._updateZIndex(anchorElement);104}105106/**107* Walk up from the anchor element to find the nearest ancestor with an explicit108* z-index and place the overlay one level above it. This ensures the overlay sits109* above modal layers or other stacking contexts.110*/111private _updateZIndex(anchorElement: HTMLElement): void {112let zIndex = '';113for (let el: HTMLElement | null = anchorElement; el; el = el.parentElement) {114const computed = getComputedStyle(el).zIndex;115if (computed && computed !== 'auto') {116zIndex = String(Number(computed) + 1);117break;118}119}120this.content.style.zIndex = zIndex;121}122123private _updateClipping(clippingContainer: HTMLElement | undefined): void {124if (this._clippingAnchor?.element === clippingContainer) {125return;126}127128this._root.style.removeProperty('position-anchor');129130const ws = this._root.style;131if (clippingContainer) {132const name = getOrCreateAnchorName(clippingContainer);133ws.clipPath = 'content-box';134ws.setProperty('position-anchor', name);135ws.setProperty('top', 'anchor(top)');136ws.setProperty('left', 'anchor(left)');137ws.setProperty('width', `anchor-size(width)`);138ws.setProperty('height', `anchor-size(height)`);139this._clippingAnchor = { element: clippingContainer, name };140} else {141ws.clipPath = '';142ws.setProperty('top', '0');143ws.setProperty('left', '0');144ws.setProperty('right', '0');145ws.setProperty('bottom', '0');146this._clippingAnchor = undefined;147}148}149}150151152