代码做了简化,只保留核心部分。
欢迎大家评论交流。

1, 组件介绍

Collapse 折叠面板

作用:通过折叠面板收纳内容区域

  • 同时打开多个,互不影响
  • 只能打开一个,也就是手风琴效果。

组件展示

2,组件组成

由 3 个组件组成

  • el-collapse,作为整体容器。
  • el-collapse-item,包括面板标题和面板内容
  • el-collapse-transition,实现面板内容的过度动画

相关 AttributesEventsSlots,举例如下:

<el-collapse accordion v-model="activeNames" @change="handleChange">
  <el-collapse-item name="1">
    <template #title>
      Consistency
      <el-icon class="header-icon">
        <info-filled />
      </el-icon>
    </template>
    <div>Consistent with real life: in line with the process and logic of real life, and comply with languages and habits that the users are used to;</div>
    <div>Consistent within interface: all elements should be consistent, such as: design style, icons and texts, position of elements, etc.</div>
  </el-collapse-item>
  <el-collapse-item title="Feedback" name="2" disabled>
    <div>Operation feedback: enable the users to clearly perceive their operations by style updates and interactive effects;</div>
  </el-collapse-item>
</el-collapse>

3,组件实现

3.1,el-collapse

1,v-model=“activeNames”

对组件使用v-model

<el-collapse v-model="activeNames"></el-collapse>

相当于

<el-collapse :modelValue="activeNames" @update:modelValue="newValue => activeNames = newValue"></el-collapse>

所以 el-collapse 在组件中定义了:

  • props: modelValue
  • emits: update:modelValue

2,手风琴效果

每次只能展开1个面板

element-plus 为了使用方便,当为手风琴效果时,activeNames 类型可以设置为 string。

// 使用
const activeName = ref('1')

非手风琴效果时,传入数组

// 使用
const activeNames = ref(['1'])

所以,最终 modelValue 的类型如下:

const props = defineProps({
  modelValue: {
    type: [Array, String, Number],
    default: () => []
  }
})

3,el-collapse 关键逻辑

  1. 处理 activeNames,统一为数组格式。
  2. 定义点击面板的事件 handleItemClick,统一处理面板的折叠状态。

html 部分

<template>
  <div class="el-collapse">
    <slot />
  </div>
</template>

js 部分

// 暴露给用户的参数,可以让用户手动设置 activeNames
const { activeNames, setActiveNames } = useCollapse(props, emit)
defineExpose({
  /** @description active names */
  activeNames,
  /** @description set active names */
  setActiveNames
})

主要逻辑

/**
 * @param props {accordion: boolean, modelValue: string | []}
 * @param emit {"update:modelValue", "change"}
 */
const useCollapse = (props, emit) => {
  // activeNames 数组表示已打开的面板
  // ensureArray 将传入的值转为数组
  const activeNames = ref(ensureArray(props.modelValue))

  const setActiveNames = (_activeNames) => {
    activeNames.value = _activeNames
    // 手风琴 ? 则只打开一个 : 所有都可以打开
    const value = props.accordion ? activeNames.value[0] : activeNames.value
    emit('update:modelValue', value)
    emit('change', value)
  }

  // 点击面板
  const handleItemClick = (name) => {
    if (props.accordion) {
      // 手风琴点击相同会关闭
      setActiveNames([activeNames.value[0] === name ? '' : name])
    } else {
      const _activeNames = [...activeNames.value]
      const index = _activeNames.indexOf(name)

      // 点击已存在的,则从 activeNames 删除(关闭),反之打开
      if (index > -1) {
        _activeNames.splice(index, 1)
      } else {
        _activeNames.push(name)
      }
      setActiveNames(_activeNames)
    }
  }

  // 先统一处理为数组
  watch(
    () => props.modelValue,
    () => (activeNames.value = ensureArray(props.modelValue)),
    { deep: true }
  )

  // 传递给 el-collapse-item 使用,collapseContextKey 作为唯一标识 = Symbol('collapseContextKey')
  provide(collapseContextKey, {
    activeNames,
    handleItemClick
  })
  return {
    activeNames,
    setActiveNames
  }
}

Tips:ensureArray 实现, lodash.castArray

3.2,el-collapse-item

1,el-collapse-item 关键逻辑

  1. 在点击面板时,将对应 name 传递给父级 el-collapsehandleItemClick 处理
  2. 计算展示相关样式(样式这里不做讨论,因为都是一些简单的布局样式)

html 部分

<template>
  <div :class="rootKls">
    <!-- 面板 title 部分 -->
    <div>
      <div
        :class="headKls"
        :tabindex="disabled ? -1 : 0"
        @click="handleHeaderClick"
        @keypress.space.enter.stop.prevent="handleEnterClick"
        @focus="handleFocus"
        @blur="focusing = false"
      >
        <slot name="title">{{ title }}</slot>
        <el-icon :class="arrowKls">
          <arrow-right />
        </el-icon>
      </div>
    </div>
    <!-- 内容部分 -->
    <el-collapse-transition>
      <div v-show="isActive" class="el-collapse-item__wrap">
        <div class="el-collapse-item__content">
          <slot />
        </div>
      </div>
    </el-collapse-transition>
  </div>
