child::

自用多行省略与Tooltip组件

<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
 
const props = withDefaults(
  defineProps<{
    text?: string
    rows?: number
    tooltip?: boolean | string
    tooltipProps?: Record<string, unknown>
    reserve?: boolean
  }>(),
  {
    text: undefined,
    rows: 1,
    tooltip: undefined,
    tooltipProps: undefined,
    reserve: false,
  },
)
 
const textRef = ref<HTMLElement | null>(null)
const isActuallyTruncated = ref(false)
const innerTextContent = ref('')
const lineHeightPx = ref(0)
const reservedMinHeightPx = ref<number | null>(null)
 
const displayText = computed<string>(() => {
  return (props.text ?? '').toString()
})
 
const tooltipEnabled = computed<boolean>(() => {
  if (props.tooltip === false) return false
  // undefined or true or string means allowed when truncated
  return true
})
 
const tooltipContent = computed<string>(() => {
  if (typeof props.tooltip === 'string') return props.tooltip
  // Prefer the rendered slot/text innerText
  const content = (innerTextContent.value || '').trim()
  if (content) return content
  // Fallback to prop text when innerText not yet measured
  return displayText.value
})
 
let resizeObserver: ResizeObserver | null = null
 
function updateTruncationLater(): void {
  // Measure after DOM updates/styles applied
  nextTick(() => {
    const el = textRef.value
    if (!el) {
      isActuallyTruncated.value = false
      return
    }
    // Read live innerText if slot content used
    innerTextContent.value = el.innerText ?? ''
    const hasText = innerTextContent.value.trim().length > 0
    if (!hasText) {
      isActuallyTruncated.value = false
      // still measure min-height if reserve is enabled
      measureAndReserve(el)
      return
    }
    const heightOverflow = el.scrollHeight > el.clientHeight + 1
    const widthOverflow = el.scrollWidth > el.clientWidth + 1
    isActuallyTruncated.value = heightOverflow || widthOverflow
    measureAndReserve(el)
  })
}
 
function measureAndReserve(el: HTMLElement): void {
  if (!props.reserve || props.rows <= 1) {
    reservedMinHeightPx.value = null
    return
  }
  const cs = window.getComputedStyle(el)
  const lh = cs.lineHeight
  const fs = cs.fontSize
  let px = 0
  if (lh && lh !== 'normal') {
    px = parseFloat(lh)
  } else if (fs) {
    px = parseFloat(fs) * 1.5
  } else {
    px = 20
  }
  lineHeightPx.value = px
  reservedMinHeightPx.value = Math.max(0, Math.round(px * props.rows))
}
 
onMounted(() => {
  updateTruncationLater()
  if (window && 'ResizeObserver' in window) {
    resizeObserver = new ResizeObserver(() => updateTruncationLater())
    if (textRef.value) resizeObserver.observe(textRef.value)
  }
})
 
onUnmounted(() => {
  if (resizeObserver && textRef.value) {
    resizeObserver.unobserve(textRef.value)
  }
  resizeObserver = null
  window.removeEventListener('resize', updateTruncationLater)
})
 
watch(
  () => [props.text, props.rows, props.tooltip, props.reserve],
  () => updateTruncationLater(),
  { flush: 'post' },
)
 
const showTooltip = computed<boolean>(() => tooltipEnabled.value && isActuallyTruncated.value)
</script>
 
<template>
  <el-tooltip
    :disabled="!showTooltip"
    :content="tooltipContent"
    placement="top"
    v-bind="tooltipProps"
  >
    <div
      ref="textRef"
      class="ellipsis-paragraph__content"
      :class="{ 'is-single-line': rows === 1 }"
      :style="{
        '--line-clamp': String(rows),
        minHeight: reserve && reservedMinHeightPx != null ? `${reservedMinHeightPx}px` : undefined,
      }"
    >
      <slot>{{ text }}</slot>
    </div>
  </el-tooltip>
</template>
 
<style scoped>
.ellipsis-paragraph__content {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  /* multiline clamp */
  line-clamp: var(--line-clamp);
  -webkit-line-clamp: var(--line-clamp);
}
 
.ellipsis-paragraph__content.is-single-line {
  display: block;
  white-space: nowrap;
}
</style>
 
指向原始笔记的链接