UNPKG

xtendui

Version:

Xtend UI is a powerful frontend library of Tailwind CSS components enhanced by vanilla js. It helps you build interfaces with advanced interactions and animations.

587 lines (560 loc) 16.6 kB
/*! * Xtend UI (https://xtendui.github.io/xtendui/) * @copyright (c) 2017-2026 Riccardo Caroli * @license MIT (https://github.com/xtendui/xtendui/blob/master/LICENSE.txt) */ import { Xt } from '../xt.mjs' /** * InfinitescrollInit */ export class InfinitescrollInit { // // init // /** * init */ _init() { const self = this // init self._initVars() self._initLogic() } /** * init vars */ _initVars() { const self = this // options self._optionsDefault = Xt.merge([self.constructor.optionsDefault, Xt.options[self.componentName]]) self._optionsInitial = self.options = Xt.merge([self._optionsDefault, self._optionsCustom]) // vars const options = self.options self.current = Math.floor(options.min / options.perPage) // fake if (!options.get) { self._itemsFake = self.container.querySelector(options.elements.itemsContainer).cloneNode(true) } } /** * init logic */ _initLogic() { const self = this const options = self.options // namespace self.ns = self.ns ?? Xt.uniqueId() // enable first for proper initial activation self.enable() // matches Xt._initMatches({ self, optionsInitial: self._optionsInitial }) // vars self.initial = true // elements self.scrollUp = self.container.querySelectorAll(options.elements.scrollUp) self.scrollDown = self.container.querySelectorAll(options.elements.scrollDown) self.itemsContainer = self.container.querySelector(options.elements.itemsContainer) self.spaceAdditionals = self.container.querySelectorAll(options.elements.spaceAdditional) self.paginations = self.container.querySelectorAll(options.elements.pagination) // events if (options.nocache) { const beforeunloadHandler = Xt.dataStorage.put( window, `beforeunload/${self.ns}`, self._eventBeforeunload.bind(self), ) addEventListener('beforeunload', beforeunloadHandler) } const scrollHandler = Xt.dataStorage.put(window, `scroll/${self.ns}`, self._eventScroll.bind(self)) addEventListener('scroll', scrollHandler) // trigger const events = options.events?.on ? options.events.on.split(' ') : [] if (events.length) { for (const trigger of [...Array.from(self.scrollUp), ...Array.from(self.scrollDown)]) { const triggerHandler = Xt.dataStorage.put( trigger, `${options.events.on}/${self.ns}`, self._eventTrigger.bind(self, { trigger }), ) for (const event of events) { trigger.addEventListener(event, triggerHandler) } } } // initial self._initStart() // setup // dispatch event self.container.dispatchEvent(new CustomEvent(`setup.${self._componentNs}`)) // needs frameDouble after ondone Xt.frameDouble({ el: self.container, func: () => { // initialized class self.container.setAttribute(`data-${self.componentName}-init`, '') // dispatch event self.container.dispatchEvent(new CustomEvent(`init.${self._componentNs}`)) self.initial = false // debug if (options.debug) { // eslint-disable-next-line no-console console.debug(`${self.componentName} init`, self) } }, ns: `${self.ns}Init`, }) // disable last for proper options.disableDeactivate if (self.options.disabled) { self.disable() } } /** * init start */ _initStart() { const self = this const options = self.options // disabled if (self.disabled) { return } // logic self._setCurrent() self._update() self._paginate() self._prefetch() if (self.itemsContainer) { const found = self.itemsContainer.querySelector(options.elements.item) if (found) { found.setAttribute('data-item-first', self.current) } } // resume state if (options.nocache) { const add = self._additionalSpace() const state = history.state if (state && state[`scrollResume${self._componentNs}`]) { // need readyState complete to properly scroll after page load browsers scrolling Xt.ready({ state: 'complete', func: () => { requestAnimationFrame(() => { const found = self.itemsContainer.querySelector(options.elements.item) document.scrollingElement.scrollTop = state[`scrollResume${self._componentNs}`] + found.offsetTop + add }) }, }) } } } // // methods // /** * trigger * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.trigger */ _eventTrigger({ trigger } = {}) { const self = this const options = self.options // disabled if (self.disabled) { return } // request const up = parseFloat(trigger.getAttribute('data-xt-infinitescroll-up')) const down = parseFloat(trigger.getAttribute('data-xt-infinitescroll-down')) const amount = up || down if (!amount) { self._setCurrent({ page: Math.floor(options.min / options.perPage) }) location = self._url.href } else { const current = self._getNext({ amount }) const itemCurrent = self.itemsContainer.querySelector(`[data-item-first="${current}"]`) if (current !== self.current && !itemCurrent) { self._setCurrent({ page: current }) self.inverse = !!up // not if requesting if (!self.container.classList.contains('xt-infinitescroll-loading')) { self.container.classList.add('xt-infinitescroll-loading') Xt.perf({ func: () => { self._request() self._prefetch({ trigger }) }, }) } } } } /** * get next page index * @param {Object} params * @param {Number} params.amount */ _getNext({ amount } = {}) { const self = this const options = self.options // return next index let current = self.current + amount const min = Math.floor(options.min / options.perPage) const max = Math.ceil(options.max / options.perPage) current = current < min ? min : current current = current > max ? max : current return current } /** * unload */ _eventBeforeunload() { const self = this // disabled if (self.disabled) { return } // save scroll position if (self._scrollResume) { const state = {} state[`scrollResume${self._componentNs}`] = self._scrollResume history.replaceState(state, '', self._url.href) } } /** * scroll */ _eventScroll() { const self = this const options = self.options // disabled if (self.disabled || self._loading) { return } // scroll const scrollTop = document.scrollingElement.scrollTop const windowHeight = window.innerHeight // current page const items = Array.from(self.itemsContainer.querySelectorAll('[data-item-first]')).reverse() let found = items[items.length - 1] for (const item of items) { const itemTop = item.getBoundingClientRect().top if (itemTop < windowHeight / 2) { found = item break } } self._setCurrent({ page: parseFloat(found.getAttribute('data-item-first')) }) self._paginate() // save scroll position if (options.nocache) { const add = self._additionalSpace() self._scrollResume = scrollTop - found.offsetTop - add } // replace state const linkOrigin = self._url.origin || `${self._url.protocol}//${self._url.host}` if (linkOrigin === location.origin) { if (self._url.href !== location.href) { history.replaceState(null, '', self._url.href) } } else { console.error('Error: Xt.Infinitescroll cannot set history with different origin', linkOrigin) } // triggers const events = options.events?.on ? options.events.on.split(' ') : [] if (events.length) { if (options.events.scrollUp && self._scrollTopOld > scrollTop) { for (const trigger of self.scrollUp) { const top = trigger.offsetTop if (scrollTop < top) { const triggerHandler = Xt.dataStorage.get(trigger, `${options.events.on}/${self.ns}`) triggerHandler({ target: trigger }) } } } if (options.events.scrollDown && self._scrollTopOld <= scrollTop) { for (const trigger of self.scrollDown) { const top = trigger.offsetTop const bottom = top + trigger.offsetHeight if (scrollTop + windowHeight > bottom) { const triggerHandler = Xt.dataStorage.get(trigger, `${options.events.on}/${self.ns}`) triggerHandler({ target: trigger }) } } } } self._scrollTopOld = scrollTop } /** * request */ _request() { const self = this // request self._loading = true fetch(self._url.href, { method: 'GET', }) .then(response => { if (response.ok) { return response.text() } else { return Promise.reject(response) } }) .then(text => { self._success({ text }) }) .catch(() => { self._error() }) } /** * success * @param {Object} params * @param {String} params.text Html response */ _success({ text } = {}) { const self = this const options = self.options // set substitute const html = document.createElement('html') html.innerHTML = text self.loadedHtml = html const itemsContainer = html.querySelector(options.elements.itemsContainer) if (options.get && itemsContainer) { self._loading = false self._populate({ itemsContainer }) } else { // fake setTimeout(() => { self._loading = false self._populate({ itemsContainer: self._itemsFake.cloneNode(true) }) }, 1000) } } /** * error */ _error() { const self = this // class self._loading = false self.container.classList.remove('xt-infinitescroll-loading') } /** * populate * @param {Object} params * @param {Node|HTMLElement|EventTarget|Window} params.itemsContainer Items element */ _populate({ itemsContainer } = {}) { const self = this const options = self.options // vars const items = itemsContainer.querySelectorAll(options.elements.item) // current page items[0].setAttribute('data-item-first', self.current) Xt.perf({ func: () => { items[0].querySelector('input, select, textarea, button, object, a, area[href], [tabindex]')?.focus() }, }) // populate dom let last const all = self.itemsContainer.querySelectorAll(`${options.elements.item}`) for (const item of items) { if (self.inverse) { const first = all[0] first.before(item) } else { // repeat code querySelectorAll because it always needs to be the last inside loop const all = self.itemsContainer.querySelectorAll(`${options.elements.item}`) last = all[all.length - 1] last.after(item) } } // class self.container.classList.remove('xt-infinitescroll-loading') // update self._update() self._paginate() self._eventScroll() // populate Xt.frame({ el: self.container, ns: `${self.ns}Populate`, func: () => { // dispatch event self.container.dispatchEvent(new CustomEvent(`populate.${self._componentNs}`)) }, }) } /** * paginate */ _paginate() { const self = this const options = self.options // paginate for (const pagination of self.paginations) { if (!pagination.dataset.current || self.current > parseFloat(pagination.dataset.current)) { pagination.dataset.current = self.current pagination.dataset.html = pagination.dataset.html ? pagination.dataset.html : pagination.innerHTML let html = pagination.dataset.html let regex = new RegExp('xt-num', 'ig') if (html.search(regex) !== -1) { let current = self.current * options.perPage current = current > options.max ? options.max : current html = html.replace(regex, current) } regex = new RegExp('xt-tot', 'ig') if (html.search(regex) !== -1) { html = html.replace(regex, options.max) } pagination.innerHTML = Xt.sanitize(html) } } } /** * update */ _update() { const self = this const options = self.options // class if (self.current <= Math.floor(options.min / options.perPage)) { self.container.classList.add('xt-infinitescroll-first') } if (self.current >= Math.ceil(options.max / options.perPage)) { self.container.classList.add('xt-infinitescroll-last') } } // // status // /** * enable */ enable() { const self = this if (self.disabled) { // enable self.disabled = false // dispatch event self.container.dispatchEvent(new CustomEvent(`status.${self._componentNs}`)) } } /** * disable * @param {Object} params * @param {Boolean} params.skipEvent Skip dispatch event */ disable({ skipEvent = false } = {}) { const self = this if (!self.disabled) { // disable self.disabled = true // dispatch event if (!skipEvent) { self.container.dispatchEvent(new CustomEvent(`status.${self._componentNs}`)) } } } // // util // /** * additionalSpace * @return {Number} additionalSpace */ _additionalSpace() { const self = this // logic let add = 0 for (const additional of self.spaceAdditionals) { add += additional.offsetHeight } return add } /** * setCurrent * @param {Object} params * @param {Number} params.page Page number to set */ _setCurrent({ page = null } = {}) { const self = this const options = self.options // check url const url = new URL(location.href) const searchParams = new URLSearchParams(url.search) // set current const get = searchParams.get(options.get) self.current = page !== null ? page : get ? parseFloat(get) : self.current searchParams.set(options.get, self.current) url.search = searchParams.toString() self._url = url } /** * prefetch next page */ _prefetch({ trigger } = {}) { const self = this const options = self.options // loop scroll down if (options.prefetch) { const triggers = trigger ? [trigger] : [...Array.from(self.scrollUp), ...Array.from(self.scrollDown)] for (const trigger of triggers) { const up = parseFloat(trigger.getAttribute('data-xt-infinitescroll-up')) const down = parseFloat(trigger.getAttribute('data-xt-infinitescroll-down')) const amount = up || down // check url const url = new URL(location.href) const searchParams = new URLSearchParams(url.search) // prefetch const next = self._getNext({ amount }) if (self.current !== next) { searchParams.set(options.get, next) url.search = searchParams.toString() const link = document.createElement('link') link.rel = 'prefetch' link.href = url link.as = 'fetch' document.head.appendChild(link) } } } } /** * reinit */ reinit() { const self = this // reinit self._initLogic() } /** * destroy */ destroy() { const self = this const options = self.options // events if (options.nocache) { const beforeunloadHandler = Xt.dataStorage.get(window, `beforeunload/${self.ns}`) removeEventListener('beforeunload', beforeunloadHandler) } const scrollHandler = Xt.dataStorage.get(window, `scroll/${self.ns}`) removeEventListener('scroll', scrollHandler) for (const trigger of [...Array.from(self.scrollUp), ...Array.from(self.scrollDown)]) { const triggerHandler = Xt.dataStorage.get(trigger, `${options.events.on}/${self.ns}`) trigger.removeEventListener(options.events.on, triggerHandler) } // initialized class self.container.removeAttribute(`data-${self.componentName}-init`) // set self Xt._remove({ name: self.componentName, el: self.container }) // dispatch event self.container.dispatchEvent(new CustomEvent(`destroy.${self._componentNs}`)) // delete delete this } }