UNPKG

bona

Version:

Super lightweight microframework focused on static websites and landing pages.

207 lines (180 loc) 7.54 kB
import Component from '../component'; import {checkAnchorEligibility, replaceNodesBySelectors} from '../utils'; /** * @typedef {Object} BonaAjaxProps * @property {boolean} [bindLinks] Handle all links on the page. * @property {boolean} [bindHistory] Handle history changes. * @property {RegExp} [checkLinkUrlRegExp] RegExp to check link URL eligibility. * @property {boolean} [restoreScrollHistory] Restore the window scroll after the history change. * @property {string} [scrollRestoration] Scroll restoration behavior on history navigation. * * @typedef {Object} BonaReqOptions * @property {boolean} [checkResponseStatus] Check the response status and redirect if an error occurs. * @property {string|boolean} [history] Push or replace history. * @property {object} [historyState] Pass history state object. * @property {boolean} [preventSame] Prevent goTo to the same URLs. * @property {boolean} [preventHash] Prevent goTo to hashes. * @property {boolean} [preventRunning] Prevent goTo when a transition is already running. * @property {DOMParserSupportedType} [parserType] Specifies the parser type used to parse the DOM. * @property {string[]} [updateSelectors] Selectors to update. * @property {boolean} [extendNodes] Extend or replace new nodes. * @property {boolean} [removeNodes] Remove non-existent nodes. * @property {boolean} [detachNodes] Detach non-existent nodes from store. * @property {boolean} [resetScroll] Reset the window scroll after an update. * @property {boolean} [restoreScroll] Restore the window scroll after an update. * @property {boolean} [scrollToHash] Scroll to hashes after an update. * @property {boolean} [fireLeave] Fire leave hook. * @property {boolean} [fireLoaded] Fire loaded hook. * @property {boolean} [fireRefresh] Fire refresh ho.ok. * @property {boolean} [fireEnter] Fire enter hook. * @property {boolean} [fireComplete] Fire complete hook. * @property {boolean} [fireDestroy] Fire destroy hook. * @property {object} [fetch] Fetch options. * * @typedef {BonaAjaxProps & BonaReqOptions} BonaAjaxOptions */ export default class Ajax extends Component { constructor() { super(...arguments); /** @type {BonaAjaxOptions} **/ this.options = { bindLinks: true, bindHistory: true, checkLinkUrlRegExp: /(\?.*)?\/(?:|[^.]+(?:\.(?:htm|html|php)|))(?:\?.*|)$/, // regex101.com/r/k4iNu1/1 checkResponseStatus: true, history: 'push', historyState: {}, preventSame: false, preventHash: true, preventRunning: false, parserType: 'text/html', scrollRestoration: 'manual', updateSelectors: ['title', 'meta', '#view-main'], extendNodes: false, removeNodes: true, detachNodes: true, resetScroll: true, restoreScroll: false, restoreScrollHistory: true, scrollToHash: true, fireLeave: true, fireLoaded: true, fireRefresh: true, fireEnter: true, fireComplete: true, fireDestroy: true, fetch: {}, ...this.options, }; this.event = {}; this.scroll = {}; this.parser = new DOMParser(); this.running = false; this.url = new URL(window.location.href); this.prevUrl = null; if (this.options.scrollRestoration) window.history.scrollRestoration = this.options.scrollRestoration; if (this.options.bindLinks) this.bindLinks(); if (this.options.bindHistory) this.bindHistory(); } /** * Handle all links on the page. */ bindLinks() { document.addEventListener('click', (e) => { if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return; for (let target = e.target; target && target !== document; target = target.parentNode) { if (!checkAnchorEligibility(target, this.options.checkLinkUrlRegExp)) continue; e.preventDefault(); this.goTo(target.href); break; } }); } /** * Handle history change. */ bindHistory() { this.event.popstate = () => { this.goTo(window.location.href, { history: false, preventRunning: false, restoreScroll: this.options.restoreScrollHistory, }); }; window.addEventListener('popstate', this.event.popstate); } /** * Push or replace url in history. * * @param {string} url Url. * @param {string} [method] Push or replace. * @param {object} [state] History state object. */ pushHistory(url, method = 'push', state = {}) { window.history[method === 'push' ? 'pushState' : 'replaceState'](state, '', url); } /** * Go to a new location. * * @param {string} url URL. * @param {BonaReqOptions} [options] Request options. * @return {Promise} */ async goTo(url, options) { options = {...this.options, ...options}; if (options.preventRunning && this.running) { return false; } this.prevUrl = this.url; this.url = new URL(url, window.location.origin); if (options.preventSame && this.url.href === this.prevUrl.href) { return false; } if (options.preventHash && this.url.hash !== this.prevUrl.hash) { const cleanUrl = this.url.href.split('#')[0]; const cleanPrevUrl = this.prevUrl.href.split('#')[0]; if (cleanUrl === cleanPrevUrl) { return false; } } this.scroll[this.prevUrl.href] = {top: window.scrollY, left: window.scrollX}; this.running = true; if (options.history) this.pushHistory(this.url.href, options.history, options.historyState); const data = await Promise.all([ this.executeRequest(url, options), options.fireLeave ? this.app.executeAll('leave') : null, ]); if (options.fireLoaded) await this.app.executeAll('loaded'); if (options.updateSelectors) { replaceNodesBySelectors(options.updateSelectors, data[0], options.removeNodes, options.extendNodes); } if (options.resetScroll) { window.scrollTo(0, 0); } await this.app.refresh(options.fireRefresh, options.fireDestroy, options.detachNodes); if (options.restoreScroll && this.scroll[this.url.href]) { window.scrollTo(this.scroll[this.url.href]); } else if (options.scrollToHash && this.url.hash && this.app.query(this.url.hash)) { this.app.query(this.url.hash)?.scrollIntoView(); } if (options.fireEnter) await this.app.executeAll('enter'); if (options.fireComplete) await this.app.executeAll('complete'); this.running = false; } /** * Execute prepared request. * * @param {string} url Url. * @param {BonaReqOptions} [options] Request options. * @return {Promise} */ async executeRequest(url, options) { const req = await fetch(url, options.fetch); if (options.checkResponseStatus && !req.ok) { window.location.assign(url); return; } const data = await req.text(); return this.parser.parseFromString(data, options.parserType); } }