Vue elementUi collapse 折叠面板写法解读

3,424 阅读2分钟

baby 朋友们 又是新的一天 今天开始继续分析起来 今天分析的 collapse 折叠面板 基本的示例

<template>
  <div>
    <!-- 我们可以看到 这个是由一个父级  多个子选项组成  value来控制当前展开的是哪一些面板 -->
    <!-- 子选项呢 用一个name 来标识自己 -->
    <!-- ok 接下来来分析实现 -->
    <el-collapse v-model="activeNames" @change="handleChange">
      <el-collapse-item title="一致性 Consistency" name="1">
        <div class="mytest">
          <div>与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;</div>
        </div>
      </el-collapse-item>
      <el-collapse-item title="反馈 Feedback" name="2">
        <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
        <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
      </el-collapse-item>
      <el-collapse-item title="效率 Efficiency" name="3">
        <div>简化流程:设计简洁直观的操作流程;</div>
        <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
        <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
      </el-collapse-item>
      <el-collapse-item title="可控 Controllability" name="4">
        <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
        <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
      </el-collapse-item>
    </el-collapse>
  </div>
</template>

collapse

<template>
   <!-- 看这个机构很简单 父级的 -->
  <div class="el-collapse" role="tablist" aria-multiselectable="true">
    <slot></slot>
  </div>
</template>
<script>
export default {
  name: 'ElCollapse',

  componentName: 'ElCollapse',

  props: {
    // 是否手风琴模式
    accordion: Boolean,
    // 当前展开的值
    value: {
      type: [Array, String, Number],
      default() {
        return [];
      },
    },
  },

  data() {
    return {
      // 这边内部使用activeNames这个值   对传进来的值做个包装
      activeNames: [].concat(this.value),
    };
  },

  provide() {
    return {
      // 把自己暴露出去  方便子组件更自己 做交互
      collapse: this,
    };
  },

  watch: {
    value(value) {
      // 当值变化了
      this.activeNames = [].concat(value);
    },
  },

  methods: {
    setActiveNames(activeNames) {
      // 设置下这个值然后把这个发送出去
      activeNames = [].concat(activeNames);
      const value = this.accordion ? activeNames[0] : activeNames;
      // 感觉这边没有必要赋值  因为上面不是有 watch value了  value改变 自然this.activeNames 会变化
      // 都无所谓吧
      this.activeNames = activeNames;
      this.$emit('input', value);
      this.$emit('change', value);
    },
    // item参数  是子组件本身
    handleItemClick(item) {
      // 如果是手风琴模式的话   也就是说当前点击的这个是  开启的状态   其他的都关闭了
      if (this.accordion) {
        // 等下再来看下
        this.setActiveNames(
          (this.activeNames[0] || this.activeNames[0] === 0)
            && this.activeNames[0] === item.name
            ? '' : item.name,
        );
      } else {
        const activeNames = this.activeNames.slice(0);
        const index = activeNames.indexOf(item.name);
        // 找的到说明是有了  就关闭
        if (index > -1) {
          activeNames.splice(index, 1);
        } else {
          activeNames.push(item.name);
        }
        this.setActiveNames(activeNames);
      }
    },
  },

  created() {
    // 这边监听这个事件  因为子组件会发射这个事件上来  代表点击了  这个item
    this.$on('item-click', this.handleItemClick);
  },
};
</script>

collapse-item

<template>
  <div class="el-collapse-item"
    :class="{'is-active': isActive, 'is-disabled': disabled }">
    <div
      role="tab"
      :aria-expanded="isActive"
      :aria-controls="`el-collapse-content-${id}`"
      :aria-describedby ="`el-collapse-content-${id}`"
    >
      <!-- 这边是头部 标题部分 -->
      <!-- 一般情况下,onblur事件只在input等元素中才有,而div却没有,因为div没有tabindex属性,所以要给div加上此属性。 -->
      <!-- 设置这个之后 tab操作有效果 focusing控制聚焦的时候可以有高亮显示  也可以让这边响应 空格 enter 按键  -->
      <div
        class="el-collapse-item__header"
        @click="handleHeaderClick"
        role="button"
        :id="`el-collapse-head-${id}`"
        :tabindex="disabled ? undefined : 0"
        @keyup.space.enter.stop="handleEnterClick"
        :class="{
          'focusing': focusing,
          'is-active': isActive
        }"
        @focus="handleFocus"
        @blur="focusing = false"
      >
        <!-- 标题插槽 -->
        <slot name="title">{{title}}</slot>
        <i
          class="el-collapse-item__arrow el-icon-arrow-right"
          :class="{'is-active': isActive}">
        </i>
      </div>
    </div>
    <!-- 这边打开关闭加个动画 等下分析下这个动画 组件 -->
    <el-collapse-transition>
      <div
        class="el-collapse-item__wrap"
        style="padding-top: 30px;"
        v-show="isActive"
        role="tabpanel"
        :aria-hidden="!isActive"
        :aria-labelledby="`el-collapse-head-${id}`"
        :id="`el-collapse-content-${id}`"
      >
        <div class="el-collapse-item__content">
          <slot></slot>
        </div>
      </div>
    </el-collapse-transition>
  </div>
