my-typography/src/ellipsis.js

import Vue from 'vue'
import {setStyle} from 'element-ui/lib/utils/dom'

// We only handle element & text node.
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const COMMENT_NODE = 8;


const wrapperStyle = {
  padding: 0,
  margin: 0,
  display: 'inline',
  lineHeight: 'inherit'
};

/**
 * 像素转换成数字类型
 * @param {string} value
 * @return {number}
 */
function pxToNumber(value) {
  if (!value) return 0;
  
  const match = value.match(/^\d*(\.\d*)?/);
  return match ? Number(match[0]) : 0;
}

/**
 * 样式对象转换成字符串形式
 * @param {Object} style
 * @return {string}
 */
function styleToString(style) {
  const styleNames = Array.prototype.slice.apply(style);
  return styleNames.map(name => `${name}: ${style.getPropertyValue(name)};`).join('');
}


/**
 * 创建临时容器
 * @param {Object} style
 * @return {HTMLElement}
 */
function createEllipsisContainer(style) {
  const el = document.createElement('div')
  const originCSS = styleToString(style)
  el.setAttribute('style', originCSS)
  setStyle(el, {
    position: 'fixed',
    left: '0',
    height: 'auto',
    minHeight: 'auto',
    maxHeight: 'auto',
    top: '-999999px',
    zIndex: '-1000',
    textOverflow: 'clip',
    whiteSpace: 'normal',
    webkitLineClamp: 'none'
  })
  document.body.appendChild(el)
  return el
}

/**
 *
 * 删除临时容器
 * @param {HTMLElement} el
 */
function removeEllipsisContainer(el) {
  el.parentNode.removeChild(el)
}

/**
 * 实例化用于计算的临时DOM
 * @param el
 * @param content
 * @param fixedContent
 * @return {*}
 */
function instanceVue(el, content, fixedContent) {
  const Container = Vue.extend({
    functional: true,
    render() {
      return (
        <div style={wrapperStyle}>
          <span style={wrapperStyle}>{content}</span>
          <span style={wrapperStyle}>{fixedContent}</span>
        </div>
      )
    }
  })
  
  const vm = new Container()
  vm.$mount()
  el.appendChild(vm.$el)
  return vm
}

/**
 * 销毁临时DOM
 * @param vm
 */
function destroyVue(vm) {
  vm.$destroy()
  vm.$el.parentNode.removeChild(vm.$el)
}

/**
 * 检查是否在范围内,超出范围需要显示省略号
 * @param el
 * @param maxHeight
 * @return {boolean}
 */
function inRange(el, maxHeight) {
  return el.offsetHeight < maxHeight
}

/**
 *  createEllipsisContentHolder
 * @param el
 * @param ellipsisTextNode
 * @return {HTMLSpanElement}
 */
function createEllipsisContentHolder(el, ellipsisTextNode) {
  el.innerHTML = ''
  // Create origin content holder
  const ellipsisContentHolder = document.createElement('span');
  el.appendChild(ellipsisContentHolder);
  ellipsisContentHolder.appendChild(ellipsisTextNode);
  return ellipsisContentHolder
}

/**
 * Append before fixed nodes
 * @param ellipsisContentHolder
 * @param node
 * @param ellipsisTextNode
 */
function appendChildNode(ellipsisContentHolder, node, ellipsisTextNode) {
  ellipsisContentHolder.insertBefore(node, ellipsisTextNode);
}

/**
 * 计算省略文本
 * @param {HTMLElement} originEle
 * @param {Number} rows
 * @param {VNode[]} content
 * @param {VNode[]} fixedContent
 * @param {string} ellipsisStr
 * @return {Object}
 */
export default function (originEle, rows, content, fixedContent, ellipsisStr = '...') {
  
  // 获取原始样式
  const originStyle = window.getComputedStyle(originEle)
  const ellipsisContainer = createEllipsisContainer(originStyle)
  const lineHeight = pxToNumber(originStyle.lineHeight) || 16
  const maxHeight = lineHeight * (rows + 1) + pxToNumber(originStyle.paddingTop) + pxToNumber(originStyle.paddingBottom)
  if (maxHeight === 0) {
    removeEllipsisContainer(ellipsisContainer)
    return {
      content,
      ellipsis: false
    }
  }
  const vm = instanceVue(ellipsisContainer, content, fixedContent)
  
  if (inRange(ellipsisContainer, maxHeight)) {
    destroyVue(vm)
    removeEllipsisContainer(ellipsisContainer)
    return {
      content,
      ellipsis: false
    }
  }
  
  const cloneNodes = Array.prototype.slice.apply(ellipsisContainer.childNodes[0].childNodes[0].cloneNode(true).childNodes)
  const childNodes = cloneNodes.filter(nodeType => nodeType !== COMMENT_NODE)
  const fixedNodes = Array.prototype.slice.apply(
    ellipsisContainer.childNodes[0].childNodes[1].cloneNode(true).childNodes
  )
  
  destroyVue(vm)
  
  // ========================= Find match ellipsis content =========================
  
  const ellipsisChildren = []
  const ellipsisTextNode = document.createTextNode(ellipsisStr)
  const ellipsisContentHolder = createEllipsisContentHolder(ellipsisContainer, ellipsisTextNode)
  
  fixedNodes.forEach(childNode => {
    ellipsisContainer.appendChild(childNode);
  });
  
  function measureText(textNode, fullText, startLoc = 0, endLoc = fullText.length, lastSuccessLoc = 0) {
    const midLoc = Math.floor((startLoc + endLoc) / 2)
    textNode.textContent = fullText.slice(0, midLoc)
    
    if (startLoc >= endLoc - 1) {
      for (let step = endLoc; step >= startLoc; step -= 1) {
        const currentStepText = fullText.slice(0, step)
        textNode.textContent = currentStepText
        if (inRange(ellipsisContainer, maxHeight)) {
          return step === fullText.length
            ? {
              finished: false,
              vnode: fullText
            }
            : {
              finished: true,
              vnode: currentStepText
            }
        }
      }
    }
    
    if (inRange(ellipsisContainer, maxHeight)) {
      return measureText(textNode, fullText, midLoc, endLoc, midLoc);
    }
    
    return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc)
  }
  
  function measureNode(childNode, index) {
    const type = childNode.nodeType
    
    if (type === ELEMENT_NODE) {
      appendChildNode(ellipsisContentHolder, childNode, ellipsisTextNode)
      if (inRange(ellipsisContainer, maxHeight)) {
        return {
          finished: false,
          vnode: content[index]
        }
      }
      // Clean up if can not pull in
      ellipsisContentHolder.removeChild(childNode)
      return {
        finished: true,
        vnode: null
      }
    }
    
    if (type === TEXT_NODE) {
      const fullText = childNode.textContent || '';
      const textNode = document.createTextNode(fullText);
      appendChildNode(ellipsisContentHolder, textNode, ellipsisTextNode)
      return measureText(textNode, fullText);
    }
    
    return {
      finished: false,
      vnode: null
    }
  }
  
  
  childNodes.some((childNode, index) => {
    const {finished, vnode} = measureNode(childNode, index);
    if (vnode) {
      ellipsisChildren.push(vnode);
    }
    return finished;
  });
  
  removeEllipsisContainer(ellipsisContainer)
  
  return {
    content: ellipsisChildren,
    ellipsis: true
  }
}