欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用JavaScript優(yōu)雅實現(xiàn)文本展開收起功能

 更新時間:2024年04月26日 11:25:33   作者:迷途小碼農(nóng)么么噠  
這篇文章主要為大家詳細介紹了如何使用JavaScript優(yōu)雅實現(xiàn)文本展開收起功能,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

前言

實現(xiàn)文本溢出的展開收起功能,純 CSS 方案在網(wǎng)頁中可行,但在小程序中存在兼容性問題。

最優(yōu)的解決方案就是使用 JavaScript 的二分截斷法。

看了下 vant 的 TextEllipsis 組件源碼。

理解了算法的實現(xiàn)原理后就寫了一個uniapp版本和vue3版本的展開收起組件。

算法步驟:

  • 創(chuàng)建隱藏容器并渲染內(nèi)容。
  • 計算最大行高(行數(shù) × 單行行高)。
  • 使用遞歸算法,類似于 tail(left, content.length)。
  • 取中間值,并將其寫入隱藏容器。
  • 等待渲染完成后獲取最新高度。
  • 如果隱藏容器的高度超過最大行高,則繼續(xù)調(diào)用 tail,使用 left = left,right = middle。
  • 否則,可能是內(nèi)容太少了(或者無法再繼續(xù)截斷,那就返回截取的內(nèi)容)。使用 left = middle,right = right 繼續(xù)調(diào)用 tail。

這個算法通過不斷地二分截斷,尋找到最合適的截取內(nèi)容。
就算是1000多字,限定2行展示,截斷次數(shù)也只在10次左右。

擴展:canvas海報的文字溢出功能也可以用這個算法。

uniapp版本

下面是從源碼抽離出來單獨封裝的uniapp和vue3版本(網(wǎng)頁,小程序,app都測試過)

先上效果圖 300多ms:

uniapp版本有一些需要注意的點,如果兼容運行在小程序和app的話。

  • 在小程序中,樣式計算是在渲染過程中異步進行的,必須nextTick后才能獲取容器最新高度(因為小程序樣式計算是異步的。所以性能比不上網(wǎng)頁的2ms,實測是300+ms)。
  • 獲取元素節(jié)點信息的方法也不一樣。
  • 行高如果是繼承的獲取的就是inherit。所以需要傳行高進去。
<template>
  <view
    :class="{root:true,visible:!show}"
    :style="{ lineHeight: props.lineHeight }"
  >
    {{ expanded ? props.content : text }}
    <text class="action" v-if="hasAction" @click="onClickAction"
      >{{ actionText }}</text
    >
  </view>
  <view :class="{hiddenText:true}" :style="{ lineHeight: props.lineHeight }"
    >{{ text }}</view
  >
</template>

<script lang="ts" setup>
  import { defineProps, ref, getCurrentInstance, nextTick, computed, onMounted } from 'vue';
  const instance = getCurrentInstance(); // 獲取組件實例

  const props = defineProps({
  	content: {
  		type: String,
  		default: ''
  	},
  	rows: {
  		type: Number,
  		default: 2
  	},
  	lineHeight: {
  		type: Number,
  		default: '30rpx'
  	}
  });

  const expanded = ref(false);
  const text = ref(props.content);
  const hasAction = ref(false);
  const show= ref(false);

  const actionText = computed(() => {
  	return expanded.value ? '收起' : '展開';
  });
  const onClickAction = () => {
  	expanded.value = !expanded.value;
  };
  // 查詢元素形狀信息
  const qeuryRect = queryText => {
  	let query = uni.createSelectorQuery().in(instance);
  	return new Promise((resolve, reject) => {
  		query
  			.select(queryText)
  			.boundingClientRect(rect => {
  				resolve(rect);
  			})
  			.exec();
  	});
  };
  // 查詢元素樣式屬性等信息
  const qeuryRectProp = queryText => {
  	let query = uni.createSelectorQuery().in(instance);
  	return new Promise((resolve, reject) => {
  		query
  			.select(queryText)
  			.fields({ computedStyle: ['lineHeight', 'height'], dataset: true, size: true }, rect => {
  				resolve(rect);
  			})
  			.exec();
  	});
  };
  let dots = '...';
  let content = props.content;
  let end = content.length;
  const setHiddenText = val => {
  	return new Promise((_, reject) => {
  		text.value = val;
  		console.error(val);
  		nextTick(() => {
  			_(val);
  		});
  	});
  };
  // 計算截斷
  const calcEllipsisText = maxHeight => {
  	const tail = async (left, right) => {
  		// 遞歸終止條件
  		if (right - left <= 1) {
  			return content.slice(0, left) + dots;
  		}
  		const middle = Math.round((left + right) / 2);
  		// 設(shè)置攔截位置(注意slice 0,middle,雖然left ,right不斷變,但是0是不變的)
  		await setHiddenText(content.slice(0, middle) + dots + actionText.value);
  		let result = await qeuryRectProp('.hiddenText');
  		if (parseInt(result.height) > maxHeight) {
  			return tail(left, middle);
  		}
  		// 太往左了,內(nèi)容不夠,需要往右邊移動
  		return tail(middle, right);
  	};
  	tail(0, end).then(res => {
  		text.value = res;
  		show.value=true
  		console.timeEnd("完成計算")
  	});
  };
  // 開始計算
  onMounted(() => {
  	console.time("完成計算")
  	nextTick(async () => {
  		let result = await qeuryRectProp('.hiddenText');
  		let maxHeight = parseInt(result.lineHeight) * props.rows;
  		// 隱藏的行高大于限定行數(shù)高度
  		if (maxHeight < parseInt(result.height)) {
  			hasAction.value = true;
  			calcEllipsisText(maxHeight);
  		} else {
  			hasAction.value = false;
  			text.value = props.content;
  			show.value=true
  		}
  	});
  });
