javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript文本展开收起

使用JavaScript优雅实现文本展开收起功能

作者:迷途小码农么么哒

这篇文章主要为大家详细介绍了如何使用JavaScript优雅实现文本展开收起功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

前言

实现文本溢出的展开收起功能,纯 CSS 方案在网页中可行,但在小程序中存在兼容性问题。

最优的解决方案就是使用 JavaScript 的二分截断法。

看了下 vant 的 TextEllipsis 组件源码。

理解了算法的实现原理后就写了一个uniapp版本和vue3版本的展开收起组件。

算法步骤:

这个算法通过不断地二分截断,寻找到最合适的截取内容。
就算是1000多字,限定2行展示,截断次数也只在10次左右。

扩展:canvas海报的文字溢出功能也可以用这个算法。

uniapp版本

下面是从源码抽离出来单独封装的uniapp和vue3版本(网页,小程序,app都测试过)

先上效果图 300多ms:

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

<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);
  		// 设置拦截位置(注意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);
  		}
  		// 太往左了,内容不够,需要往右边移动
  		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;
  		// 隐藏的行高大于限定行数高度
  		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)
          // 设置拦截位置
          container.innerText = content.slice(0, middle) + dots + actionText.value
          if (container.offsetHeight > maxHeight) {
            return tail(left, middle)
          }
          // 太往左了,内容不够,需要往右边移动
          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>

到此这篇关于使用JavaScript优雅实现文本展开收起功能的文章就介绍到这了,更多相关JavaScript文本展开收起内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文