element-ui element-plus collapse - 分析

郭盛
2023-12-01

源代码地址-collapse
源代码地址-collapse-item

version:element-plus 1.0.1-beta.0

collapse

<template>
  <div class="el-collapse" role="tablist" aria-multiselectable="true">
    <slot></slot>
  </div>
</template>
<script lang="ts">
import {
  defineComponent,
  ref,
  watch,
  provide,
  PropType,
  Ref,
  onUnmounted,
} from 'vue'
import mitt, { Emitter } from 'mitt'

export interface CollapseProvider {
  activeNames: Ref
  collapseMitt: Emitter
}
export default defineComponent({
  name: 'ElCollapse',
  props: {
    // 是否手风琴模式
    accordion: Boolean,
    // string | number | Array<string | number>
    // 文档说 如果是手风琴模式,绑定值类型需要为string,否则为array
    modelValue: {
      type: [Array, String, Number] as PropType<
        string | number | Array<string | number>
      >,
      default: () => [],
    },
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // concat 接受modelValue限制的所有类型 拼接成数组
    const activeNames = ref([].concat(props.modelValue))
    const collapseMitt: Emitter = mitt()

    const setActiveNames = _activeNames => {
      activeNames.value = [].concat(_activeNames)
      // 手风琴模式下 返回第一个 不然 数组返回
      const value = props.accordion ? activeNames.value[0] : activeNames.value
      emit('update:modelValue', value)
    }

    // 子组件item被点击
    const handleItemClick = name => {
      // 是否是手风琴模式
      if (props.accordion) {
        // 手风琴模式其实就是一个字符串
        setActiveNames(
          // 如果激活的有1个 || 第一个 === 0
          (activeNames.value[0] || activeNames.value[0] === 0) &&
            // 第一个 === 传入的name ? '' : name
            // 就是 点击展开的 -> 要收回去
            activeNames.value[0] === name
            ? ''
            : name,
        )
      } else {
        // 非手风琴模式
        // 深拷贝一份
        let _activeNames = activeNames.value.slice(0)
        // 找到name在[]中的index
        const index = _activeNames.indexOf(name)

        // 存在就收回去 不存在就push进去
        if (index > -1) {
          _activeNames.splice(index, 1)
        } else {
          _activeNames.push(name)
        }
        setActiveNames(_activeNames)
      }
    }

    // props.modelValue 每次变化 都会更新activeNames.value的值,使得每次都是最近的[]结构
    watch(
      () => props.modelValue,
      () => {
        activeNames.value = [].concat(props.modelValue)
      },
    )

    // 接收到子组件传上来的click事件
    collapseMitt.on('item-click', handleItemClick)

    onUnmounted(() => {
      collapseMitt.all.clear()
    })

    provide('collapse', {
      activeNames,
      collapseMitt,
    })

    return {
      activeNames,
      setActiveNames,
      handleItemClick,
    }
  },
})
</script>

collapse-item

<template>
  <div
    class="el-collapse-item"
    :class="{'is-active': isActive, 'is-disabled': disabled }"
  >
    <!-- title -->
    <div
      role="tab"
      :aria-expanded="isActive"
      :aria-controls="`el-collapse-content-${id}`"
      :aria-describedby="`el-collapse-content-${id}`"
    >
      <div
        :id="`el-collapse-head-${id}`"
        class="el-collapse-item__header"
        role="button"
        :tabindex="disabled ? -1 : 0"
        :class="{
          'focusing': focusing,
          'is-active': isActive
        }"
        @click="handleHeaderClick"
        @keyup.space.enter.stop="handleEnterClick"
        @focus="handleFocus"
        @blur="focusing = false"
      >
        <!-- title 插槽 -->
        <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>
      <!-- v-show 控制 -->
      <div
        v-show="isActive"
        :id="`el-collapse-content-${id}`"
        class="el-collapse-item__wrap"
        role="tabpanel"
        :aria-hidden="!isActive"
        :aria-labelledby="`el-collapse-head-${id}`"
      >
        <div class="el-collapse-item__content">
          <slot></slot>
        </div>
      </div>
    </el-collapse-transition>
  </div>
</template>
<script lang='ts'>
import { defineComponent, PropType, inject, computed, ref } from 'vue'
import { CollapseProvider } from './collapse'
// 生成[0,10000]的随机数
import { generateId } from '@element-plus/utils/util'
import ElCollapseTransition from '@element-plus/collapse-transition'

export default defineComponent({
  name: 'ElCollapseItem',
  components: { ElCollapseTransition },
  props: {
    title: {
      type: String,
      default: '',
    },
    // 默认值是随机生成的number [0,10000]
    name: {
      type: [String, Number] as PropType<string | number>,
      default: () => {
        return generateId()
      },
    },
    disabled: Boolean,
  },
  setup(props) {
    const collapse = inject<CollapseProvider>('collapse')
    const collapseMitt = collapse?.collapseMitt

    // 暴露出去但是没有使用
    const contentWrapStyle = ref({
      height: 'auto',
      display: 'block',
    })
    // 暴露出去但是没有使用
    const contentHeight = ref(0)
    const focusing = ref(false)
    const isClick = ref(false)
    const id = ref(generateId())

    // 是否是展开状态  判断activeNames中是哦否含有 当前item的name
    // is-active 的样式就是title中的border-bottom 为transparent
    const isActive = computed(() => {
      return collapse?.activeNames.value.indexOf(props.name) > -1
    })

    // focus 事件
    const handleFocus = () => {
      // 50ms后 如果不是isClick 则置为true 反则false
      // 我看了感觉就是isClick取反
      // 我看了一下样式 focusing 就是 title 的 color 是主题蓝色
      setTimeout(() => {
        // ? isClick.value = !isClick.value
        if(!isClick.value) {
          focusing.value = true
        } else {
          isClick.value = false
        }
      }, 50)
    }
    // item 点击事件
    const handleHeaderClick = () => {
      if(props.disabled) return
      collapseMitt?.emit('item-click', props.name)
      focusing.value = false
      isClick.value = true
    }

    // focus 的可以通过键盘中的 space 和 enter 触发点击事件
    // 但是space会往下滚动 不太清楚原因
    const handleEnterClick = () => {
      collapseMitt?.emit('item-click', props.name)
    }

    return {
      isActive,
      contentWrapStyle,
      contentHeight,
      focusing,
      isClick,
      id,
      handleFocus,
      handleHeaderClick,
      handleEnterClick,
      collapse,
    }
  },
})
</script>
 类似资料: