facilmap/frontend/src/lib/components/ui/popover.vue

140 wiersze
4.1 KiB
Vue
Czysty Zwykły widok Historia

2023-10-11 12:18:25 +00:00
<script lang="ts">
2023-11-11 07:01:40 +00:00
import { computed, ref, toRef, watch, watchEffect } from "vue";
import Popover from "bootstrap/js/dist/popover";
import Tooltip from "bootstrap/js/dist/tooltip";
2023-10-23 21:05:19 +00:00
import { useResizeObserver } from "../../utils/vue";
import { useDomEventListener } from "../../utils/utils";
2023-10-11 12:18:25 +00:00
/**
* Like Bootstrap Popover, but uses an existing popover element rather than creating a new one. This way, the popover
* content can be made reactive.
*/
export class CustomPopover extends Popover {
declare _popper: any;
contentEl: Element;
constructor(element: string | Element, options: Partial<Popover.Options> & { content: Element }) {
super(element, {
...options,
content: ' '
});
this.contentEl = options.content;
}
_createTipElement(): Element {
// Return content element rather than creating a new one
return this.contentEl;
}
_disposePopper(): void {
// Do not remove content element here
if (this._popper) {
this._popper.destroy()
this._popper = null
}
}
}
</script>
<script lang="ts" setup>
2023-10-30 00:14:54 +00:00
const props = withDefaults(defineProps<{
2023-10-11 12:18:25 +00:00
element: HTMLElement | undefined;
show: boolean;
hideOnOutsideClick?: boolean;
2023-10-23 21:05:19 +00:00
/** If true, the width of the popover will be fixed to the width of the element. */
enforceElementWidth?: boolean;
2023-11-11 07:01:40 +00:00
placement?: Tooltip.PopoverPlacement;
2023-10-30 00:14:54 +00:00
}>(), {
placement: "bottom"
});
2023-10-11 12:18:25 +00:00
const emit = defineEmits<{
2023-11-01 18:45:16 +00:00
"update:show": [show: boolean];
2023-11-11 07:01:40 +00:00
shown: [];
hide: [];
hidden: [];
2023-10-11 12:18:25 +00:00
}>();
2023-11-11 07:01:40 +00:00
const popoverContent = ref<HTMLElement>();
2023-10-11 12:18:25 +00:00
const renderPopover = ref(false);
2023-11-11 07:01:40 +00:00
watchEffect((onCleanup) => {
onCleanup(() => {}); // TODO: Delete me https://github.com/vuejs/core/issues/5151#issuecomment-1515613484
if (props.element && popoverContent.value) {
const popover = new CustomPopover(props.element, {
2023-10-30 00:14:54 +00:00
placement: props.placement,
2023-11-11 07:01:40 +00:00
content: popoverContent.value,
2023-11-07 01:19:20 +00:00
trigger: 'manual',
2023-11-09 13:52:02 +00:00
popperConfig: (defaultConfig) => ({
...defaultConfig,
strategy: "fixed"
})
2023-11-11 07:01:40 +00:00
});
popover.show();
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
onCleanup(() => {
popover.dispose();
});
2023-10-11 12:18:25 +00:00
}
2023-11-11 07:01:40 +00:00
});
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
useDomEventListener(toRef(() => props.element), "shown.bs.popover", () => {
emit("shown");
});
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
useDomEventListener(toRef(() => props.element), "hide.bs.popover", () => {
emit("hide");
});
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
useDomEventListener(toRef(() => props.element), "hidden.bs.popover", () => {
renderPopover.value = false;
emit("hidden");
});
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
watch(() => props.show, (show) => {
renderPopover.value = show;
});
2023-10-30 00:14:54 +00:00
2023-11-11 07:01:40 +00:00
useDomEventListener(() => props.element, "focusout", (e: Event) => {
const event = e as FocusEvent;
// relatedTarget == null: target is out of viewport (ignore to allow focussing dev tools)
if (event.relatedTarget && !popoverContent.value?.contains(event.relatedTarget as Node) && !props.element?.contains(event.relatedTarget as Node)) {
emit("update:show", false);
2023-10-11 12:18:25 +00:00
}
2023-11-11 07:01:40 +00:00
});
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
function handlePopoverFocusOut(event: FocusEvent) {
// relatedTarget == null: target is out of viewport (ignore to allow focussing dev tools)
if (event.relatedTarget && !popoverContent.value?.contains(event.relatedTarget as Node) && !props.element?.contains(event.relatedTarget as Node)) {
emit("update:show", false);
2023-10-11 12:18:25 +00:00
}
2023-11-11 07:01:40 +00:00
}
2023-10-11 12:18:25 +00:00
2023-11-11 07:01:40 +00:00
useDomEventListener(document, "click", (e: Event) => {
if (props.show && props.hideOnOutsideClick && e.target instanceof Node && !props.element?.contains(e.target) && !popoverContent.value?.contains(e.target)) {
emit("update:show", false);
2023-10-11 12:18:25 +00:00
}
2023-11-11 07:01:40 +00:00
}, { capture: true });
2023-10-23 21:05:19 +00:00
const elementSize = useResizeObserver(computed(() => props.enforceElementWidth ? props.element : undefined));
2023-10-11 12:18:25 +00:00
</script>
<template>
2023-10-23 21:05:19 +00:00
<div
v-if="renderPopover"
2023-11-11 07:01:40 +00:00
class="popover fm-popover fade bs-popover-auto"
2023-10-23 21:05:19 +00:00
ref="popoverContent"
:style="props.enforceElementWidth && elementSize ? { maxWidth: 'none', width: `${elementSize.contentRect.width}px` } : undefined"
2023-11-11 07:01:40 +00:00
@focusout="handlePopoverFocusOut"
:tabindex="-1 /* Allow focusing by click (for focusout event relatedTarget), do not allow focusing by tab */"
2023-10-23 21:05:19 +00:00
>
2023-10-11 12:18:25 +00:00
<div class="popover-arrow"></div>
2023-10-23 21:05:19 +00:00
<h3 v-if="$slots.header" class="popover-header">
2023-10-11 12:18:25 +00:00
<slot name="header"></slot>
</h3>
<div class="popover-body">
<slot></slot>
</div>
</div>
</template>