mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-23 04:34:15 -04:00

/* */ comments get stripped out by scss in compressed format: https://sass-lang.com/documentation/syntax/comments/, so /*! */ must be used so that rtlcss can process the directive: https://rtlcss.com/learn/getting-started/why-rtlcss/#processing-directives adjustment to 1f88c83a96e16e1166da14c3f0c6f4a1906d14f5
180 lines
5.4 KiB
Vue
180 lines
5.4 KiB
Vue
<template>
|
|
<span
|
|
ref="reference"
|
|
@mouseenter="show"
|
|
@mouseleave="hide"
|
|
><slot></slot><span
|
|
v-if="visible && hasContent"
|
|
ref="floating"
|
|
class="popover"
|
|
:style="floatingStyles"
|
|
><span
|
|
ref="arrow"
|
|
class="popover-arrow bg-dark text-white"
|
|
:style="arrowStyles"
|
|
></span><div
|
|
ref="content"
|
|
class="overflow-auto bg-dark text-white px-2 py-1 rounded"
|
|
><slot name="content"></slot></div></span></span>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import type { PropType } from 'vue';
|
|
import { arrow, autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom';
|
|
import Vue from 'vue';
|
|
import type { CSSProperties } from 'vue/types/jsx';
|
|
import type { ComputePositionReturn, Placement } from '@floating-ui/dom';
|
|
|
|
const getDPR = (element: Element): number => {
|
|
if (typeof window === 'undefined') {
|
|
return 1;
|
|
}
|
|
const win = element.ownerDocument.defaultView || window;
|
|
return win.devicePixelRatio || 1;
|
|
};
|
|
|
|
const roundByDPR = (element: Element, value: number): number => {
|
|
const dpr = getDPR(element);
|
|
return Math.round(value * dpr) / dpr;
|
|
};
|
|
|
|
const remToPx = (valueInRem: number): number => {
|
|
return valueInRem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
};
|
|
|
|
interface Data {
|
|
visible: boolean;
|
|
position: ComputePositionReturn | null;
|
|
cleanup: (() => void) | null;
|
|
}
|
|
|
|
interface Refs {
|
|
reference: HTMLSpanElement | undefined;
|
|
floating: HTMLSpanElement | undefined;
|
|
content: HTMLSpanElement | undefined;
|
|
arrow: HTMLSpanElement | undefined;
|
|
}
|
|
|
|
export default Vue.extend({
|
|
props: {
|
|
placement: { default: 'bottom', type: String as PropType<Placement> },
|
|
resize: { type: Boolean },
|
|
},
|
|
data(): Data {
|
|
return {
|
|
visible: false,
|
|
position: null,
|
|
cleanup: null,
|
|
};
|
|
},
|
|
computed: {
|
|
$tRefs(): Refs {
|
|
return this.$refs as unknown as Refs;
|
|
},
|
|
hasContent(): boolean {
|
|
return !!this.$slots.content?.[0];
|
|
},
|
|
floatingStyles(): CSSProperties | undefined {
|
|
if (!this.position || !this.$tRefs.floating) {
|
|
return;
|
|
}
|
|
const xVal = roundByDPR(this.$tRefs.floating, this.position.x);
|
|
const yVal = roundByDPR(this.$tRefs.floating, this.position.y);
|
|
return {
|
|
transform: `translate(${xVal}px, ${yVal}px)`,
|
|
...getDPR(this.$tRefs.floating) >= 1.5 && {
|
|
willChange: 'transform',
|
|
},
|
|
};
|
|
},
|
|
arrowStyles(): CSSProperties | undefined {
|
|
if (!this.position) {
|
|
return;
|
|
}
|
|
|
|
const arrowData = this.position.middlewareData.arrow;
|
|
|
|
const staticSite = {
|
|
top: 'bottom',
|
|
right: 'left',
|
|
bottom: 'top',
|
|
left: 'right',
|
|
}[this.position.placement.split('-')[0]]!;
|
|
|
|
return {
|
|
left: typeof arrowData?.x === 'number' ? `${arrowData.x}px` : '',
|
|
top: typeof arrowData?.y === 'number' ? `${arrowData.y}px` : '',
|
|
[staticSite]: '-0.25rem',
|
|
};
|
|
},
|
|
},
|
|
beforeDestroy() {
|
|
this.hide();
|
|
},
|
|
methods: {
|
|
async show() {
|
|
this.visible = true;
|
|
// floating element will be rendered on next tick
|
|
await this.$nextTick();
|
|
if (!this.$tRefs.reference || !this.$tRefs.floating) {
|
|
return;
|
|
}
|
|
// remove title from reference element to prevent a double tooltip
|
|
this.$tRefs.reference.removeAttribute('title');
|
|
this.cleanup = autoUpdate(this.$tRefs.reference, this.$tRefs.floating, () => this.update());
|
|
},
|
|
hide() {
|
|
this.visible = false;
|
|
if (this.cleanup) {
|
|
this.cleanup();
|
|
this.cleanup = null;
|
|
}
|
|
},
|
|
async update() {
|
|
if (!this.$tRefs.reference || !this.$tRefs.floating || !this.$tRefs.arrow) {
|
|
return;
|
|
}
|
|
const middleware = [offset(remToPx(0.25)), flip(), shift()];
|
|
if (this.resize) {
|
|
middleware.push(size({
|
|
apply: ({ availableWidth, availableHeight }) => {
|
|
if (!this.$tRefs.content) {
|
|
return;
|
|
}
|
|
Object.assign(this.$tRefs.content.style, {
|
|
maxWidth: `${availableWidth}px`,
|
|
maxHeight: `${availableHeight}px`,
|
|
});
|
|
},
|
|
}));
|
|
}
|
|
middleware.push(arrow({ element: this.$tRefs.arrow }));
|
|
|
|
this.position = await computePosition(this.$tRefs.reference, this.$tRefs.floating, {
|
|
middleware,
|
|
placement: this.placement as Placement,
|
|
});
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.popover {
|
|
position: absolute;
|
|
top: 0;
|
|
/*! rtl:ignore: popovers are always positioned absolutely from the upper-left corner */
|
|
left: 0;
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
font-size: .85rem;
|
|
z-index: 999;
|
|
}
|
|
.popover-arrow {
|
|
position: absolute;
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
transform: rotate(45deg);
|
|
}
|
|
</style>
|