mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
121 lines
3.5 KiB
Vue
121 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
import { onClickOutside, useEventListener } from '@vueuse/core'
|
|
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
|
|
|
const props = defineProps<{
|
|
label: string
|
|
total: number
|
|
}>()
|
|
|
|
const active = ref(false)
|
|
const list = computed(() => Array.from({ length: props.total }, (_, i) => i))
|
|
const position = ref({ x: 0, y: 0 })
|
|
|
|
const popover = useTemplateRef<HTMLDivElement>('popover')
|
|
const button = useTemplateRef<HTMLButtonElement>('button')
|
|
onClickOutside(popover, () => (active.value = false), {
|
|
ignore: [button],
|
|
})
|
|
|
|
function updatePosition() {
|
|
if (__VUEPRESS_SSR__)
|
|
return
|
|
if (!active.value || !popover.value || !button.value)
|
|
return
|
|
const { x: _x, y: _y, width: w, height: h } = button.value.getBoundingClientRect()
|
|
const x = _x + w / 2
|
|
const y = _y + h
|
|
|
|
const { width, height } = popover.value.getBoundingClientRect()
|
|
const { clientWidth, clientHeight } = document.documentElement
|
|
position.value.x = x + width + 16 > clientWidth ? clientWidth - x - width - 16 : 0
|
|
position.value.y = y + height + 16 > clientHeight ? clientHeight - y - height - 16 : 0
|
|
}
|
|
|
|
watch(active, () => nextTick(updatePosition))
|
|
useEventListener('resize', updatePosition)
|
|
</script>
|
|
|
|
<template>
|
|
<span class="vp-annotation" :class="{ active, [label]: true }" :aria-label="label">
|
|
<span ref="button" class="vpi-annotation" @click="active = !active" />
|
|
<Transition name="fade">
|
|
<div
|
|
v-show="active" ref="popover"
|
|
class="annotations-popover" :class="{ list: list.length > 1 }"
|
|
:style="{ '--vp-annotation-x': `${position.x}px`, '--vp-annotation-y': `${position.y}px` }"
|
|
>
|
|
<div v-for="i in list" :key="label + i" class="annotation">
|
|
<slot :name="`item-${i}`" />
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</span>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.vp-annotation {
|
|
position: relative;
|
|
}
|
|
|
|
.vpi-annotation {
|
|
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10a10 10 0 0 1-4.262-.951l-4.537.93a1 1 0 0 1-1.18-1.18l.93-4.537A10 10 0 0 1 2 12m10-4a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2H9a1 1 0 1 1 0-2h2V9a1 1 0 0 1 1-1' clip-rule='evenodd'/%3E%3C/svg%3E");
|
|
|
|
position: relative;
|
|
top: -2px;
|
|
z-index: 2;
|
|
width: 1.2em;
|
|
height: 1.2em;
|
|
color: currentcolor;
|
|
cursor: pointer;
|
|
opacity: 0.5;
|
|
transition: color var(--vp-t-color), opacity var(--vp-t-color), transform var(--vp-t-color);
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
.vp-annotation.active {
|
|
z-index: 10;
|
|
}
|
|
|
|
.vp-annotation:where(:hover, .active) .vpi-annotation {
|
|
color: var(--vp-c-brand-2);
|
|
opacity: 1;
|
|
}
|
|
|
|
.vp-annotation.active .vpi-annotation {
|
|
transform: rotate(-45deg);
|
|
}
|
|
|
|
.annotations-popover {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: max-content;
|
|
max-width: min(calc(100vw - 32px), 360px);
|
|
max-height: 360px;
|
|
padding: 8px 12px;
|
|
overflow-y: auto;
|
|
font-size: 14px;
|
|
background-color: var(--vp-c-bg);
|
|
border: solid 1px var(--vp-c-divider);
|
|
border-radius: 4px;
|
|
box-shadow: var(--vp-shadow-2);
|
|
transform: translateX(var(--vp-annotation-x, 0)) translateY(var(--vp-annotation-y, 0));
|
|
}
|
|
|
|
.annotations-popover.list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding: 12px;
|
|
background-color: var(--vp-c-bg-soft);
|
|
}
|
|
|
|
.annotations-popover.list .annotation {
|
|
padding: 4px 12px;
|
|
background-color: var(--vp-c-bg);
|
|
border-radius: 4px;
|
|
box-shadow: var(--vp-shadow-1);
|
|
}
|
|
</style>
|