UNPKG

lu2

Version:

Simple and flexible UI component library based on native HTML and JavaScript

368 lines (299 loc) 10.9 kB
/** * @ScrollSnap.js * @author zhangxinxu * @version * @description 基于 scroll snap 实现的 UI 组件 * @created: 22-03-08 */ class uiScrollSnap extends HTMLElement { constructor () { super(); this.events(); } static get observedAttributes () { return ['loop', 'type', 'stop', 'align', 'padding']; } set type (value) { this.setAttribute('type', value); } get type () { let type = 'both'; if (this.closest('ui-scroll-snap-swipe')) { type = 'x mandatory'; } return this.getAttribute('type') || type; } set stop (value) { this.setAttribute('stop', value); } get stop () { return this.getAttribute('stop') || 'normal'; } set loop (value) { this.toggleAttribute('loop', value); } get loop () { return this.hasAttribute('loop'); } set align (value) { if (value == 'center') { value = 'centre'; } this.setAttribute('align', value); } get align () { let align = this.getAttribute('align'); if (align !== 'start' || align !== 'end') { align = 'center'; } return align; } appendItemForScroll () { if (this.isAppendItem) { return; } let eleOriginItems = this.items; // 方向 let type = this.type; // 循环滚动不支持双向 // 要么垂直滚动,要么水平滚动 var size = 'height'; var scroll = 'top'; if (type.split(/\s/).includes('x')) { size = 'width'; scroll = 'left'; } // 首字母大写 let sizeUpper = size.replace(/^[a-z]/, function (matches) { return matches.toUpperCase(); }); let scrollUpper = scroll.replace(/^[a-z]/, function (matches) { return matches.toUpperCase(); }); // 此时的 items let clientSize = this['client' + sizeUpper]; let scrollSize = this['scroll' + sizeUpper]; // 如果滚动尺寸不足,没有必要无限滚动,不处理 if (scrollSize <= clientSize) { return; } // 如果滚动位置不足 let eleFragmentAfter = document.createDocumentFragment(); let eleFragmentBefore = document.createDocumentFragment(); [...eleOriginItems].forEach(item => { let cloneItem1 = item.cloneNode(true); let cloneItem2 = item.cloneNode(true); // 去除 selected 状态 cloneItem1.removeAttribute('selected'); cloneItem2.removeAttribute('selected'); // DOM 片段组装 eleFragmentAfter.appendChild(cloneItem1); eleFragmentBefore.appendChild(cloneItem2); }); this.prepend(eleFragmentBefore); this.append(eleFragmentAfter); // 第一个和最后一个元素 let eleCurrentItem = this.querySelectorAll('ui-scroll-snap-item'); let lastItem = eleCurrentItem[eleCurrentItem.length - 1]; // 定位 let offset = eleOriginItems[0]['offset' + scrollUpper] - this['offset' + scrollUpper]; this['scroll' + scrollUpper] = offset; // 判断滚动的方向 let lastScroll = offset; // 滚动事件 this.addEventListener('scroll', () => { let scrollValue = this['scroll' + scrollUpper]; // 尺寸重新 clientSize = this['client' + sizeUpper]; scrollSize = this['scroll' + sizeUpper]; if (scrollValue < lastScroll && scrollValue == 0) { if (this.onScrollStart) { this.onScrollStart(eleCurrentItem, eleOriginItems, offset); } this.style.scrollBehavior = 'auto'; this['scroll' + scrollUpper] = offset; } else if (scrollValue > lastScroll && scrollValue >= (scrollSize - clientSize - 1)) { if (this.onScrollEnd) { this.onScrollEnd(eleCurrentItem, eleOriginItems, offset); } this.style.scrollBehavior = 'auto'; // 偏移一整个原始 items 的高度 this['scroll' + scrollUpper] -= (lastItem['offset' + scrollUpper] - eleOriginItems[eleOriginItems.length - 1]['offset' + scrollUpper]); } lastScroll = this['scroll' + scrollUpper]; // 移除 selected 元素 let eleItemSelected = this.querySelector('ui-scroll-snap-item[selected]'); if (eleItemSelected) { eleItemSelected.selected = false; } // 还原是否平滑滚动 setTimeout(() => { this.style.scrollBehavior = ''; }, 1); }); if (this.onScrollInited) { this.onScrollInited(eleCurrentItem, eleOriginItems, offset); } this.isAppendItem = true; } // 滚动事件处理 events () { // 元素匹配偏移值 let numOffsetMatch = 5; // 滚动事件执行 this.addEventListener('scroll', () => { clearTimeout(this.timerScroll); this.timerScroll = setTimeout(() => { // 方向 let type = this.type; // 循环滚动不支持双向 // 要么垂直滚动,要么水平滚动 var size = 'height'; var dir = 'top'; if (type.split(/\s/).includes('x')) { size = 'width'; dir = 'left'; } // 容器相关计算点 let boundSnap = this.getBoundingClientRect(); // 100毫秒内滚动事件没触发,认为停止滚动了 // 对列表元素进行位置检测 [].slice.call(this.children).forEach((eleList, index) => { // 对齐方式 let align = eleList.align || this.align; // 容器对比位置 let pointSnap = boundSnap[dir] + boundSnap[size] / 2; if (align == 'start') { pointSnap = boundSnap[dir]; } else if (align == 'end') { pointSnap = boundSnap[dir] + boundSnap[size]; } // 列表的尺寸和定位 let boundList = eleList.getBoundingClientRect(); // 列表元素对比点 let pointList = boundList[dir] + boundList[size] / 2; if (align == 'start') { pointList = boundList[dir]; } else if (align == 'end') { pointList = boundList[dir] + boundSnap[size]; } // 如果和目标距离小于指定偏移 if (Math.abs(pointList - pointSnap) < numOffsetMatch) { // 添加标志类名 eleList.selected = true; // 触发事件 this.dispatchEvent(new CustomEvent('scrollEnd', { detail: { index: index, item: eleList } })); } else { eleList.selected = false; } }); }, 100); }); } attributeChangedCallback (name) { if (name === 'loop' && this.loop) { this.appendItemForScroll(); } } connectedCallback () { this.items = this.querySelectorAll('ui-scroll-snap-item'); if (this.loop) { this.appendItemForScroll(); } } } if (!customElements.get('ui-scroll-snap')) { customElements.define('ui-scroll-snap', uiScrollSnap); } class uiScrollSnapItem extends HTMLElement { static get observedAttributes () { return ['margin', 'align']; } constructor () { super(); this.addEventListener('click', function () { let href = this.href; if (href) { location.href = href; } }); } set align (value) { this.setAttribute('type', value); } get align () { return getComputedStyle(this).scrollSnapAlign || 'center'; } set selected (value) { this.toggleAttribute('selected', value); } get selected () { return this.hasAttribute('selected'); } set disabled (value) { this.toggleAttribute('disabled', value); } get disabled () { return this.hasAttribute('disabled'); } get href () { return this.getAttribute('href'); } set href (value) { this.setAttribute('href', value); } // 选中元素的重定位 position () { const eleSnap = this.snap; // 定位 if (eleSnap && this.selected) { let align = this.align; // 方向 let type = eleSnap.type; // 循环滚动不支持双向 // 要么垂直滚动,要么水平滚动 var size = 'height'; var dir = 'top'; var scrollDir = 'scrollTop'; if (type.split(/\s/).includes('x')) { size = 'width'; dir = 'left'; scrollDir = 'scrollLeft'; } // 定位 let scrollDistance = eleSnap[scrollDir]; // 偏移定位距离 let scrollOffset = 0; // 计算应该偏移的距离 let boundSnap = eleSnap.getBoundingClientRect(); let boundList = this.getBoundingClientRect(); // 默认是居中定位 scrollOffset = (boundSnap[dir] + boundSnap[size] / 2) - (boundList[dir] + boundList[size] / 2); if (align == 'start') { scrollOffset = boundSnap[dir] - boundList[dir]; } else if (align == 'end') { scrollOffset = (boundSnap[dir] + boundSnap[size]) - (boundList[dir] + boundList[size]); } eleSnap[scrollDir] = scrollDistance - scrollOffset; } } connectedCallback () { this.snap = this.closest('ui-scroll-snap'); if (!this.snap) { return this; } this.position(); // 如果有链接行为 if (this.hasAttribute('href')) { this.setAttribute('role', 'link'); } } } if (!customElements.get('ui-scroll-snap-item')) { customElements.define('ui-scroll-snap-item', uiScrollSnapItem); }