Skip to main content
Glama
Popover.vue5.98 kB
<template> <Teleport v-if="isOpen" to="body"> <div ref="internalRef" :style="computedStyle" :class=" clsx( 'absolute ml-sm', onTopOfEverything ? 'z-100' : 'z-50', isRepositioning && 'invisible', ) " > <slot /> </div> </Teleport> </template> <script setup lang="ts"> import { ref, onBeforeUnmount, PropType, computed, onMounted, onUnmounted, } from "vue"; import * as _ from "lodash-es"; import clsx from "clsx"; import { windowListenerManager } from "@si/vue-lib"; const props = defineProps({ anchorTo: { type: Object }, // anchorDirectionX determines the direction the Popover pops out from its anchoring element, left or right anchorDirectionX: { type: String as PropType<"left" | "right">, default: "right", }, // anchorPositionY determines how the Popover aligns to its anchor - aligning the top edges, bottom edges, or middles anchorAlignY: { type: String as PropType<"top" | "middle" | "bottom">, default: "middle", }, // override the default positioning logic and give the popover a fixed position fixedPosition: { type: Object as PropType<{ x: number; y: number }> }, // override the default position logic and pop out below the anchorTo element popDown: { type: Boolean }, // go on top of all elements, including the navbar and statusbar onTopOfEverything: { type: Boolean }, // act like a Modal that cannot be closed noExit: { type: Boolean }, }); const internalRef = ref<HTMLElement>(); const isOpen = ref(false); const isRepositioning = ref(false); const anchorEl = ref<HTMLElement>(); const anchorPos = ref<{ x: number; y: number }>(); function onWindowMousedown(e: MouseEvent) { if ( (e.target instanceof Element && internalRef.value?.contains(e.target)) || props.noExit ) { return; // Don't close on click inside popover or if noExit is set } close(); } function onKeyboardEvent(e: KeyboardEvent) { if (e.key === "Escape") { e.stopPropagation(); if (props.noExit) return; close(); } } function nextFrame(cb: () => void) { requestAnimationFrame(() => requestAnimationFrame(cb)); } function open(e?: MouseEvent, anchorToMouse?: boolean) { const clickTargetIsElement = e?.target instanceof HTMLElement; if (props.anchorTo) { // can anchor to a specific element via props if (props.anchorTo instanceof HTMLElement) { anchorEl.value = props.anchorTo; } else { anchorEl.value = props.anchorTo.$el; } } else if (e && (anchorToMouse || !clickTargetIsElement)) { // or can anchor to mouse position if anchorToMouse is true (or event has not target) anchorEl.value = undefined; anchorPos.value = { x: e?.clientX, y: e.clientY }; } else if (clickTargetIsElement) { // otherwise anchor to click event target anchorEl.value = e.target; } else { // shouldn't happen...? anchorEl.value = undefined; } isRepositioning.value = true; isOpen.value = true; nextFrame(finishOpening); } function openAt(pos: { x: number; y: number }) { anchorPos.value = pos; isRepositioning.value = true; isOpen.value = true; nextFrame(finishOpening); } function finishOpening() { startListening(); readjustPosition(); } function startListening() { windowListenerManager.addEventListener("keydown", onKeyboardEvent, 10); windowListenerManager.addEventListener("mousedown", onWindowMousedown, 10); } function removeListeners() { windowListenerManager.removeEventListener("keydown", onKeyboardEvent); windowListenerManager.removeEventListener("mousedown", onWindowMousedown); } function readjustPosition() { if (!internalRef.value) return; isRepositioning.value = false; if (props.fixedPosition) { anchorPos.value = { x: props.fixedPosition.x, y: props.fixedPosition.y }; return; } let anchorRect; if (anchorEl.value) { anchorRect = anchorEl.value.getBoundingClientRect(); } else if (anchorPos.value) { anchorRect = new DOMRect(anchorPos.value.x, anchorPos.value.y); } else { throw new Error("Menu must be anchored to an element or mouse position"); } const popoverRect = internalRef.value.getBoundingClientRect(); anchorPos.value = { x: 0, y: 0 }; if (props.popDown) { anchorPos.value.x = anchorRect.left - internalRef.value.clientWidth / 2; anchorPos.value.y = anchorRect.bottom + 8; return; } const windowWidth = document.documentElement.clientWidth; if (props.anchorDirectionX === "left") { anchorPos.value.x = windowWidth - anchorRect.left; } else { anchorPos.value.x = anchorRect.right; } if (props.anchorAlignY === "bottom") { anchorPos.value.y = anchorRect.bottom - popoverRect.height; } else if (props.anchorAlignY === "top") { anchorPos.value.y = anchorRect.top; } else { anchorPos.value.y = anchorRect.top + anchorRect.height / 2 - popoverRect.height / 2; } } const computedStyle = computed(() => { if (anchorPos.value) { const style: Record<string, string> = {}; if (props.anchorDirectionX === "left") { style.right = `${anchorPos.value.x}px`; } else { style.left = `${anchorPos.value.x}px`; } style.top = `${anchorPos.value.y}px`; return style; } else { return { display: "hidden" }; } }); function close() { isOpen.value = false; anchorPos.value = undefined; removeListeners(); } // If the browser window is resized, close the Popover // TODO(Wendy) - Close the Popover if the element it is anchored to is scrolled away from its starting position by more than a certain amount const closeOnResize = _.debounce(close, 1000, { leading: true, trailing: false, }); onMounted(() => { window.addEventListener("resize", closeOnResize); }); onUnmounted(() => { window.removeEventListener("resize", closeOnResize); }); onBeforeUnmount(() => { removeListeners(); }); defineExpose({ open, openAt, close, isOpen }); </script>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server