</template>

<script>
// 这个动画组件 collapse 展开折叠 的一个动画组件 砸门来分析下  其他东西也没什么了
import ElCollapseTransition from '@/transitions/collapse-transition';
// 这个之前有分析过了
import Emitter from '@/mixins/emitter';
import { generateId } from '@/utils/util';

export default {
  name: 'ElCollapseItem',

  componentName: 'ElCollapseItem',

  mixins: [Emitter],

  components: { ElCollapseTransition },

  data() {
    return {
      contentWrapStyle: {
        height: 'auto',
        display: 'block',
      },
      contentHeight: 0,
      focusing: false,
      isClick: false,
      id: generateId(), // 随机数取前五位  这感觉怕是不稳啊
      // export const generateId = function () {
      //   return Math.floor(Math.random() * 10000);
      // };
    };
  },

  inject: ['collapse'],

  props: {
    title: String,
    name: {
      type: [String, Number],
      default() {
        return this._uid;
      },
    },
    disabled: Boolean,
  },

  computed: {
    isActive() {
      return this.collapse.activeNames.indexOf(this.name) > -1;
    },
  },

  methods: {
    handleFocus() {
      setTimeout(() => {
        if (!this.isClick) {
          this.focusing = true;
        } else {
          this.isClick = false;
        }
      }, 50);
    },
    // 点击事件
    handleHeaderClick() {
      if (this.disabled) return;
      this.dispatch('ElCollapse', 'item-click', this);
      this.focusing = false;
      this.isClick = true;
    },
    // 相当于点击了这个
    handleEnterClick() {
      // 让ElCollapse 触发 item-click事件
      this.dispatch('ElCollapse', 'item-click', this);
    },
  },
};
</script>

ElCollapseTransition

collapse 展开折叠 的一个动画组件

/* eslint-disable class-methods-use-this */
import { addClass, removeClass } from '@/utils/dom';

const Transition = {
  // 进入之前
  beforeEnter(el) {
    // 加上class
    addClass(el, 'collapse-transition');
    // .collapse-transition {
    //   transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out;
    // }
    if (!el.dataset) el.dataset = {};
    // 保存之前的 padding-top padding-bottom
    el.dataset.oldPaddingTop = el.style.paddingTop;
    el.dataset.oldPaddingBottom = el.style.paddingBottom;
    // 设置
    el.style.height = '0';
    el.style.paddingTop = 0;
    el.style.paddingBottom = 0;
  },
  enter(el) {
    // 保存overflow 属性
    el.dataset.oldOverflow = el.style.overflow;
    // scrollHeight 这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容
    // 设置高度 然后自然就会有动画了 因为设置了class
    if (el.scrollHeight !== 0) {
      // console.log(el.scrollHeight, parseInt(el.dataset.oldPaddingTop), Number(el.dataset.oldPaddingBottom));
      // 这边是有bug的  可以提个pr了
      // 高度计算的有问题因为在之前 el.style.paddingTop以及Bottom 都设置为了0 那么这边获取的话就缺少了这块高度
      el.style.height = `${el.scrollHeight}px`;
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    } else {
      el.style.height = '';
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    }

    el.style.overflow = 'hidden';
    console.log(el.style.height, 'before');
  },
  afterEnter(el) {
    // for safari: remove class then reset height is necessary
    // 进入之后把这些还原
    removeClass(el, 'collapse-transition');
    el.style.height = '';
    // console.log(el.style.height, 'af');
    el.style.overflow = el.dataset.oldOverflow;
  },
  // 要隐藏之前
  // 也都是差不多了 离开之前先记住原来的 离开时加上动画的class  离开完之后还原一些属性
  beforeLeave(el) {
    if (!el.dataset) el.dataset = {};
    el.dataset.oldPaddingTop = el.style.paddingTop;
    el.dataset.oldPaddingBottom = el.style.paddingBottom;
    el.dataset.oldOverflow = el.style.overflow;
    el.style.height = `${el.scrollHeight}px`;
    el.style.overflow = 'hidden';
  },

  leave(el) {
    if (el.scrollHeight !== 0) {
      // for safari: add class after set height, or it will jump to zero height suddenly, weired
      addClass(el, 'collapse-transition');
      el.style.height = 0;
      el.style.paddingTop = 0;
      el.style.paddingBottom = 0;
    }
  },

  afterLeave(el) {
    removeClass(el, 'collapse-transition');
    el.style.height = '';
    el.style.overflow = el.dataset.oldOverflow;
    el.style.paddingTop = el.dataset.oldPaddingTop;
    el.style.paddingBottom = el.dataset.oldPaddingBottom;
  },
};

export default {
  name: 'ElCollapseTransition',
  // 使组件无状态 (没有 data) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使它们渲染的代价更小。
  functional: true,
  render(h, { children }) {
    const data = {
      on: { ...Transition },
    };
    return h('transition', data, children);
  },
};