使用方式:
<footer v-fixed="{
fixedPosition: 'bottom', // bottom浮动吸底、top浮动吸顶
immediate: true, // 初始化页面是否启用浮动
fixedStyle: { // 浮动后节点样式
'border-radius': '5px 5px 0 0',
'background-color': 'rgba(90, 90, 90, 0.4)'
}
}">
<el-button @click="back">返回</el-button>
<el-button type="primary">确定</el-button>
</footer>
源码:
/**
*
* description: dom吸顶、吸底
*
*/
// 获取上个存在滚动条的节点
function getScrollDom(el) {
let dom = el
// while (!(dom.scrollHeight > dom.clientHeight) && dom) {
// dom = dom.parentElement
// }
do {
dom = dom.parentElement
dom?.scroll(0, dom.scrollTop + 1)
} while (dom && !dom.scrollTop)
dom?.scroll(0, dom.scrollTop - 1)
return dom
}
// 监听节点是否完全处于视口内
class intersectionObserver {
immediate = false
intersectionObserver = null
markNode = null
constructor(scrollDom, markNode, { immediate }, revert, fixed) {
this.markNode = markNode
this.immediate = immediate
this.intersectionObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
if (this.immediate) revert()
this.immediate = true
return
};
if (this.immediate) {
fixed()
}
}, {
root: scrollDom,
rootMargin: '0px',
threshold: [0, 1] // 交叉阈值,这里设置为0和1,表示从完全不交叉到完全交叉的整个过程都会触发回调
})
}
observe() {
this.intersectionObserver.observe(this.markNode)
}
disconnect() {
this.intersectionObserver.disconnect(this.markNode)
}
}
export default {
inserted: (el, binding, vnode) => {
if (!IntersectionObserver) return
/**
* description: 指令参数说明
* @param fixedPosition bottom下浮动、top上浮动
* @param fixedStyle 浮动后附加样式
* @param immediate 是否页面加载就启用浮动,默认false
* @return {}
*
*/
const { fixedPosition, immediate, fixedStyle, fixedClassName } = {
fixedPosition: 'bottom',
immediate: true,
...binding.value,
fixedStyle: { // 浮动后节点样式
'z-index': '3000',
...(binding.value?.fixedStyle || {})
}
}
if (fixedClassName) el = el.querySelector(fixedClassName)
// 记录节点原有样式
const oldStyle = [
'transition',
'zIndex',
'position',
'margin',
'width',
'left',
'right',
[fixedPosition],
...Object.keys(fixedStyle)
].reduce((old, val) => ({
...old,
[val]: el.style.getPropertyPriority(val)
}), {})
el.oldStyle = oldStyle
el.fixedStyle = fixedStyle
const scrollDom = getScrollDom(el)
// 创建锚点
const markNode = document.createElement('div')
markNode.style.setProperty('padding', '0')
markNode.style.setProperty('margin', '0')
markNode.style.setProperty('transform', `translateY(${el.offsetHeight / 4}px)`)
el.insertAdjacentElement('beforebegin', markNode)
// eslint-disable-next-line new-cap
const domObserver = new intersectionObserver(
scrollDom,
markNode,
{ immediate },
function () {
// 样式还原
Object.entries(el.oldStyle).forEach(([k, v]) => {
el.style.setProperty(k, v)
})
el.scrollIntoView({ behavior: 'instant', block: { top: 'start', bottom: 'end' }[fixedPosition] })
el.fixedOpen = false
},
async function () {
const { clientWidth } = document.body
const { left, right } = el.getBoundingClientRect()
el.style.setProperty('transition', 'all 300ms')
el.style.setProperty('z-index', 999)
el.style.setProperty('position', 'fixed')
el.style.setProperty(fixedPosition, '0px')
el.style.setProperty('left', left + 'px')
el.style.setProperty('right', clientWidth - right + 'px')
el.style.setProperty('margin', '0px')
el.style.setProperty('width', 'auto')
// 添加浮动后样式
Object.entries(el.fixedStyle).forEach(([k, v]) => {
el.style.setProperty(k, v)
})
el.fixedOpen = true
})
// 执行监听
domObserver.observe()
},
update: (el, binding, vnode) => {
if (!IntersectionObserver) return
el.fixedStyle = {
...(el?.fixedStyle || {}),
...(binding.value?.fixedStyle || {})
}
if (!el.fixedOpen) return
Object.entries(binding.value?.fixedStyle || {}).forEach(([k, v]) => {
el.style.setProperty(k, v)
})
}
}
备注:主要使用技术是IntersectionObserver监听节点是否处于视口,css position:fixed 进行吸底、吸顶浮动。代码有升级空间,可改为不使用fixed浮动,节点固定位置改为可滚动窗口的顶部或底部;