</script>

<style lang="scss" scoped>
  .visible {
  	visibility: hidden;
  }
  .hiddenText {
  	position: fixed;
  	z-index: -999;
  	top: -9999px;
  }
  .action{
  	color:#1989fa;
  }
</style>

vue3版本

先上效果圖:2ms

<template>
  <div ref="root">
    {{ expanded ? props.content : text }}
    <span v-if="hasAction" class="action" @click="onClickAction">
      {{ actionText }}
    </span>
  </div>
</template>

<script setup>
  import { ref, watch, computed, onMounted, onUnmounted, onActivated, defineProps, defineEmits } from 'vue'

  const emit = defineEmits(['clickAction'])
  const props = defineProps({
    rows: {
      type: Number,
      default: 2,
    },
    dots: {
      type: String,
      default: '...',
    },
    content: {
      type: String,
      default: '',
    },
    expandText: {
      type: String,
      default: '展開',
    },
    collapseText: {
      type: String,
      default: '收起',
    },
  })

  const useWindowResize = () => {
    const window_width = ref(window.innerWidth)
    onMounted(() => {
      window.addEventListener('resize', () => {
        windowWidth.value = window.innerWidth
      })
    })
    onUnmounted(() => {
      window.removeEventListener('resize', () => {
        windowWidth.value = window.innerWidth
      })
    })
    return window_width
  }
  const windowWidth = useWindowResize()

  const text = ref('')
  const expanded = ref(false)
  const hasAction = ref(false)
  const root = ref(null)
  let needRecalculate = false
  const actionText = computed(() => (expanded.value ? props.collapseText : props.expandText))

  const pxToNum = (value) => {
    if (!value) return 0
    const match = value.match(/^\d*(\.\d*)?/)
    return match ? Number(match[0]) : 0
  }

  const cloneContainer = () => {
    if (!root.value || !root.value.isConnected) return
    const originStyle = window.getComputedStyle(root.value)
    const container = document.createElement('div')
    const styleNames = Array.from(originStyle)
    styleNames.forEach((name) => {
      container.style.setProperty(name, originStyle.getPropertyValue(name))
    })
    container.style.position = 'fixed'
    container.style.zIndex = '-9999'
    container.style.top = '-9999px'
    container.style.height = 'auto'
    container.style.minHeight = 'auto'
    container.style.maxHeight = 'auto'
    container.innerText = props.content
    document.body.appendChild(container)
    return container
  }
  const calcEllipsised = () => {
    console.time('完成計算')
    const calcEllipsisText = (container, maxHeight) => {
      const { content, dots } = props
      const end = content.length
      const calcEllipse = () => {
        const tail = (left, right) => {
          // 遞歸終止條件
          if (right - left <= 1) {
            return content.slice(0, left) + dots
          }
          const middle = Math.round((left + right) / 2)
          // 設(shè)置攔截位置
          container.innerText = content.slice(0, middle) + dots + actionText.value
          if (container.offsetHeight > maxHeight) {
            return tail(left, middle)
          }
          // 太往左了,內(nèi)容不夠,需要往右邊移動
          return tail(middle, right)
        }
        container.innerText = tail(0, end)
        console.timeEnd('完成計算')
      }
      calcEllipse()
      return container.innerText
    }

    // 計算截斷文本
    const container = cloneContainer()

    if (!container) {
      needRecalculate = true
      return
    }

    const { paddingBottom, paddingTop, lineHeight } = container.style
    const maxHeight = Math.ceil(
      (Number(props.rows) + 0.5) * pxToNum(lineHeight) + pxToNum(paddingTop) + pxToNum(paddingBottom)
    )

    if (maxHeight < container.offsetHeight) {
      hasAction.value = true
      text.value = calcEllipsisText(container, maxHeight)
    } else {
      hasAction.value = false
      text.value = props.content
    }

    document.body.removeChild(container)
  }

  const toggle = (isExpanded = !expanded.value) => {
    expanded.value = isExpanded
  }

  const onClickAction = (event) => {
    toggle()
    emit('clickAction', event)
  }

  onMounted(calcEllipsised)

  onActivated(() => {
    if (needRecalculate) {
      needRecalculate = false
      calcEllipsised()
    }
  })

  watch([windowWidth, () => [props.content, props.rows]], calcEllipsised)

  defineExpose({ toggle })
</script>

<style scoped>
  .action {
    color: #1989fa;
  }
</style>

到此這篇關(guān)于使用JavaScript優(yōu)雅實現(xiàn)文本展開收起功能的文章就介紹到這了,更多相關(guān)JavaScript文本展開收起內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論