代码做了简化,只保留核心部分。
欢迎大家评论交流。
Collapse 折叠面板
作用:通过折叠面板收纳内容区域
由 3 个组件组成
相关 Attributes
, Events
, Slots
,举例如下:
<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>
对组件使用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
每次只能展开1个面板。
element-plus 为了使用方便,当为手风琴效果时,activeNames
类型可以设置为 string。
// 使用
const activeName = ref('1')
非手风琴效果时,传入数组
// 使用
const activeNames = ref(['1'])
所以,最终 modelValue
的类型如下:
const props = defineProps({
modelValue: {
type: [Array, String, Number],
default: () => []
}
})
activeNames
,统一为数组格式。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
name
传递给父级 el-collapse
的 handleItemClick
处理html 部分
<template>
<div :class="rootKls">
<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
}
}
el-collapse-item__wrap
的折叠动画。html 部分
<template>
<transition name="el-collapse-transition" v-on="on">
<slot />
transition>
template>
js 部分
先说几点前提:
<button v-on="{ mousedown: doThis, mouseup: doThat }">button>
对应关系
css 过渡 class | JavaScript 钩子函数 |
---|---|
v-enter-from | beforeEnter |
v-enter-active | enter |
v-enter-to | afterEnter |
v-leave-from | beforeLeave |
v-leave-active | leave |
v-leave-to | afterLeave |
paddingTop
和 paddingBottom
需要做处理,因为当元素 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 折叠面板的全部逻辑。
如果对你有帮助,可以点个关注支持下。