@pierre/diffs
Version:
191 lines (190 loc) • 7.82 kB
JavaScript
import { addEventListener } from "./utils.js";
//#region src/editor/popover.ts
const POPOVER_BOUNDARY_LINES = 3;
const POPOVER_FLIP_HYSTERESIS_PX = 4;
/**
* Shared placement logic for the editor's overlay popovers: each anchors below
* or above a document position, flipping to the opposite side only when the
* preferred side would be clipped by the visible scrollport.
*/
var PopoverManager = class {
#fileContainer;
#codeElement;
#scrollContainer;
#scrollContainerSource;
#cachedCodeRect;
#cachedScrollContainerRect;
#cachedCodeScrollLeft = 0;
#cachedCodeScrollTop = 0;
#cachedCodeClientWidth = 0;
#viewportRectsDirty = true;
#viewportRectListenersDisposes;
#placements = /* @__PURE__ */ new Map();
#hasActivePopover;
#updateActivePopover;
constructor(options) {
this.#hasActivePopover = options.hasActivePopover;
this.#updateActivePopover = options.updateActivePopover;
}
setViewportElements(fileContainer, codeElement) {
const viewportElementChanged = this.#fileContainer !== fileContainer || this.#codeElement !== codeElement;
this.#fileContainer = fileContainer;
if (!viewportElementChanged) return;
this.#codeElement = codeElement;
this.#viewportRectsDirty = true;
this.#viewportRectListenersDisposes?.forEach((dispose) => dispose());
this.#viewportRectListenersDisposes = void 0;
}
cleanUp() {
this.#codeElement = void 0;
this.#fileContainer = void 0;
this.#scrollContainer = void 0;
this.#scrollContainerSource = void 0;
this.#cachedCodeRect = void 0;
this.#cachedScrollContainerRect = void 0;
this.#cachedCodeScrollLeft = 0;
this.#cachedCodeScrollTop = 0;
this.#cachedCodeClientWidth = 0;
this.#viewportRectsDirty = true;
this.#viewportRectListenersDisposes?.forEach((dispose) => dispose());
this.#viewportRectListenersDisposes = void 0;
this.#placements.clear();
}
resetPlacement(placementKey = "default") {
this.#placements.delete(placementKey);
}
setPlacement(placement, placementKey = "default") {
this.#placements.set(placementKey, placement);
}
choosePlacement(input) {
const { preferred, fallback, viewport, popoverHeight, atDocumentEdge, placementKey = "default" } = input;
const previousPlacement = this.#placements.get(placementKey);
let placement;
if (viewport !== void 0 && popoverHeight > 0) {
const fits = (bounds, margin = 0) => bounds.top >= viewport.top + margin && bounds.bottom <= viewport.bottom - margin;
if (previousPlacement === "fallback" && fits(fallback) && !fits(preferred, 4)) placement = "fallback";
else if (!fits(preferred) && fits(fallback)) placement = "fallback";
else placement = "preferred";
} else placement = atDocumentEdge ? "fallback" : "preferred";
this.#placements.set(placementKey, placement);
return placement;
}
/**
* Returns the bounds of the popover in overlay coordinate space.
*/
getPlacementBounds() {
const codeRect = this.#getCodeRect();
if (codeRect === void 0) return;
const scrollContainerRect = this.#getScrollContainerRect();
let topScreen;
let bottomScreen;
if (scrollContainerRect !== void 0) {
topScreen = scrollContainerRect.top;
bottomScreen = scrollContainerRect.bottom;
} else {
topScreen = 0;
bottomScreen = window.innerHeight;
}
topScreen = Math.max(topScreen, codeRect.top);
bottomScreen = Math.min(bottomScreen, codeRect.bottom);
if (bottomScreen <= topScreen) return;
const codeScrollLeft = this.#cachedCodeScrollLeft;
const codeScrollTop = this.#cachedCodeScrollTop;
const codeViewportWidth = this.#cachedCodeClientWidth > 0 ? this.#cachedCodeClientWidth : codeRect.width;
const leftScreen = scrollContainerRect !== void 0 ? scrollContainerRect.left : 0;
const rightScreen = scrollContainerRect !== void 0 ? scrollContainerRect.right : window.innerWidth;
const codeViewportLeft = codeScrollLeft;
const codeViewportRight = codeScrollLeft + codeViewportWidth;
const visibleLeft = Math.max(codeViewportLeft, leftScreen - codeRect.left + codeScrollLeft);
const visibleRight = Math.min(codeViewportRight, rightScreen - codeRect.left + codeScrollLeft);
return {
top: topScreen - codeRect.top + codeScrollTop,
bottom: bottomScreen - codeRect.top + codeScrollTop,
left: visibleLeft,
right: Math.max(visibleLeft, visibleRight)
};
}
#getScrollContainer() {
const fileContainer = this.#fileContainer;
if (fileContainer === void 0) return;
if (this.#scrollContainerSource === fileContainer) {
if (this.#scrollContainer === void 0 || this.#scrollContainer.isConnected) return this.#scrollContainer;
}
let element = fileContainer.parentElement;
while (element !== null) {
const overflowY = getComputedStyle(element).overflowY;
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") {
this.#scrollContainer = element;
this.#scrollContainerSource = fileContainer;
return element;
}
element = element.parentElement;
}
this.#scrollContainer = void 0;
this.#scrollContainerSource = fileContainer;
}
#refreshViewportRectsIfNeeded() {
if (!this.#viewportRectsDirty) return;
this.#ensureViewportRectListeners();
const codeElement = this.#codeElement;
this.#cachedCodeRect = codeElement?.getBoundingClientRect();
this.#cachedCodeScrollLeft = codeElement?.scrollLeft ?? 0;
this.#cachedCodeScrollTop = codeElement?.scrollTop ?? 0;
this.#cachedCodeClientWidth = codeElement?.clientWidth ?? 0;
this.#cachedScrollContainerRect = this.#getScrollContainer()?.getBoundingClientRect();
this.#viewportRectsDirty = false;
}
#getCodeRect() {
this.#refreshViewportRectsIfNeeded();
return this.#cachedCodeRect;
}
#getScrollContainerRect() {
this.#refreshViewportRectsIfNeeded();
return this.#cachedScrollContainerRect;
}
#ensureViewportRectListeners() {
if (this.#viewportRectListenersDisposes !== void 0) return;
let repositionRafId;
const markDirty = () => {
this.#viewportRectsDirty = true;
if (!this.#hasActivePopover() || repositionRafId !== void 0) return;
repositionRafId = requestAnimationFrame(() => {
repositionRafId = void 0;
this.#updateActivePopover();
});
};
const scroller = this.#getScrollContainer();
const codeElement = this.#codeElement;
this.#viewportRectListenersDisposes = [
scroller !== void 0 ? addEventListener(scroller, "scroll", markDirty, { passive: true }) : addEventListener(window, "scroll", markDirty, { passive: true }),
...codeElement !== void 0 ? [addEventListener(codeElement, "scroll", markDirty, { passive: true })] : [],
addEventListener(window, "resize", markDirty, { passive: true }),
() => {
if (repositionRafId !== void 0) {
cancelAnimationFrame(repositionRafId);
repositionRafId = void 0;
}
}
];
}
};
function setPopoverPositionStyles(popover, { gutterWidth, placeAbove, viewport, x, y }) {
popover.style.setProperty("--gutter-width", gutterWidth + "px");
popover.style.setProperty("--popover-x", x + "px");
popover.style.setProperty("--popover-y", y + "px");
popover.style.setProperty("--popover-y-shift", placeAbove ? "-100%" : "0px");
if (viewport === void 0) {
popover.style.removeProperty("--popover-viewport-left");
popover.style.removeProperty("--popover-viewport-right");
popover.style.removeProperty("--popover-viewport-top");
popover.style.removeProperty("--popover-viewport-bottom");
return;
}
popover.style.setProperty("--popover-viewport-left", viewport.left + "px");
popover.style.setProperty("--popover-viewport-right", viewport.right + "px");
popover.style.setProperty("--popover-viewport-top", viewport.top + "px");
popover.style.setProperty("--popover-viewport-bottom", viewport.bottom + "px");
}
//#endregion
export { POPOVER_BOUNDARY_LINES, POPOVER_FLIP_HYSTERESIS_PX, PopoverManager, setPopoverPositionStyles };
//# sourceMappingURL=popover.js.map