</template>

js 部分

name 唯一标识符,对应activeName,用于判断打开和折叠面板

// { title: string, name: [String, Number], disabled: boolean }
const props = defineProps(['title', 'name', 'disabled'])

const { focusing, isActive, handleFocus, handleHeaderClick, handleEnterClick } = useCollapseItem(props)

const { arrowKls, headKls, rootKls } = useCollapseItemDOM(props, { focusing, isActive })

// 暴露出给用户用的参数
defineExpose({
  /** @description current collapse-item whether active */
  isActive
})

主要逻辑

const useCollapseItem = (props) => {
  // { activeNames, handleItemClick }
  const collapse = inject(collapseContextKey)

  // tabindex 可以控制 Tab 键切换 el-collapse-item 面板,元素会处于 focus 状态,
  // focusing,isClick,handleFocus 这3个都是为了控制 focus 状态的 css
  const focusing = ref(false)
  const isClick = ref(false)
  // 是否被选中,影响 css
  const isActive = computed(() => collapse?.activeNames.value.includes(props.name))

  const handleFocus = () => {
    setTimeout(() => {
      if (!isClick.value) {
        focusing.value = true
      } else {
        isClick.value = false
      }
    }, 50)
  }

  // 调用 el-collapse 传递的方法
  const handleHeaderClick = () => {
    if (props.disabled) return
    collapse?.handleItemClick(props.name)
    focusing.value = false
    isClick.value = true
  }

  const handleEnterClick = () => {
    collapse?.handleItemClick(props.name)
  }

  return {
    focusing,
    isActive,
    handleFocus,
    handleHeaderClick,
    handleEnterClick
  }
}
// 动态设置 class
const useCollapseItemDOM = (props, { focusing, isActive }) => {
  const rootKls = computed(() => ['el-collapse-item', unref(isActive) && 'is-active', props.disabled && 'is-disabled'])
  const headKls = computed(() => ['el-collapse-item__header', unref(isActive) && 'is-active', { focusing: unref(focusing) && !props.disabled }])
  const arrowKls = computed(() => ['el-collapse-item__arrow', unref(isActive) && 'is-active'])

  return {
    rootKls,
    headKls,
    arrowKls
  }
}

3.3,el-collapse-transition

1,el-collapse-transition 关键逻辑

  1. 实现面板内容部分 el-collapse-item__wrap 的折叠动画。

html 部分

<template>
  <transition name="el-collapse-transition" v-on="on">
    <slot />
  </transition>
</template>

js 部分

先说几点前提:

  1. v-on 支持对象语法
<!-- 对象语法 -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
  1. Transition 支持 JavaScript 钩子函数

对应关系

css 过渡 classJavaScript 钩子函数
v-enter-frombeforeEnter
v-enter-activeenter
v-enter-toafterEnter
v-leave-frombeforeLeave
v-leave-activeleave
v-leave-toafterLeave
  1. 在高度变化的折叠动画中:
    • paddingToppaddingBottom 需要做处理,因为当元素 height = 0 时,padding 还会占据高度。
    • overflow: hidden 为了形成BFC,让浮动元素也参与高度计算。浮动元素会让父级高度塌陷
// el 是被 transition 组件包裹的元素
const on = {
  beforeEnter(el) {
    if (!el.dataset) el.dataset = {}
    el.dataset.oldPaddingTop = el.style.paddingTop
    el.dataset.oldPaddingBottom = el.style.paddingBottom

    el.style.maxHeight = 0
    el.style.paddingTop = 0
    el.style.paddingBottom = 0
  },

  enter(el) {
    el.dataset.oldOverflow = el.style.overflow
    el.style.overflow = 'hidden'
    
    el.style.maxHeight = `${el.scrollHeight}px`
    el.style.paddingTop = el.dataset.oldPaddingTop
    el.style.paddingBottom = el.dataset.oldPaddingBottom
  },

  afterEnter(el) {
    el.style.maxHeight = '' // 需要还原。
    el.style.overflow = el.dataset.oldOverflow
  },

  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.overflow = 'hidden'
    el.style.maxHeight = `${el.scrollHeight}px`
  },
  
  leave(el) {
    if (el.scrollHeight !== 0) {
      el.style.maxHeight = 0
      el.style.paddingTop = 0
      el.style.paddingBottom = 0
    }
  },
  
  afterLeave(el) {
    el.style.maxHeight = ''
    el.style.overflow = el.dataset.oldOverflow
    el.style.paddingTop = el.dataset.oldPaddingTop
    el.style.paddingBottom = el.dataset.oldPaddingBottom
  },
}

css 部分

.el-collapse-transition-leave-active,
.el-collapse-transition-enter-active {
  transition: 0.3s max-height ease-in-out,
    0.3s padding-top ease-in-out,
    0.3s padding-bottom ease-in-out;
}

以上是 Collapse 折叠面板的全部逻辑。
如果对你有帮助,可以点个关注支持下。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