UNPKG

@rucebee/recycler-vue

Version:

Android RecyclerView port for Vue.js

1,577 lines (1,222 loc) 48.2 kB
import defaults from 'lodash/defaults' import mergeWith from 'lodash/mergeWith' import findIndex from 'lodash/findIndex' import isFunction from 'lodash/isFunction' import noop from 'lodash/noop' import animate from '@rucebee/utils/src/animate' import is_iOS from '@rucebee/utils/src/is_iOS' const mmin = Math.min, mmax = Math.max, mfloor = Math.floor, mround = Math.round, mabs = Math.abs, NEAR_ZERO = .0001, NEAR_ONE = 1 - NEAR_ZERO function beforeCreate() { let el, win, doc, isWindow, isFixed, wrapper, container, source, _itemCount, _getItem, stackFromBottom, stickToTop, emptySlot, hsCache = {} const vm = this, slots = {}, hs = [], hsBinded = [], timeStamp = Date.now() <= new Event('check').timeStamp ? Date.now : performance.now.bind(performance), _scroll = top => { win.scrollTo(doc.scrollLeft, top) scrollTime = timeStamp() //console.log('_scroll', top, scrollTime) }, _windowHeight = () => { const EMPTY_DIV = document.createElement('div') EMPTY_DIV.style.height = '100vh' EMPTY_DIV.style.width = 0 EMPTY_DIV.style.position = 'absolute' document.body.append(EMPTY_DIV) const height = EMPTY_DIV.clientHeight EMPTY_DIV.remove() return height }, _bodyHeight = () => { const EMPTY_DIV = document.createElement('div'), ADD_1PX = el.offsetHeight ? 0 : 1 EMPTY_DIV.style.height = '1px' if (ADD_1PX) el.style.minHeight = '1px' let height = 0 //let stat for (let i = 0; i < doc.childElementCount; i++) { const c = doc.children[i], computedStyle = c.currentStyle || getComputedStyle(c) c.append(EMPTY_DIV) const r = c.getBoundingClientRect(), h = r.bottom - doc.offsetTop + doc.scrollTop + (parseInt(computedStyle.marginBottom) || 0) - 1 - ADD_1PX if (height < h) { height = h //stat = [c, h, r, doc.offsetTop, doc.scrollTop, c.offsetTop, c.offsetHeight] } EMPTY_DIV.remove() } if (ADD_1PX) el.style.minHeight = '' //console.log(...stat) return height } let _clientHeight, updateId, itemCount = 0, maxPosition = -1, position = -1, offset = 0, hsPosition = 0, hsOffset = 0, hsHeight = 0, allShown = true, windowHeight, clientHeight, clientHeightOld, clientHeightEx, scrollHeight = 0, headerHeight = 0, footerHeight = 0, firstHeight = 0, lastHeight = 0, scrollTop, scrollMax, scrollRatio, maxOffset, scrollTime = 0, posId, posResolve, posPosition, posOffset const update = () => { if (updateId) cancelAnimationFrame(updateId) updateId = requestAnimationFrame(updateFrame) }, updateCancel = () => { if (updateId) { cancelAnimationFrame(updateId) updateId = 0 } }, updateNow = () => { updateCancel() updateFrame() } function hsPop(position) { let h if (hsBinded[position]) { h = hsBinded[position] delete hsBinded[position] //console.log('binded', position) return h } let item = _getItem(position), type = item?.type || 'default', hsTypeCache = hsCache[type] if (!hsTypeCache) hsCache[type] = hsTypeCache = [] if (item?.id) { let index = findIndex(hsTypeCache, ['id', item.id]) if (index < 0) index = findIndex(hsTypeCache, ['id', undefined]) if (index > -1) { h = hsTypeCache[index] hsTypeCache.splice(index, 1) } } else { h = hsTypeCache.pop() } if (!h) { const factory = slots[type] || emptySlot h = factory() h.position = position h.$mount() h._isMounted = true if (h.$options.mounted) for (const hook of h.$options.mounted) hook.call(h) for (const c of h.$children) if (c.$options.mounted) for (const hook of c.$options.mounted) hook.call(c) //vm.callHook(vm, 'mounted') //h.$emit = (...args) => vm.$emit.apply(vm, args) h._watcher.active = false h.position = -1 h.position = position h.style = h.$el.style const style = h.style, computedStyle = h.$el.currentStyle || getComputedStyle(h.$el) style.position = 'absolute' style.left = 0 style.right = 0 container.append(h.$el) h.marginTop = parseFloat(computedStyle.marginTop) || 0 h.marginBottom = parseFloat(computedStyle.marginBottom) || 0 style.marginTop = 0 style.marginBottom = 0 if (computedStyle.maxHeight.endsWith('%')) { h.maxHRatio = (parseFloat(computedStyle.maxHeight) || 0) / 100 style.maxHeight = 'initial' } if (computedStyle.minHeight.endsWith('%')) { h.minHRatio = (parseFloat(computedStyle.minHeight) || 0) / 100 style.minHeight = 'initial' } else { h.minHeight = (parseFloat(computedStyle.minHeight) || h.$el.offsetHeight) } //console.log('create', position) } else { if (!h.$el.parentElement) container.append(h.$el) //h.style.display = '' h.position = -1 h.position = position h._update(h._render(), false) //console.log('bind', position, h.source) } h.id = item?.id h.top = position ? 0 : headerHeight if (h.calcHeight) { h.$el.style.height = '' h.height = h.$el.offsetHeight + h.top if (position === maxPosition) h.height += footerHeight let _height = clientHeight for (let i = position - 1; i >= 0; i--) { let _h if (i >= hsPosition && i < hsPosition + hs.length) { _h = hs[i - hsPosition] } else { hsPush(_h = hsPop(i)) } if (_h.calcHeight) break _height -= _h.height if (_height <= h.height) break } if (h.height < _height) { h.$el.style.height = (_height - h.top - footerHeight) + 'px' if (position !== maxPosition) _height -= footerHeight h.height = _height } } else if (h.maxHRatio > 0) { if (h.minHRatio) h.minHeight = h.minHRatio * (windowHeight - headerHeight - footerHeight) - h.marginBottom h.height = mmax(h.minHeight, h.maxHRatio * (windowHeight - headerHeight - footerHeight) - h.marginBottom) + h.top + (position === maxPosition ? footerHeight : 0) //Doing it later: //h.style.height = h.height + 'px' } else { h.height = h.$el.offsetHeight + h.top if (position === maxPosition) h.height += footerHeight } return h } function hsPush(h) { if (h.height) { hsBinded[h.position] = h return } let type = h.type, hsTypeCache = hsCache[type] //h.$el.remove() //h.style.display = 'none' if (!hsTypeCache) { //console.log('hsPush', {hsCache, slots, type, h}) hsCache[type] = hsTypeCache = [] } hsTypeCache.push(h) } function hsFlush() { for (let i in hsBinded) { const h = hsBinded[i] h.height = 0 hsPush(h) } hsBinded.length = 0 for (const type in hsCache) { const hsTypeCache = hsCache[type] for (const h of hsTypeCache) { if (h.$el.parentElement) { //console.log('hsFlush.remove', {'h.type': h.type, 'h.position': h.position}) h.$el.remove() } } } } function hsInvalidate(_position, count) { if (hsPosition >= itemCount) { if (_position <= 1) firstHeight = 0 lastHeight = 0 for (const h of hs) h.height = 0 return true } if (!count) return true if (_position - 1 >= hsPosition && _position - 1 <= hsPosition + hs.length || hsPosition >= _position - 1 && hsPosition <= _position + count + 1) { if (_position <= 1) firstHeight = 0 if (_position + count + 1 >= itemCount) lastHeight = 0 for (let i = mmax(0, _position - 1 - hsPosition), len = mmin(hs.length, _position + count + 1 - hsPosition); i < len; i++) hs[i].height = 0 return true } if (_position + count + 1 >= itemCount && lastHeight) { if (_position <= 1) firstHeight = 0 lastHeight = 0 return true } if (_position <= 1) { firstHeight = 0 return true } } function hsCleanUp() { while (hs.length) hsPush(hs.pop()) hsFlush() for (const type in hsCache) { const hsTypeCache = hsCache[type] for (const h of hsTypeCache) { h.$destroy() } } hsCache = {} } function _scrollMax() { let fluidCount = 0, fluidHeight = 0 for (let h of hs) if (h.maxHRatio) { fluidCount++ fluidHeight += h.height } let height = hs.length === itemCount ? hsHeight : mround(mmin(9 * clientHeight, itemCount === fluidCount ? fluidHeight : (hs.length > fluidCount ? (hsHeight - fluidHeight) * itemCount / (hs.length - fluidCount) + fluidHeight : 2 * clientHeight ) )) //console.log({height, scrollHeight, clientHeight}) if (scrollHeight !== height) { scrollHeight = height wrapper.style.height = (scrollHeight - headerHeight - footerHeight) + 'px' if (isFixed) container.style.width = wrapper.offsetWidth + 'px' else container.style.height = (mmax(scrollHeight, clientHeight) - footerHeight) + 'px' // clientHeight = _clientHeight() // clientHeightEx = mmax(parseInt(doc.style.minHeight) || 0, clientHeight) scrollMax = mmax(0, doc.scrollHeight - clientHeight) // console.log('scrollMax', { // scrollHeight, // doc_scrollHeight: doc.scrollHeight, // clientHeight, // scrollMax, // }) } } function updateFrame() { itemCount = _itemCount() maxPosition = itemCount - 1 while (hs.length) hsPush(hs.pop()) if (!itemCount) { hsFlush() if (!scrolling) vm.$emit('laidout', 0, hs) win.dispatchEvent(scrolledEvent = new Event('scroll')) return } let h, up, down, i if (!firstHeight) { h = hsPop(0) firstHeight = h.height hsPush(h) } if (!lastHeight) { h = hsPop(maxPosition) lastHeight = h.height hsPush(h) } maxOffset = clientHeight - lastHeight scrollTop = doc.scrollTop if (scrolling && !keyboard) { scrolled = Date.now() if (touched) { hs.push(h = hsPop(hsPosition = touchPosition)) hsOffset = touchOffset - scrollTop + touchTop } else { scrollRatio = mmax(0, mmin(1, scrollMax > 0 ? scrollTop / scrollMax : 0)) const positionReal = maxPosition * scrollRatio hs.push(h = hsPop(hsPosition = mfloor(positionReal))) hsOffset = scrollRatio * maxOffset - (positionReal % 1) * h.height } // console.log('scrolling', {hsPosition, hsOffset, scrollTop, scrollMax, touching}) } else if (posId) { hsPosition = posPosition hsOffset = posOffset hs.push(h = hsPop(hsPosition)) } else { if (stackFromBottom) { if (position > maxPosition) { hsPosition = maxPosition hs.push(h = hsPop(hsPosition)) } else { hsPosition = position < 0 ? (stickToTop ? 0 : maxPosition) : position hs.push(h = hsPop(hsPosition)) hsOffset = clientHeight - offset - h.height } } else { if (position > maxPosition) { hsPosition = maxPosition hs.push(h = hsPop(hsPosition)) hsOffset = maxOffset } else { hsPosition = position > -1 ? position : 0 hs.push(h = hsPop(hsPosition)) hsOffset = offset } } // console.log({'-> hsPosition': hsPosition, hsOffset, position, offset, clientHeight}) } up = hsOffset down = up + h.height i = hsPosition while (i-- > 0 && up > 0) { hs.unshift(h = hsPop(i)) up -= h.height } i = hsPosition if (hs.length > 1) hsPosition -= hs.length - 1 hsHeight = down - up hsOffset = up while (++i < itemCount && (down < clientHeight || hsHeight < clientHeightEx)) { hs.push(h = hsPop(i)) down += h.height hsHeight += h.height } const bottomSpace = clientHeight - down //if (!scrolling && !scrolled && bottomSpace > 0) { if (bottomSpace > 0) { i = hsPosition; while (i-- > 0 && up > -bottomSpace) { hs.unshift(h = hsPop(i)) up -= h.height hsHeight += h.height hsPosition-- } hsOffset = up } allShown = hs.length === itemCount && hsHeight < clientHeightEx + 1 //if (!scrolling && !scrolled) { if (!scrolling) { if (stackFromBottom) { if (bottomSpace > 0) { up += bottomSpace hsOffset = up } else if (!hsPosition && hsOffset > 0) { up -= mmin(-bottomSpace, hsOffset) hsOffset = up } } else if (!hsPosition) { if (hsOffset >= 0) { up = 0 hsOffset = 0 } else if (bottomSpace > 0) { up += mmin(bottomSpace, -hsOffset) hsOffset = up } } } let clearScrolled = true if (!keyboard) { if (allShown) { if (!scrolling) { _scrollMax() if (!stackFromBottom || stickToTop) { if (Math.abs(scrollTop + hsOffset) >= 1) _scroll(scrollTop = -hsOffset) } } else { hsOffset = -mround(scrollMax * hsOffset / (clientHeight - hsHeight)) } if (!stackFromBottom || stickToTop) { up = -scrollTop } } else if (!scrolling) { const scrollClue = scrollTop < .5 ? -1 : (scrollTop + .5 > scrollMax ? 1 : 0) _scrollMax() // scrollRatio = scrollTop / scrollTopMax // positionReal = maxPosition * scrollRatio // hsPosition = mfloor(positionReal) // // offsetRatio = positionReal % 1 // = maxPosition * scrollTop / scrollTopMax - hsPosition // 0 <= offsetRatio < 1 // // hsOffset = scrollRatio * maxOffset - (positionReal % 1) * h.height // // scrollTop = (hsOffset - hsPosition * h.height) / (maxOffset - maxPosition * h.height) * scrollTopMax let newScrollTop = scrollTop, hOffset = hsOffset, delta = Number.MAX_VALUE, offsetRatio = 1 //const stat = [] for (let i = 0; i < hs.length; i++) { const hh = hs[i].height, _scrollTop = scrollMax * (hOffset - (hsPosition + i) * hh) / (maxOffset - maxPosition * hh), _offsetRatio = maxPosition * _scrollTop / scrollMax - (hsPosition + i) if (scrolled) { const _delta = mabs(scrollTop - _scrollTop) //stat.push({'#': hsPosition + i, _scrollTop, _offsetRatio, _delta}) if (_offsetRatio > -NEAR_ZERO && _offsetRatio < 1 && _delta < delta) { delta = _delta newScrollTop = _scrollTop //break } } else { //stat.push({'#': hsPosition + i, _scrollTop, _offsetRatio}) if (_offsetRatio > -NEAR_ZERO && _offsetRatio < offsetRatio) { offsetRatio = _offsetRatio newScrollTop = _scrollTop //break } } hOffset += hh } const _scrollRatio = mmax(0, mmin(1, scrollMax > 0 ? newScrollTop / scrollMax : 0)), _positionReal = maxPosition * _scrollRatio, _hsPosition = mfloor(_positionReal) // if (_hsPosition >= hsPosition && _hsPosition < hsPosition + hs.length) { // const _h = hs[_hsPosition - hsPosition], // _hsOffset = _scrollRatio * maxOffset - (_positionReal % 1) * _h.height // // let a = '' // // if (_hsPosition - 1 >= hsPosition) // a += (_hsPosition - 1) + ' - ' + (_hsOffset - hs[_hsPosition - hsPosition - 1].height) + ', ' // // a += _hsPosition + ' - ' + _hsOffset + ', ' // // if (_hsPosition + 1 < hsPosition + hs.length) // a += (_hsPosition + 1) + ' - ' + (_hsOffset + hs[_hsPosition - hsPosition].height) + ', ' // // console.log({hsPosition, hsOffset, a}) // } else { // console.log({hsPosition, _hsPosition, hsOffset}) // // } //console.log(stat) if (scrolled && touched) { let scrollDelta = 0 //console.log({scrollClue, scrollTop, scrollMax}) if (scrollClue < 0) { scrollDelta = !hsPosition ? hsOffset : -newScrollTop //console.log({scrollDelta}) _scroll(scrollTop = newScrollTop = 0) } else if (scrollClue > 0) { const hsOffsetOld = hsOffset //hsOffset -= clientHeight - clientHeightOld scrollDelta = hsPosition + hs.length - 1 === maxPosition ? hsOffset - (clientHeight - hsHeight) : scrollMax - newScrollTop //console.log({scrollDelta, delta: clientHeight - clientHeightOld, hsOffset, hsOffsetOld}) _scroll(scrollTop = newScrollTop = scrollMax) } if (mabs(scrollDelta) >= 1) { clearScrolled = false const r = mmax(0, mmin(1, (Date.now() - scrolled) / 2000)), hsOffsetOld = hsOffset hsOffset -= r * scrollDelta //console.log({r, hsOffsetOld, hsOffset}) update() } else { if (scrollClue < 0) { if (!hsPosition) { hsOffset = 0 } } else if (scrollClue > 0) { if (hsPosition + hs.length - 1 === maxPosition) { hsOffset = clientHeight - hsHeight } } } } if (mabs(scrollTop - newScrollTop) >= 1) { //console.log('adjustScroll', {scrollTop, newScrollTop}) _scroll(scrollTop = newScrollTop) } } } if (!scrolling) clientHeightOld = clientHeight // console.log(keyboardAnchor.getBoundingClientRect().bottom, clientHeight) const scrollOffset = (isFixed ? 0 : ( keyboard ? scrollTop + keyboardAnchor.getBoundingClientRect().bottom - clientHeight : scrollTop )) down = up = hsOffset //down = hsOffset + hsHeight let j = 0, fluidCheck = 0 while (j < hs.length) { if (down > clientHeight - footerHeight) { h = hs.pop() hsHeight -= h.height hsPush(h) continue } h = hs[j] //h.style.zIndex = j //h.style.order = j if (h.maxHRatio) { let top = down, height = h.height - h.top if (hsPosition + j === maxPosition) height -= footerHeight if (!allShown) { if (j === 0 && down + h.top < 0) { top = down + mmin(height - h.minHeight, -down - h.top) height -= top - down } if (j === hs.length - 1 && down + height > clientHeight) height = mmax(h.minHeight, windowHeight - headerHeight - footerHeight - down) } h.style.top = scrollOffset + top + h.top + 'px' h.style.height = height + 'px' fluidCheck = fluidCheck | 2 } else { h.style.top = scrollOffset + down + h.top + 'px' fluidCheck = fluidCheck | 1 } down += h.height if (down < headerHeight) { hsPosition++ hsOffset += h.height hsHeight -= h.height hs.splice(j, 1) hsPush(h) } else { j++ } } if (scrolling && touched) { touchPosition = hsPosition touchOffset = hsOffset touchTop = scrollTop } hsFlush() if (stackFromBottom) { position = hsPosition + hs.length - 1 offset = clientHeight - hsHeight - hsOffset if (fluidCheck === 3) { for (i = hs.length - 1; i >= 0; i--) { if (hs[i].maxHRatio) { position-- offset += hs[i].height } else break } } else if (fluidCheck === 2 && hs.length === itemCount) { position = -1 } } else { position = hsPosition offset = hsOffset if (fluidCheck === 3) { for (h of hs) { if (h.maxHRatio) { position++ offset += h.height } else break } } else if (fluidCheck === 2 && hs.length === itemCount) { position = -1 } } // console.log({ // '<- hsPosition': hsPosition, // hsOffset, // position, // offset, // clientHeight, // }) if (!scrolling && clearScrolled) { scrolled = 0 vm.$emit('laidout', hsPosition, hs) } if (scrolling) win.dispatchEvent(scrolledEvent = new Event('scroll')) vm.$emit('scrolled', hsPosition, hs) } let scrolled = 0, scrolling = false, scrolledEvent = null, scrollStarted = 0, scrollEndTimeout, touched = false, touching = false, touchTop, touchPosition, touchOffset, touchEnd = 0, keyboard = false, keyboardAnchor const onResize = () => { hsInvalidate(0, itemCount) clientHeight = _clientHeight() windowHeight = isWindow ? _windowHeight() : clientHeight clientHeightEx = mmax(parseInt(doc.style.minHeight) || 0, clientHeight) scrollMax = mmax(0, doc.scrollHeight - clientHeight) headerHeight = wrapper.offsetTop - doc.offsetTop if (isFixed) container.style.width = wrapper.offsetWidth + 'px' else container.style.top = -headerHeight + 'px' const bodyHeight = _bodyHeight() footerHeight = bodyHeight - headerHeight - el.offsetHeight // console.log('resize', { // clientHeight, // clientHeightEx, // scrollMax, // headerHeight, // bodyHeight, // elHeight: el.offsetHeight, // footerHeight // }) update() }, onScroll = ev => { if (scrolledEvent === ev) { scrolledEvent = null return } // console.log('onScroll', { // type: ev.type, // timeStamp: ev.timeStamp, // scrollTime, // scrollStarted, // scrollTop, // scrollMax, // }) ev.cancelBubble = true if (ev.timeStamp > scrollTime + 99) onScrollContinue(ev) if (scrollStarted) { scrolling = true update() } }, onScrollContinue = ev => { //console.log('onScrollContinue', ev?.type) if (!scrollStarted && (keyboard || touching || !touched)) scrollStarted = 1 if (scrollStarted > 0 && ev && (ev.type === 'mousedown' || ev.type === 'touchstart')) { scrollStarted = -1 //win.addEventListener('mousemove', onScrollContinue) addEventListener('mouseup', onScrollEnd) addEventListener('touchend', onScrollEnd) } if (scrollEndTimeout) clearTimeout(scrollEndTimeout) if (scrollStarted > 0) scrollEndTimeout = setTimeout(onScrollEnd, 500) }, onScrollEnd = ev => { //console.log('onScrollEnd', ev?.type) if (scrollStarted < 0) { removeEventListener('mouseup', onScrollEnd) removeEventListener('touchend', onScrollEnd) //win.removeEventListener('mousemove', onScrollContinue) } scrollStarted = 0 if (scrollEndTimeout) { clearTimeout(scrollEndTimeout) scrollEndTimeout = 0 } if (scrolling) { scrolling = false //console.log('onScrollEnd', ev, scrollTop, scrollMax) update() } }, onTouchStart = ev => { //console.log('onTouchStart', ev?.type, allShown) if (allShown) { onScrollContinue(ev) return } //touchEnd = 0 touchTop = doc.scrollTop touchPosition = hsPosition touchOffset = hsOffset win.addEventListener('touchend', onTouchEnd) touched = true touching = true }, onTouchEnd = ev => { //console.log('onTouchEnd', ev?.type) win.removeEventListener('touchend', onTouchEnd) // const scrollTop = doc.scrollTop // if (scrollTop < 0) { // touchEnd = scrollTop // } else if (scrollTop > scrollMax) { // touchEnd = scrollTop - scrollMax // } else { // touchEnd = 0 // } //console.log('onTouchEnd', {touchEnd, scrollTop, scrollMax}) if (touching) { touching = false update() } }, onKeyboardFocus = ev => { console.log('onKeyboardFocus', ev.target.nodeName, document.activeElement?.nodeName) // , { // position, // offset, // hsPosition, // hsOffset, // }, keyboardAnchor, keyboardAnchor.getBoundingClientRect()) keyboard = ev.target.nodeName === document.activeElement?.nodeName //doc.style.position = keyboard ? 'fixed' : '' update() }, onKeyboardFocusOut = ev => { console.log('onKeyboardFocusOut', ev.target.nodeName, document.activeElement?.nodeName) if (['TEXTAREA', 'INPUT'].indexOf(ev.target.nodeName) < 0 && ['TEXTAREA', 'INPUT'].indexOf(document.activeElement?.nodeName) < 0) { keyboard = false //doc.style.position = keyboard ? 'fixed' : '' update() } }, onKeyboardBlur = ev => { console.log('onKeyboardBlur', ev.target.nodeName, document.activeElement?.nodeName) keyboard = false //doc.style.position = keyboard ? 'fixed' : '' update() }, onKeyboardTouch = ev => { const _keyboard = ['TEXTAREA', 'INPUT'].indexOf(ev.target.nodeName) > -1 || ['TEXTAREA', 'INPUT'].indexOf(document.activeElement?.nodeName) > -1 if (keyboard !== _keyboard) { console.log('onKeyboardTouch', ev.target.nodeName, document.activeElement?.nodeName) keyboard = _keyboard //doc.style.position = keyboard ? 'fixed' : '' update() } } function created() { source = this.source _itemCount = source.itemCount _getItem = source.getItem stackFromBottom = this.stackFromBottom stickToTop = this.stickToTop itemCount = _itemCount() maxPosition = itemCount - 1 } function mounted() { const Vue = this.$root.__proto__.constructor el = this.$el win = el.closest('.recycler-layout')?.parentElement ?? el.closest('.recycler-window') ?? window isWindow = win === window isFixed = false//isWindow doc = isWindow ? document.documentElement : win _clientHeight = isWindow ? () => win.innerHeight : () => doc.clientHeight wrapper = el.children[0] container = wrapper.children[0] if (isFixed) { container.style.position = 'fixed' container.style.top = 0 //container.style.left = 0 //container.style.right = 0 container.style.bottom = 0 } addEventListener('resize', onResize) win.addEventListener('scroll', onScroll, true) win.addEventListener('wheel', onScrollContinue, true) win.addEventListener('mousedown', onScrollContinue, true) win.addEventListener('touchstart', onTouchStart, true) if (is_iOS()) { addEventListener('focus', onKeyboardFocus, true) addEventListener('blur', onKeyboardBlur, true) addEventListener('touchstart', onKeyboardTouch, true) addEventListener('focusout', onKeyboardFocusOut, true) keyboardAnchor = document.createElement('div') keyboardAnchor.style.position = 'fixed' keyboardAnchor.style.bottom = 0 keyboardAnchor.style.height = '1px' document.body.append(keyboardAnchor) } onResize() clientHeightOld = clientHeight //onScrollContinue() //win.dispatchEvent(new Event('scroll')) win.recycler = this loop: for (const type in vm.$slots) for (const vnode of vm.$slots[type]) { if (!vnode || !vnode.tag) continue; ((type, vnode) => { const Ctor = vnode.componentOptions ? vnode.componentOptions.Ctor : Vue.extend({ render(h) { return vm.$slots[type] } }), oldOptions = Ctor.options, options = Object.assign({}, oldOptions), dataFn = options.data options.data = function () { const data = dataFn ? dataFn.call(this) : {} data.position = -1 return data } options.computed = defaults({ type: () => type, source: () => source, item() { return _getItem(this.position) } }, oldOptions.computed) delete options.computed.position if (vnode.componentOptions) { slots[type] = () => { Ctor.options = options const o = new Ctor({ _isComponent: true, _parentVnode: vnode, parent: vm }) Ctor.options = oldOptions return o } } else { slots[type] = () => { Ctor.options = options const o = new Ctor({ // _isComponent: true, // _parentVnode: vnode, parent: vm }) Ctor.options = oldOptions return o } } })(type, vnode) continue loop } emptySlot = () => new Vue({ render: h => h('div') }) //console.log(slots, emptySlot) source.attach(vm) } function beforeDestroy() { source.detach(vm) if (win.recycler === this) delete win.recycler removeEventListener('resize', onResize) win.removeEventListener('scroll', onScroll, true) win.removeEventListener('wheel', onScrollContinue, true) win.removeEventListener('mousedown', onScrollContinue, true) win.removeEventListener('mousemove', onScrollContinue, true) removeEventListener('focus', onKeyboardFocus, true) removeEventListener('blur', onKeyboardBlur, true) removeEventListener('touchstart', onKeyboardTouch, true) removeEventListener('focusout', onKeyboardFocusOut, true) if (keyboardAnchor) keyboardAnchor.remove() if (keyboard) { keyboard = false //doc.style.position = keyboard ? 'fixed' : '' } scrolling = false onScrollEnd() win.removeEventListener('touchstart', onTouchStart, true) win.removeEventListener('touchend', onTouchEnd, true) touched = false touching = false onTouchEnd() updateCancel() // for (const h of hs) hsPush(h) // hs.length = 0 // hsBinded.length = 0 // // for (const key in hsCache) delete hsCache[key] // hsCleanUp() firstHeight = 0 lastHeight = 0 scrollHeight = 0 } mergeWith(this.$options, {created, mounted, beforeDestroy}, (objValue, srcValue) => Array.isArray(objValue) ? objValue.concat([srcValue]) : (objValue ? undefined : [srcValue])) this.$options.watch = defaults({ source(newValue) { if (source !== newValue) { source.detach(vm) source = newValue source.attach(vm) _itemCount = source.itemCount _getItem = source.getItem this.onDatasetChanged() hsCleanUp() } }, stackFromBottom(newValue) { stackFromBottom = newValue this.onDatasetChanged() }, stickToTop(newValue) { stickToTop = newValue this.onDatasetChanged() }, }, this.$options.watch) this.$options.methods = defaults({ onDatasetChanged() { //console.log('update', {hsPosition, position, _position, count}) if (hsInvalidate(0, _itemCount())) update() }, onUpdate(_position, count) { //console.log('update', {hsPosition, position, _position, count}) if (hsInvalidate(_position, count)) update() }, onInsert(_position, count) { // console.log('insert', { // hsPosition, // position, // offset, // _position, // count, // stackFromBottom, // item: _getItem(_position) // }) if (position === -1) { if (stackFromBottom) { position = maxPosition + count offset = 0 } else { position = 0 offset = 0 } } else if (stackFromBottom && _position >= maxPosition && position === maxPosition && -offset < (footerHeight + lastHeight) / 2 ) { position = maxPosition + count offset = 0 } else { if (_position < position) position += count } if (touched && _position < touchPosition) touchPosition += count if (posId && _position < posPosition) posPosition += count for (let i = mmax(0, _position - hsPosition); i < hs.length; i++) hs[i].position += count if (hsInvalidate(_position, count)) update() // console.log('inserted', {hsPosition, position, offset, item: _getItem(_position)}) }, onRemove(_position, count) { //console.log('remove', {hsPosition, position, _position, count, offset, stackFromBottom}) const invalid = hsInvalidate(_position, count) if (!_itemCount()) position = -1 else { if (_position < position) position -= mmin(count, position - _position + 1) if (touched && _position <= touchPosition) touchPosition -= mmin(count, touchPosition - _position + 1) if (posId && _position <= posPosition) posPosition -= mmin(count, posPosition - _position + 1) } if (invalid) { for (let i = mmax(0, _position + count - hsPosition); i < hs.length; i++) hs[i].position -= count // for (let i = mmax(0, _position - hsPosition), len = mmin(hs.length, _position - hsPosition + count); i < len; i++) { // const [h] = hs.splice(i, 1) // len-- // i-- // // //hsHeight -= h.height // h.height = 0 // hsPush(h) // //h.$el.remove() // } update() } //console.log('removed', {hsPosition, position, _position, count, offset, stackFromBottom}) }, update, updateNow, setStackFromBottom(_stackFromBottom) { if (stackFromBottom !== _stackFromBottom) { updateNow() stackFromBottom = _stackFromBottom let allFluid = true if (stackFromBottom) { position = hsPosition + hs.length - 1 offset = clientHeight - hsHeight - hsOffset for (let i = hs.length - 1; i >= 0; i--) { if (hs[i].maxHRatio) { position-- offset += hs[i].height } else { allFluid = false break } } } else { position = hsPosition offset = hsOffset for (let h of hs) { if (h.maxHRatio) { position++ offset += h.height } else { allFluid = false break } } } if (allFluid && hs.length === itemCount) { position = -1 } } }, position(_position, _offset = 0) { if (_position === undefined) return [ position, stackFromBottom ? position !== maxPosition ? offset - footerHeight : offset : position ? offset - headerHeight : offset ] position = _position < 0 ? itemCount + _position : _position ?? 0 offset = stackFromBottom ? position !== maxPosition ? _offset + footerHeight : _offset : position ? _offset + headerHeight : _offset console.log('position', {position, offset}) update() }, positionFromTop(_position, _offset) { if (stackFromBottom) { vm.setStackFromBottom(false) vm.position(_position, _offset) vm.setStackFromBottom(true) } else { vm.position(_position, _offset) } }, positionSmooth(positionFn, _offset = 0, _stackFromBottom = stackFromBottom) { if (posId) cancelAnimationFrame(posId) if (posResolve) posResolve() let prevPosition, prevOffset return new Promise(resolve => { posResolve = resolve const next = () => { posId = null if (scrolling || prevPosition === hsPosition && mabs(prevOffset - hsOffset) < 2) { posResolve = null resolve() return } prevPosition = hsPosition prevOffset = hsOffset posPosition = hsPosition posOffset = hsOffset const positionDelta = 48, //positionDelta = (clientHeight - headerHeight - footerHeight) / 8, _position = isFunction(positionFn) ? positionFn() : positionFn if (_position < 0 && isFunction(positionFn)) { posResolve = null resolve() return } if (_position >= hsPosition && _position < hsPosition + hs.length) { let offset = _offset if (_stackFromBottom) { const h = hs[_position - hsPosition] offset = clientHeight - (_position !== maxPosition ? _offset + footerHeight : _offset) - h.height } else { offset = _position ? _offset + headerHeight : _offset } let hOffset = hsOffset for (let i = 0; i < _position - hsPosition; i++) hOffset += hs[i].height const delta = offset - hOffset if (mabs(delta) < 2) { posResolve = null resolve() return } posOffset = hsOffset + mmin(mabs(delta), positionDelta) * (delta < 0 ? -1 : 1) } else { if (_position < hsPosition) { posOffset = hsOffset + positionDelta } else { posPosition = hsPosition + hs.length - 1 posOffset = hsOffset + hsHeight - hs[hs.length - 1].height - positionDelta } } update() posId = requestAnimationFrame(next) } next() }) }, startPosition() { return hsPosition }, startOffset() { return hsOffset }, startPart() { return hs.length ? hsOffset + hs[0].height - footerHeight : 0 }, endPosition() { return hsPosition + hs.length - 1 }, // positionReal = maxPosition * scrollRatio = 1 // maxPosition * scrollTop / scrollTopMax = 1 // scrollTop = scrollTopMax / maxPosition scrollTop: top => { if (top !== undefined) { position = 0 offset = stackFromBottom ? clientHeight + top - firstHeight : -top //console.log('scrollTop ->', {position, top, hsOffset}) update() return } const _scrollTop = (scrolling || scrolled) && touched ? doc.scrollTop : (hsPosition ? firstHeight + (doc.scrollTop - scrollMax / maxPosition) / (scrollMax - scrollMax / maxPosition) * (scrollMax - firstHeight) : -hsOffset) //console.log('scrollTop <-', {scrolling, scrolled, _scrollTop}) return _scrollTop }, }, this.$options.methods) } export default { props: { source: { type: Object, required: true }, stackFromBottom: Boolean, stickToTop: Boolean, }, // watch: { // source(newValue, value) { // console.log(newValue, value) // } // }, render(h) { return h('div', { attrs: { class: 'recycler', } }, [h('div', { attrs: { style: 'position:relative;', } }, [h('div', { attrs: { class: 'recycler-items', style: 'position:relative;overflow:hidden;', } })])] ) }, beforeCreate }