article-intro.vue 4.68 KB
<template>
  <div ref="intro" class="intro-wrap" :class="introClass" :style="introStyle" @click="onExpanding">
    <div v-if="trimIntro" class="intro-content">
      <p class="pre-wrap">{{intro}}</p>
      <div ref="introPool" class="intro-pool pre-wrap" v-if="!introHeight">{{trimIntro}}</div>
      <span ref="expand" class="expand" v-if="showExpandTxt">…<b>继续阅读</b></span>
    </div>
    <div class="collapse" v-if="showCollapseTxt">收起</div>
  </div>
</template>

<script>
import {get, trim, forEach} from 'lodash';

const MAX_LINES = 6;

export default {
  name: 'ArticleIntro',
  props: {
    data: {
      type: Object,
      default() {
        return {};
      }
    },
    share: Boolean,
    thumb: Boolean
  },
  data() {
    return {
      introHeight: 0,
      introCollapseHeight: 0,
      isIntroEllipsis: true,
      isIntroExpand: false,
      isIntroAnimateing: false,
      canRetract: false,
    }
  },
  watch: {
    'data.intro': function() {
      this.introHeight = 0;
      this.updateHeight();
    }
  },
  computed: {
    maxLines() {
      return (this.data.maxLines || MAX_LINES) - 1;
    },
    trimIntro() {
      this.introHeight = 0;
      this.introCollapseHeight = 0;
      return trim(this.data.intro);
    },
    showIntro() {
      let line = 0;
      let textArr = [];

      forEach(this.trimIntro, (val, index) => {
        let isEnter = /(\r\n)|(\n)/.test(val);

        textArr[line] = textArr[line] || '';
        textArr[line] += val;

        if (textArr[line].length >= 24 || isEnter) {
          line++;
        }

        if (line > this.maxLines) {
          textArr[this.maxLines] = textArr[this.maxLines].substring(0, Math.min(21, textArr[this.maxLines].length));
          return false;
        }
      });

      this.isIntroEllipsis = line > this.maxLines;

      return trim(textArr.join(''));
    },
    intro() {
      if (this.isIntroExpand || !this.isIntroEllipsis) {
        return this.trimIntro;
      } else {
        return this.showIntro;
      }
    },
    introClass() {
      return {
        'intro-expand': this.isIntroExpand,
        'no-more': !this.isIntroEllipsis,
        'intro-will-change': this.isIntroAnimateing
      };
    },
    introStyle() {
      let introHeight;

      if (this.isIntroExpand) {
        introHeight = this.introHeight;
      } else {
        introHeight = this.introCollapseHeight;
      }

      return {
        height: introHeight ? `${introHeight}px` : void 0
      };
    },
    showExpandTxt() {
      return this.isIntroEllipsis && !this.isIntroExpand && !this.isIntroAnimateing;
    },
    showCollapseTxt() {
      return this.isIntroEllipsis && this.isIntroExpand && !this.isIntroAnimateing && this.canRetract;
    }
  },
  mounted() {
    this.updateHeight();

    this.$refs.intro && this.$refs.intro.addEventListener('transitionend', this.onExpand.bind(this));
  },
  destroyed() {
    this.$refs.intro && this.$refs.intro.removeEventListener('transitionend', this.onExpand.bind(this));
  },
  methods: {
    updateHeight() {
      this.$nextTick(() => {
        this.introHeight = get(this.$refs, 'introPool.scrollHeight', 0) + 20;
        this.introCollapseHeight = get(this.$refs, 'intro.offsetHeight', 0);
      });
    },
    onExpanding() {
      if (!this.isIntroEllipsis || (this.isIntroExpand && !this.canRetract)) {
        return;
      }

      if (!this.canRetract) {
        this.canRetract = this.introHeight - 21 > get(this.$refs, 'expand.offsetHeight', 0) * (this.maxLines + 4);
      }

      this.isIntroExpand = !this.isIntroExpand;
      this.isIntroAnimateing = true;
    },
    onExpand() {
      this.isIntroAnimateing = false;
      this.$nextTick(() => {
        this.$emit('on-expand');
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.intro-wrap {
  padding: 0 30px;
  margin-top: 20px;
  overflow: hidden;
  transition: height 250ms cubic-bezier(0.165, 0.84, 0.44, 1);

  .intro-content {
    line-height: 48px;
    font-size: 28px;
    color: #4a4a4a;
    letter-spacing: 0.06PX;
    box-sizing: content-box;
    position: relative;
  }

  .pre-wrap {
    white-space: pre-wrap;
    word-wrap: break-word;
    display: inline;
  }

  &.intro-will-change {
    will-change: height;
  }

  &.intro-expand {
    -webkit-line-clamp: initial;
    text-overflow: initial;
  }

  &.no-more {
    height: auto;
  }
}

.intro-pool {
  position: absolute;
  width: 100%;
  top: -1000px;
  visibility: hidden;
}

.collapse {
  width: 100%;
  text-align: right;
  font-size: 28px;
  color: #000;
  font-weight: bold;
}

.expand {
  font-size: 28px;
  color: #000;
  line-height: 20px;

  &.collapse {
    position: absolute;
    right: 14px;
    bottom: 0;
    height: 28px;
  }

  > b {
    font-weight: bold;
  }
}
</style>