From a232ebacdb5c6950b9e5575d7194d5a9503d41d3 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:47:52 +0100 Subject: [PATCH 01/60] Update .babelrc --- .babelrc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.babelrc b/.babelrc index 75cc0be..0ab27a6 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,16 @@ { - "presets": [["@babel/preset-env"]], + "presets": [ + [ + "@babel/preset-env", + { + "debug": true + } + ] + ], "sourceMaps": true, "retainLines": true, - "plugins": ["@babel/plugin-syntax-dynamic-import"] + "plugins": [ + "@babel/plugin-transform-runtime", + "@babel/plugin-syntax-dynamic-import" + ] } From 9c6bef41f84653881bc5c67c9b592c55518562c2 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:47:55 +0100 Subject: [PATCH 02/60] Delete .browserslistrc --- .browserslistrc | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .browserslistrc diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index 9dac89b..0000000 --- a/.browserslistrc +++ /dev/null @@ -1,13 +0,0 @@ ->= 0.5% -last 2 major versions -not dead -Chrome >= 60 -Firefox >= 60 -# needed since Legacy Edge still has usage; 79 was the first Chromium Edge version -# should be removed in the future when its usage drops or when it's moved to dead browsers -not Edge < 79 -Firefox ESR -iOS >= 10 -Safari >= 10 -Android >= 6 -not Explorer <= 11 From f747ff81d57c3e4b0297cae64603f9d5461fa4da Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:48:00 +0100 Subject: [PATCH 03/60] Create .npmignore --- .npmignore | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ffdb4b3 --- /dev/null +++ b/.npmignore @@ -0,0 +1,20 @@ +# DIRECTORIES +# -------------------- +.vscode/ +node_modules/ +src/ +demo/ +tests/ +types/store.ts + +# FILES +# ------------------- +.babelrc +.browserslistrc +.eslintignore +.gitignore +.prettierignore +rollup.config.js +tsconfig.json +changelog.md +readme.md From 376915eeee05e97817e1110b5643f03edf90554f Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:48:03 +0100 Subject: [PATCH 04/60] Create .prettierrc.js --- .prettierrc.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .prettierrc.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..a76f469 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,25 @@ +module.exports = { + overrides: [ + { + files: ['*.json', '.liquidrc', '.scss.liquid', '*.md'], + options: { + parser: 'json', + arrowParens: 'avoid', + bracketSpacing: true, + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxBracketSameLine: false, + jsxSingleQuote: false, + printWidth: 80, + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: false, + tabWidth: 2, + trailingComma: 'none', + useTabs: false + } + } + ] +} From 5e1408a1f49e03892103af4c322497f18781e8aa Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:48:16 +0100 Subject: [PATCH 05/60] Update package.json --- package.json | 56 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 9123dc8..69ad864 100644 --- a/package.json +++ b/package.json @@ -6,16 +6,20 @@ "author": "ΝΙΚΟΛΑΣ ΣΑΒΒΙΔΗΣ", "owner": "BRIXTOL TEXTILES", "license": "MIT", - "types": "types/pjax.d.ts", - "main": "package/pjax.min.js", - "module": "package/pjax.esm.js", - "browser": "package/pjax.esm.min.js", + "type": "module", + "types": "./types/index.d.ts", + "main": "./package/pjax.esm.js", + "module": "./package/pjax.esm.js", + "browser": "./package/pjax.umd.js", "scripts": { "dev": "rollup -c -w", - "build": "rollup -c --environment prod", + "build": "rollup -c --environment es5,prod", "deploy": "pnpm run build && netlify deploy -p", "test": "ava --color --verbose --watch --timeout=2m" }, + "browserslist": [ + "extends @brixtol/browserslist-config" + ], "prettier": "@brixtol/prettier-config", "eslintConfig": { "extends": [ @@ -34,28 +38,44 @@ "cjs": true }, "dependencies": { - "mergerino": "^0.4.0" + "@babel/runtime": "^7.13.10", + "custom-event-polyfill": "^1.0.7", + "element-closest-polyfill": "^1.0.2", + "history": "^5.0.0", + "intersection-observer": "^0.12.0", + "mdn-polyfills": "^5.20.0", + "mergerino": "^0.4.0", + "nanoid": "^3.1.21", + "regexp.prototype.match": "^0.1.0", + "url-polyfill": "^1.1.12" }, "devDependencies": { - "@babel/core": "^7.12.3", - "@babel/preset-env": "^7.12.1", + "@babel/core": "^7.13.10", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-property-mutators": "^7.12.13", + "@babel/plugin-transform-runtime": "^7.13.10", + "@babel/preset-env": "^7.13.10", + "@babel/runtime-corejs3": "^7.13.10", + "@brixtol/browserslist-config": "workspace:^1.0.3", "@brixtol/eslint-config-javascript": "workspace:^2.0.1", "@brixtol/prettier-config": "workspace:^1.0.3", "@brixtol/rollup-utils": "workspace:^0.1.0", - "@rollup/plugin-alias": "^3.1.1", - "@rollup/plugin-babel": "^5.2.1", - "@rollup/plugin-node-resolve": "^10.0.0", - "@rollup/plugin-replace": "^2.3.4", - "@types/aws-lambda": "^8.10.64", - "@types/facebook-js-sdk": "^3.3.1", + "@rollup/plugin-alias": "^3.1.2", + "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-inject": "^4.0.2", + "@rollup/plugin-node-resolve": "^11.2.0", + "@rollup/plugin-replace": "^2.4.1", + "@types/aws-lambda": "^8.10.72", + "@types/facebook-js-sdk": "^3.3.2", "@types/isomorphic-fetch": "^0.0.35", - "@types/lodash": "^4.14.165", + "@types/lodash": "^4.14.168", "@types/mithril": "^2.0.6", - "ava": "^3.13.0", - "esbuild": "^0.8.57", + "@types/node": "^14.14.34", + "ava": "^3.15.0", "esm": "^3.2.25", - "rollup": "^2.33.2", + "rollup": "^2.41.2", "rollup-plugin-filesize": "^9.1.1", + "rollup-plugin-globlin": "^0.1.3", "rollup-plugin-livereload": "^2.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, From c7e522b7b5da82f3885647df23266889a9390a05 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:48:43 +0100 Subject: [PATCH 06/60] modify readme NEEDS UPDATING --- readme.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index c013ac5..ba2ece6 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ import * as Pjax from "@brixtol/pjax"; /* -------------------------------------------- */ Pjax.connect({ - target: ["main", "#navbar"], + fragments: ["main"], action: "replace", prefetch: true, cache: true, @@ -112,7 +112,19 @@ Programmatic navigation visit to a URL. You can optionally pass in options for t Returns cache `Map` session. All methods available to `Map` can be accessed via this method. -## Navigation Options +## Terminology + +###### Targets + +Targets are fragment elements which contain a `data-pjax-target="*"` attribute. + +###### Actions + +Actions are manipulations executed by pjax. + +## Navigation + +
#### `data-pjax-eval="false"` @@ -140,6 +152,11 @@ Example #### `data-pjax-disable` +###### Options + +- `true` +- `history` + Place on `href`elements you don't want pjax navigation to be executed. When present a normal page navigation will be executed and cache will be cleared unless combined with a `cache` option.
@@ -215,37 +232,102 @@ Lets assume you are navigating from `Page 1` to `Page 2` and `#main` is your def
-#### `data-pjax-target="*"` +
+ +#### `data-pjax-replace` + +Executes a replacement to single or multiple fragments. -Target selectors for navigation. Use space separation when defining multiple targets to reload. +###### ATTRIBUTE + +- `(['target'])` +- `(['target' , 'target'])`
Example + ```html - + + + Link + + +
+ I will be replaced on next navigation +
+ +
+ I will be replaced on next navigation +
+ ```
-#### `data-pjax-chunks="*"` +
+ +#### `data-pjax-prepend` + +Executes a prepend visit. Locates target, then prepends it another target. A Prepend navigation will have its action recorded. -Target multiple fragments from a link navigation. Space separated expression with colon separated `target` and `action` options. This is helpful when you which to reload additional target elements using different actions, like providing infinite scrolling. +###### ATTRIBUTE + +- `(['target' , 'target'])` +- `(['target' , 'target'], ['target' , 'target'])`
Example +###### PAGE 1 + + +```html + + + Page 2 + + +
+ I will prepend to target-2 on next navigation +
+ +
+

target-1 will prepended to me on next navigation

+
+ +``` + +###### PAGE 2 + + ```html - + href="*" + data-pjax-prepend="(['target-1', 'target-2'])"> + Page 2 + + +
+ + +
+ I am target-1 and have been prepended to target-2 +
+ +

target-1 is now prepended to me

+ +
+ ```
From fc7df7127c235862e1cecc41cb420059805a300b Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:48:52 +0100 Subject: [PATCH 07/60] multiple builds --- rollup.config.js | 150 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 35 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 81fb09a..3000250 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,48 +2,128 @@ import { terser } from 'rollup-plugin-terser' import noderesolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import filesize from 'rollup-plugin-filesize' +import replace from '@rollup/plugin-replace' +import babel, { getBabelOutputPlugin } from '@rollup/plugin-babel' +import inject from '@rollup/plugin-inject' -export default { - input: 'src/index.js', - context: 'window', - output: [ - { - format: 'iife', +const { prod } = process.env + +const plugins = [ + replace({ + preventAssignment: true, + values: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) + } + }), + noderesolve({ browser: true }), + commonjs() +] + +export default [ + { + input: 'src/index.js', + output: [ + { + format: 'es', + name: 'Pjax', + file: 'package/pjax.esm.js', + sourcemap: false, + preferConst: true, + plugins: [ + prod ? terser({ + compress: { + passes: 2 + } + }) : null + ] + }, + { + format: 'umd', + name: 'Pjax', + file: 'package/pjax.umd.js', + sourcemap: false, + preferConst: true, + plugins: [ + getBabelOutputPlugin({ + allowAllFormats: true + }), + prod ? terser({ + compress: { + passes: 2 + } + }) : null + ] + } + ], + plugins: [ + ...plugins, + babel({ + babelHelpers: 'runtime', + presets: [ + [ + '@babel/preset-env', { + targets: { + esmodules: true + } + } + ] + ], + plugins: [ + [ '@babel/plugin-transform-runtime' ], + [ '@babel/plugin-syntax-dynamic-import' ] + ] + }), + filesize() + ] + }, + { + input: 'src/index.js', + context: 'window', + external: [ /@babel\/runtime/ ], + output: { + format: 'umd', name: 'Pjax', - file: 'package/pjax.min.js', + file: 'package/pjax.es5.js', sourcemap: false, plugins: [ - process.env.prod ? terser({ + getBabelOutputPlugin({ + allowAllFormats: true, + presets: [ + [ + '@babel/preset-env', + { + corejs: 3, + useBuiltIns: 'entry' + } + ] + ] + }), + prod ? terser({ + ecma: 5, compress: { passes: 2 } }) : null ] }, - { - format: 'es', - file: 'package/pjax.esm.js', - sourcemap: false, - preferConst: true, - plugins: [] - }, - { - format: 'es', - file: 'package/pjax.esm.min.js', - sourcemap: false, - preferConst: true, - plugins: [ - process.env.prod ? terser({ - compress: { - passes: 2 - } - }) : null - ] - } - ], - plugins: [ - noderesolve(), - commonjs(), - filesize() - ] -} + plugins: [ + inject({ + IntersectionObserver: [ 'intersection-observer', '*' ] + }), + ...plugins, + babel({ + babelHelpers: 'runtime', + babelrc: false, + sourceMaps: true, + retainLines: true, + compact: true, + plugins: [ + [ '@babel/plugin-transform-runtime', { absoluteRuntime: true } ], + '@babel/plugin-transform-property-mutators', + '@babel/plugin-syntax-dynamic-import' + ] + }), + filesize() + ] + } +] From 67042c2a071371a44df65540d14b6565aa755e76 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:49:05 +0100 Subject: [PATCH 08/60] controller adjustments --- src/app/controller.js | 128 +++++++++--------------------------------- 1 file changed, 26 insertions(+), 102 deletions(-) diff --git a/src/app/controller.js b/src/app/controller.js index ee6f95c..113b15a 100644 --- a/src/app/controller.js +++ b/src/app/controller.js @@ -1,69 +1,49 @@ -import { store } from './store' -import { dispatchEvent } from './utils' +import { store, cache } from './store' import { expandURL } from './location' -import { xhrSuccess } from '../constants/enums' import * as hrefs from '../observers/hrefs' -import * as prefetch from '../observers/prefetch' +import * as mouseover from '../observers/mouseover' +import * as intersect from '../observers/intersect' +import * as scroll from '../observers/scrolling' +import * as history from '../observers/history' import * as render from './render' -import * as request from './request' - -/** - * Popstate event handler - * - * @param {PopStateEvent} event - */ -function popstate (event) { - - if (store.config.prefetch) prefetch.stop() - - if (event?.state) { - render.update(event.state, true) - } else { - - // If get here default to regular back button - history.back() - } - - if (store.config.prefetch) prefetch.start() - -} /** * Sets initial page state on landing page and * caches it so return navigation don't perform an extrenous * request * - * @param {Window} window + * @param {Event} event */ -function setInitialCache ({ location: { href } }) { +function setInitialCache (event) { - const location = expandURL(href) + const location = expandURL(window.location.href) const state = store.update.page({ - location, + title: document.title, url: location.pathname + location.search, - snapshot: document.documentElement.outerHTML + snapshot: render.DOMSnapshot(document), + location }) - store.cache.set(store.page.url, state) + cache.set(state.url, state) } /** * Initialize */ -export const initialize = () => { +export function initialize () { if (!store.started) { - setInitialCache(window) - + history.start() hrefs.start() - prefetch.start() + scroll.start() + mouseover.start() + intersect.start() - console.info('Pjax: Connection Established ⚡') + addEventListener('load', setInitialCache, false) - addEventListener('popstate', popstate) - dispatchEvent('pjax:load', store.page) + console.info('Pjax: Connection Established ⚡') store.started = true @@ -80,74 +60,18 @@ export function destroy () { if (store.started) { - removeEventListener('popstate', popstate) - + history.stop() hrefs.stop() - prefetch.stop() + scroll.stop() + mouseover.start() + intersect.start() + cache.clear() - store.cache.clear() store.started = false - console.info('Pjax: Instance has been disconnected! 😔') - + console.warn('Pjax: Instance has been disconnected! 😔') } else { - console.info('Pjax: No connection made, disconnection is void 🙃') + console.warn('Pjax: No connection made, disconnection is void 🙃') } } - -/** - * Executes a visit by fetching the the cached response - * from the session and passes it to the renderer. - * - * @param {string} url - * @exports - */ -export const cacheVisit = url => { - - const state = store.cache.get(url) - - // console.log('cache', url) - - if (store.config.prefetch) prefetch.stop() - - // ensure we have state before updating - if (state) render.update(state) - - if (store.config.prefetch) prefetch.start() - -} - -/** - * Pjax link handler which dispatches a fetch request - * when `href` tag is clicked. If clicked link is in transit - * from prefetch it will pass to cache visit - * - * @param {IPjax.IState} state - */ -export async function pjaxVisit (state) { - - if (prefetch.transit.has(state.url)) { - if ((await request.inFlight(state.url))) return cacheVisit(state.url) - request.cancel(state.url) - } - - if (store.config.prefetch) prefetch.stop() - - const response = await request.get(state) - - if (response === xhrSuccess) render.update(store.page) - - if (store.config.prefetch) prefetch.start() - -} - -/** - * Executes a pjax navigation. - * - * @param {IPjax.IState} state - */ -export function navigate (state) { - - return store.cache.has(state.url) ? cacheVisit(state.url) : pjaxVisit(state) -} From 02afc01fa510cc5961c92e0c942e8e60e5902b03 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:49:09 +0100 Subject: [PATCH 09/60] Update location.js --- src/app/location.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/app/location.js b/src/app/location.js index 0613ba3..4c6ed76 100644 --- a/src/app/location.js +++ b/src/app/location.js @@ -1,13 +1,16 @@ +import history from 'history/browser' + /** * Expands URL href location. * * @param {string} url + * @returns {IPjax.ILocation} */ export function expandURL (url) { - const anchor = document.createElement('a') - - anchor.href = url.toString() + const lastPath = history.createHref(window.location) + const location = document.createElement('a') + location.href = url.toString() const { protocol @@ -16,7 +19,7 @@ export function expandURL (url) { , href , pathname , search - } = new URL(anchor.href) + } = new URL(location.href) return { protocol @@ -25,6 +28,7 @@ export function expandURL (url) { , href , pathname , search + , lastPath } } @@ -75,23 +79,21 @@ export const getLocation = ( /** * Returns the current URL * - * @param {Element} target + * @param {Element|string} target */ -export const getURL = target => expandURL(target.getAttribute('href')) +export function getURL (target) { -/** - * Returns the pathname from `href` target used for cache key. - * - * @param {IPjax.ILocation} location - */ -export const getCacheKey = ({ pathname, search }) => (pathname + search) + const href = target instanceof Element ? target.getAttribute('href') : target + const { pathname, search } = expandURL(href) + return pathname + search +} /** * Returns the pathname from `href` target used for cache key. * * @param {Element} target */ -export const getCacheKeyFromTarget = target => getCacheKey(getURL(target)) +export const getCacheKeyFromTarget = target => getURL(target) /** * Returns the protocol and host From 1ef9f53bc85dad49fc80e25d1f71d95d406bcfb6 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 06:49:24 +0100 Subject: [PATCH 10/60] various configurations --- src/app/prefetch.js | 31 +++++ src/app/progress.js | 86 ++++++++++++++ src/app/render.js | 217 +++++++++++++++++++++------------- src/app/request.js | 121 ++++++++++++------- src/app/store.js | 162 ++++++++++---------------- src/app/utils.js | 101 ++++++---------- src/app/visit.js | 191 ++++++++++++++++++++++++++++++ src/constants/common.js | 14 --- src/constants/enums.js | 24 ---- src/index.js | 32 ++++- src/observers/history.js | 97 +++++++++++++++ src/observers/hrefs.js | 203 ++++---------------------------- src/observers/intersect.js | 120 +++++++++++++++++++ src/observers/mouseover.js | 206 ++++++++++++++++++++++++++++++++ src/observers/prefetch.js | 233 ------------------------------------- src/observers/scrolling.js | 77 ++++++++++++ types/index.d.ts | 46 ++++++++ types/pjax.d.ts | 37 ------ types/store.d.ts | 92 ++++++++++++++- 19 files changed, 1307 insertions(+), 783 deletions(-) create mode 100644 src/app/prefetch.js create mode 100644 src/app/progress.js create mode 100644 src/app/visit.js delete mode 100644 src/constants/enums.js create mode 100644 src/observers/history.js create mode 100644 src/observers/intersect.js create mode 100644 src/observers/mouseover.js delete mode 100644 src/observers/prefetch.js create mode 100644 src/observers/scrolling.js create mode 100644 types/index.d.ts delete mode 100644 types/pjax.d.ts diff --git a/src/app/prefetch.js b/src/app/prefetch.js new file mode 100644 index 0000000..32ac1ef --- /dev/null +++ b/src/app/prefetch.js @@ -0,0 +1,31 @@ +import { store } from './store' +import * as mouseover from '../observers/mouseover' +import * as intersect from '../observers/intersect' + +/** + * Starts prefetch, will initialize `IntersectionObserver` and + * add event listeners and other logics. + * + * @export + */ +export function start () { + + if (store.config.prefetch) { + mouseover.start() + intersect.start() + } +} + +/** + * Stops prefetch, will disconnect `IntersectionObserver` and + * remove any event listeners or transits. + * + * @export + */ +export function stop () { + + if (store.config.prefetch) { + mouseover.stop() + intersect.stop() + } +} diff --git a/src/app/progress.js b/src/app/progress.js new file mode 100644 index 0000000..00ae963 --- /dev/null +++ b/src/app/progress.js @@ -0,0 +1,86 @@ +import { store } from './store' + +/* -------------------------------------------- */ +/* LETTINGS */ +/* -------------------------------------------- */ + +/** + * Timer Reference + * + * @type {any} + */ +let timer + +/** + * Progress Element + * + * @type {Element} + */ +let element + +/** + * Loading + * + * @type {boolean} + */ +export let loading = false + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ + +/** + * Show Progress Bar + * + * @export + */ +export function show () { + + if (store.config.progress) { + + if (timer) { + clearTimeout(timer) + timer = 0 + element.className = 'pjax-loader pjax-hide' + setTimeout(show, 25) + return + } + + if (!element) { + element = document.createElement('div') + element.innerHTML = '
' + element.setAttribute('data-pjax-track', 'true') + document.body.appendChild(element) + } + + element.className = 'pjax-loader pjax-start' + + timer = setTimeout(() => { + timer = 0 + loading = true + element.classList.add('pjax-inload') + }, 15) + } +} + +/** + * Hide Progress Bar + * + * @export + */ +export function hide () { + + if (store.config.progress) { + if (timer) clearTimeout(timer) + + element.classList.add('pjax-end') + + timer = setTimeout(() => { + timer = 0 + element.classList.add('pjax-hide') + }, 800) + + loading = false + } + +} diff --git a/src/app/render.js b/src/app/render.js index 50f02d0..c75c232 100644 --- a/src/app/render.js +++ b/src/app/render.js @@ -1,7 +1,14 @@ -import { DOMParseFallback, isReplace } from '../constants/regexp' -import { Implementation, ArraySlice, DomParser } from '../constants/common' +import { isReplace } from '../constants/regexp' import { eachSelector, dispatchEvent, forEach } from './utils' -import { store } from './store' +import { store, snapshots, cache, tracked } from './store' +import { getURL } from './location' +import { nanoid } from 'nanoid' +import history from 'history/browser' +import * as progress from './progress' + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ /** * DOM Head Nodes @@ -58,9 +65,9 @@ function appendTrackedNode (node) { // tracked element must contain id if (!node.hasAttribute('id')) return - if (!store.dom.tracked.has(node.id)) { + if (!tracked.has(node.id)) { document.body.appendChild(node) - store.dom.tracked.add(node.id) + tracked.add(node.id) } } @@ -72,32 +79,32 @@ function appendTrackedNode (node) { * @param {Element} target * @param {IPjax.IState} state * @returns {(DOM: Element) => void}} - * @memberof Render */ -const replaceTarget = (target, element, { method }) => DOM => { +const replaceTarget = (target, { method }) => DOM => { if (!isReplace.test(method)) { - dispatchEvent('pjax:render', { method, element, fragment: target }, true) + dispatchEvent('pjax:render', { method, fragment: target }, true) DOM.innerHTML = target.innerHTML } else { let fragment = document.createElement('div') - const nodes = ArraySlice.call(target.childNodes) + const nodes = [].slice.call(target.childNodes) forEach(nodes, node => fragment.appendChild(node)) if (method === 'append') { - dispatchEvent('pjax:render', { method, element, fragment }, true) + + dispatchEvent('pjax:render', { method, fragment }, true) DOM.appendChild(fragment) console.log(fragment) console.log('in append') } else { - dispatchEvent('pjax:render', { method, element, fragment }, true) + dispatchEvent('pjax:render', { method, fragment }, true) DOM.insertBefore(fragment, DOM.firstChild) } @@ -106,19 +113,104 @@ const replaceTarget = (target, element, { method }) => DOM => { } } +function runActions () { + + Object.entries(state.action).forEach(([ action, targets ]) => { + + targets.forEach((node) => { + + if (action === 'replace') { + + const element = document.body.querySelector(`[data-pjax-target="${node}"]`) + const replace = target.body.querySelector(`[data-pjax-target="${node}"]`) + + element.replaceWith(replace) + } + + if (action === 'append') { + + const element = document.querySelector(`[data-pjax-target="${node[0]}"]`) + + state.targets[node[1]].forEach(newnode => { + element.appendChild(newnode) + dispatchEvent('pjax:render', { node: newnode }, true) + }) + + } + + }) + + }) + +} + +/** + * Get targets + * + * @param {Document} element + * @param {IPjax.IState} state + */ +export function getTargets ({ body }, state) { + + body.querySelectorAll(Targets).forEach(node => { + + const name = node.getAttribute('data-pjax-target') + + if (!state.targets[name]) state.targets[name] = [] + + state.targets[name].push(node) + // node.setAttribute('data-pjax-action', uuid) + + }) + +} + +/** + * Captures current document element and sets a + * record to snapshot state + * + * @export + * @param {Document} target + * @returns {string} + */ +export function DOMSnapshot (target) { + + const uuid = nanoid(16) + snapshots.set(uuid, target.documentElement.outerHTML) + + return uuid + +} + /** * Updates cached DOM * * @export * @param {string} url + * @param {object} options */ -export function getActiveDOM (url) { +export function captureDOM (url, options) { + + url = getURL(url) - if (store.config.cache && store.cache.has(url)) { + if (store.config.cache && cache.has(url)) { - // store.cache.get(url).snapshot = document.documentElement.outerHTML + const state = cache.get(url) + const DOMString = snapshots.get(state.snapshot) + const target = DOMParse(DOMString) - // console.log(store.cache.get(url).snapshot) + console.log(url) + target.body.innerHTML = document.documentElement.querySelector('body').innerHTML + + if (options.action === 'replace') { + + snapshots.set(state.snapshot, target.documentElement.outerHTML) + } else if (options.action === 'capture') { + + state.captured = DOMSnapshot(target) + history.replace(state.location.href, state) + console.info('Pjax: DOM Captured at: ' + state.captured) + } } @@ -134,25 +226,7 @@ export function getActiveDOM (url) { */ export function DOMParse (data) { - if (DomParser) return DomParser.parseFromString(data, 'text/html') - - /** - * FALLBACK - Browser Does not support DOMParser - */ - let DOM = Implementation.createHTMLDocument('') - - if (DOMParseFallback.test(data)) { - DOM.documentElement.innerHTML = data - if (!DOM.body || !DOM.head) { - DOM = Implementation.createHTMLDocument('') - DOM.write(data) - } - } else { - DOM.body.innerHTML = data - } - - return DOM - + return new DOMParser().parseFromString(data, 'text/html') } /** @@ -165,79 +239,58 @@ export function DOMParse (data) { */ export function update (state, popstate = false) { - console.log(state) + const uuid = (popstate && state?.captured) ? state.captured : state.snapshot + const target = DOMParse(snapshots.get(uuid)) - const target = DOMParse(state.snapshot) - const title = document.title = target.title || '' + state.title = document.title = target?.title || '' if (!popstate) { - history.pushState(state, title, state.location.href) - } - if (target.head) { - DOMHead(target.head) - } + const { pathname, search } = history.location - // APPEND TRACKED NODES - // - eachSelector(target, '[data-pjax-track]', appendTrackedNode) - - eachSelector(document, '[data-pjax-replace]', element => { - - element.replaceWith(target.getElementById(element.id)) - - }) - - Object.keys(state.action).forEach(prop => { - - if (state.action[prop]) { + if ((pathname + search) === state.url) { + history.replace(state.location.href, state) + } else { + history.push(state.location.href, state) + } - forEach(state.action[prop], ([ from, to ]) => { + } else if (typeof state?.captured === 'string') { - const nodes = target.body.querySelectorAll(from) - const frag = document.body.querySelector(to) + if (snapshots.delete(uuid)) { + state.captured = null + history.replace(state.location.href, state) + console.info('Pjax: Captured snapshot removed at: ' + state.url) + } - console.log(nodes) - nodes.forEach(node => { - frag.appendChild(node) - dispatchEvent('pjax:render', { node }, true) - }) + } - }) - } + if (target?.head) DOMHead(target.head) - }) + // APPEND TRACKED NODES + eachSelector(target, '[data-pjax-track]', appendTrackedNode) - const fallback = 1 + let fallback = 1 - // REPLACE TARGETS - // - /* forEach(state.target, element => { + forEach(state.target, element => { const node = target.body.querySelector(element) - // if (node && node.hasAttribute('data-pjax-class')) setTargetClass(node) - return node ? eachSelector( document, element, - replaceTarget(node, element, state) + replaceTarget(node, state) ) : fallback++ }) -*/ - // when no targets are found we will replace the - // entire document body + if (fallback === state.target.length) { - // replaceTarget(target.body, state)(document.body) + replaceTarget(target.body, state)(document.body) } - // SET SCROLL POSITION - // - // window.scrollTo(state.position.x, state.position.y) + window.scrollTo(state.position.x, state.position.y) - console.log(store.dom) + progress.hide() - dispatchEvent('pjax:load', { state, target }) + dispatchEvent('pjax:load', state) } diff --git a/src/app/request.js b/src/app/request.js index abc76ba..61319ff 100644 --- a/src/app/request.js +++ b/src/app/request.js @@ -1,6 +1,17 @@ -import { store } from './store' +import { store, snapshots, requests, cache } from './store' import { asyncTimeout, byteConvert, byteSize, dispatchEvent } from './utils' -import { xhrFailed, xhrPrevented, xhrSuccess, xhrEmpty, xhrExists } from '../constants/enums' +import { nanoid } from 'nanoid' +import * as progress from './progress' + +/* -------------------------------------------- */ +/* LETTINGS */ +/* -------------------------------------------- */ + +let storage = 0 + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ /** * Executes on request end. Removes the XHR recrod and update @@ -17,17 +28,8 @@ import { xhrFailed, xhrPrevented, xhrSuccess, xhrEmpty, xhrExists } from '../con */ function HttpRequestEnd (url, DOMString) { - const { total } = store.request.cache - - store.request.xhr.delete(url) - store.update.request({ - cache: { - total: (total + byteSize(DOMString)), - weight: byteConvert(total) - } - }) - - // console.log('cache size: ', store.request.cache.weight) + storage = storage + byteSize(DOMString) + requests.delete(url) } @@ -37,17 +39,19 @@ function HttpRequestEnd (url, DOMString) { * @param {IPjax.IState} state * The `location.href`request address * - * @return {Promise} + * @param {boolean} [async=false] + * The XHR request is a asynchronous request or not + * * DOM string, equivelent to `document.documentElement.outerHTML` */ -function HttpRequest ({ +async function HttpRequest ({ url, prefetch, target, location: { href } -}) { +}, async) { const xhr = new XMLHttpRequest() @@ -55,7 +59,7 @@ function HttpRequest ({ // OPEN // - xhr.open('GET', href, true) + xhr.open('GET', href, async) // HEADERS // @@ -66,7 +70,7 @@ function HttpRequest ({ // EVENTS // - xhr.onloadstart = e => store.request.xhr.set(url, xhr) + xhr.onloadstart = e => requests.set(url, xhr) xhr.onload = e => xhr.status === 200 ? resolve(xhr.responseText) : null xhr.onloadend = e => HttpRequestEnd(url, xhr.responseText) xhr.onerror = reject @@ -79,6 +83,20 @@ function HttpRequest ({ } +/** + * Returns request cache metrics + * + */ +export function cacheSize () { + + return { + requests: snapshots.size, + total: storage, + weight: byteConvert(storage) + } + +} + /** * Cancels the request in transit * @@ -90,17 +108,13 @@ function HttpRequest ({ */ export function cancel (url) { - if (store.request.xhr.has(url)) { + if (requests.has(url)) { - // ABORT AND REMOVE - // - store.request.xhr.get(url).abort() - store.request.xhr.delete(url) + requests.get(url).abort() + requests.delete(url) - console.info(`XHR Request was cancelled for url: ${url}`) + console.warn(`Pjax: XHR Request was cancelled for url: ${url}`) - } else { - console.warn(`No in-flight request in transit for url: ${url}`) } } @@ -122,12 +136,18 @@ export function cancel (url) { */ export async function inFlight (url, limit = 0) { - if (store.request.xhr.has(url) && limit <= 85) { + if (requests.has(url) && limit <= 85) { + if (store.config.progress && !progress.loading) { + if (limit === store.config.threshold.progress) progress.show() + } + console.log('waiting', url, limit) + return asyncTimeout(() => inFlight(url, (limit + 1)), 25) + } - return !store.request.xhr.has(url) + return !requests.has(url) } @@ -138,40 +158,57 @@ export async function inFlight (url, limit = 0) { * @param {IPjax.IState} state * The page state object acquired from link * - * @return {Promise} + * @param {boolean} [async=false] + * The XHR request is a asynchronous request or not + * + * @return {Promise} * A boolean response representing a successful or failed fetch */ -export async function get (state) { +export async function get (state, async = true) { - if (store.request.xhr.has(state.url)) return xhrExists - if (!dispatchEvent('pjax:request', state.location, true)) return xhrPrevented + if (requests.has(state.url)) { + console.warn(`Pjax: XHR Request is already in transit for: ${state.url}`) + return null + } + + if (!dispatchEvent('pjax:request', state.location, true)) { + console.warn(`Pjax: Request cancelled via dispatched event for: ${state.url}`) + return null + } + + if (state.method !== 'prefetch') { + if (store.config.progress && !progress.loading) progress.show() + } try { - const snapshot = await HttpRequest(state) + const response = await HttpRequest(state, async) + + if (typeof response === 'string' && response.length > 0) { - if (typeof snapshot === 'string' && snapshot.length > 0) { + if (!state.snapshot) state.snapshot = nanoid(16) - state.snapshot = snapshot + snapshots.set(state.snapshot, response) - if (store.config.cache && !store.cache.has(state.url)) { + if (store.config.cache && !cache.has(state.url)) { if (dispatchEvent('pjax:cache', state, true)) { - store.cache.set(state.url, state) + cache.set(state.url, state) } } - return xhrSuccess + if (store.config.progress && progress.loading) progress.hide() + + return Promise.resolve(state) } else { - console.info(`Pjax: Failed to receive response at: ${state.url}`) - return xhrEmpty + console.warn(`Pjax: Failed to receive response at: ${state.url}`) } } catch (error) { - store.request.xhr.delete(state.url) + requests.delete(state.url) console.error(error) } - return xhrFailed + return null } diff --git a/src/app/store.js b/src/app/store.js index 68b870a..4cdb0d4 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,5 +1,44 @@ import merge from 'mergerino' +/** + * Snapshots Cache + * + * @exports + */ +export const snapshots = new Map() + +/** + * @exports + * @type {Map} + */ +export const transit = new Map() + +/** + * Tracked Elements + * + * @exports + * @type {Set} + */ +export const tracked = new Set() + +/** + * XHR Requests + * + * @type {Map} + */ +export const requests = new Map() + +/** + * Cache + * + * @exports + * @type {Map} + */ +export const cache = new Map() + +/** + * store + */ export const store = ( /** @@ -18,8 +57,6 @@ export const store = ( this.update.config(options) this.update.page(this.config) - this.update.dom() - this.update.request() } @@ -31,6 +68,8 @@ export const store = ( get started () { + if (state?.started) state.started = false + return state.started } @@ -45,21 +84,13 @@ export const store = ( , - /* -------------------------------------------- */ - /* CACHE */ - /* -------------------------------------------- */ + get location () { - /** - * @return {Map} - */ - get cache () { - - return state.cache + return state.page.url } , - /* -------------------------------------------- */ /* STORE GETTERS */ /* -------------------------------------------- */ @@ -86,32 +117,6 @@ export const store = ( , - /** - * @return {IPjax.IDom} - */ - get dom () { - - return state.dom - - } - - , - - /* -------------------------------------------- */ - /* REQUEST GETTER */ - /* -------------------------------------------- */ - - /** - * @return {IPjax.IRequest} - */ - get request () { - - return state.request - - } - - , - /* -------------------------------------------- */ /* UPDATES */ /* -------------------------------------------- */ @@ -122,18 +127,11 @@ export const store = ( config: ( initial => patch => ( - state.config = merge( - initial, - patch - ) + state.config = merge(initial, patch) ) )( { - target: [ - 'main', - '#navbar', - '[script]' - ], + target: [ 'main', '#navbar' ], method: 'replace', prefetch: true, cache: true, @@ -141,7 +139,8 @@ export const store = ( progress: false, threshold: { intersect: 250, - hover: 100 + hover: 100, + progress: 10 } } ) @@ -153,10 +152,12 @@ export const store = ( page: ( initial => patch => ( state.page = merge( - state.page || initial, + initial, { - ...patch, - action: { + ...patch + , target: state.config.target + , action: { + replace: null, append: null, prepend: null } @@ -167,25 +168,26 @@ export const store = ( { url: '', snapshot: '', + captured: null, target: [], - chunks: Object.create(null), + title: '', method: 'replace', - prefetch: 'intersect', + prefetch: 'hover', + cache: null, + progress: false, action: { + replace: null, prepend: null, append: null }, - cache: null, - progress: false, - reload: false, - throttle: 0, location: { protocol: '', origin: '', hostname: '', href: '', pathname: '', - search: '' + search: '', + lastPath: '' }, position: { x: 0, @@ -194,52 +196,10 @@ export const store = ( } ) - , - - /* DOM ---------------------------------------- */ - - dom: ( - initial => patch => ( - state.dom = merge( - state.dom || initial, - { ...patch, tracked: initial.tracked } - ) - ) - )( - { - tracked: new Set(), - head: Object.create(null) - } - ) - - , - - request: ( - initial => patch => ( - state.request = merge( - state.request || initial, - { ...patch, xhr: initial.xhr } - ) - ) - )( - { - xhr: new Map(), - cache: { - weight: '0 B', - total: 0, - limit: 50000000 // = 50 MB - } - } - ) } }) )( - Object.create( - { - started: false, - cache: new Map() - } - ) + Object.create(null) ) diff --git a/src/app/utils.js b/src/app/utils.js index 2bc6955..3f47f64 100644 --- a/src/app/utils.js +++ b/src/app/utils.js @@ -1,6 +1,41 @@ -import { isNumber, ActionAttr, ActionParams } from '../constants/regexp' +import { isNumber } from '../constants/regexp' import { Units } from './../constants/common' +/** + * Handles a clicked link, prevents special click types. + * + * @param {MouseEvent} event + * @return {boolean} + */ +export function linkEventValidate (event) { + + // @ts-ignore + return !((event.target && event.target.isContentEditable) || + event.defaultPrevented || + event.which > 1 || + event.altKey || + event.ctrlKey || + event.metaKey || + event.shiftKey) + +} + +/** + * Locted the closest link when click bubbles. + * + * @param {EventTarget} target + * The link `href` element target + * + * @param {string} selector + * The selector query name, eg: `[data-pjax]` + * + * @return {Element|false} + */ +export function linkLocator (target, selector) { + + return target instanceof Element ? target.closest(selector) : false +} + /** * Constructs a JSON object from HTML `data-pjax-*` attributes. * Attributes are passed in as array items @@ -39,10 +74,10 @@ export function jsonAttrs (accumulator, current, index, source) { * Array Chunk function * * @export - * @param {number} size + * @param {number} [size=2] * @return {(acc: any[], value: string) => any[]} */ -export function chunk (size) { +export function chunk (size = 2) { return (acc, value) => (!acc.length || acc[acc.length - 1].length === size ? ( acc.push([ value ]) @@ -52,66 +87,6 @@ export function chunk (size) { } -/** - * Constructs a JSON object from HTML `data-pjax-*` attributes. - * Attributes are passed in as array items - * - * @param {string} string - * @return {object} - */ -export function actionAttrs (string) { - - let newString - let lastIndex = 0 - - /** - * @param {object} acc - * @param {string} value - * @returns - */ - const actions = (acc, value) => { - lastIndex = string.indexOf(')', lastIndex) + 1 - newString = string.substring(string.indexOf(value) + value.length, lastIndex) - return { - ...acc, - [value]: newString.match(ActionParams).reduce(chunk(2), []) - } - } - - return string - .match(ActionAttr) - .reduce(actions, {}) - -} - -/** - * Unqiue Identifier code for cached state - * - * NOT IN USE - * - * @returns {string} - */ -export function uuid () { - - return Array.apply( - null - , { length: 36 } - ).map(( - _ - , index - ) => ( - (index === 8 || index === 13 || index === 18 || index === 23) ? ( - '-' - ) : index === 14 ? ( - '4' - ) : index === 19 ? ( - (Math.floor(Math.random() * 4) + 8).toString(16) - ) : ( - Math.floor(Math.random() * 15).toString(16) - ) - )).join('') -} - /** * Dispatches lifecycle events on the document. * diff --git a/src/app/visit.js b/src/app/visit.js new file mode 100644 index 0000000..ddec100 --- /dev/null +++ b/src/app/visit.js @@ -0,0 +1,191 @@ +import { cache, store, transit } from './store' +import { expandURL } from '../app/location' +import { forEach, jsonAttrs, chunk } from '../app/utils' +import * as regexp from '../constants/regexp' +import * as scroll from '../observers/scrolling' +import * as prefetch from './prefetch' +import * as render from './render' +import * as request from './request' + +/** + * @type {string[]} + */ +const attrs = [ + 'target', + 'method', + 'action', + 'prepend', + 'append', + 'replace', + 'prefetch', + 'cache', + 'progress', + 'throttle', + 'position', + 'reload' +] + +/** + * Get State Page + * + * + * @param {Element} target + * The link `href` element target + * + * @return {IPjax.IState} + * Returns an updated page state object + */ +export function getPageState (target) { + + const location = expandURL(target.getAttribute('href')) + const url = location.pathname + location.search + + return cache.has(url) ? cache.get(url) : store.update.page({ + url, + location + }) + +} + +/** + * Parses link `href` attributes and assigns them to + * configuration options. Each link target can define + * navigation options. + * + * @param {Element} target + * The link `href` element target + * + * @return {IPjax.IState} + * Returns an updated page state object + */ +export function visitClickState (target) { + + const state = getPageState(target) + + state.method = 'click' + + return getVisitConfig(state, target) + +} + +/** + * Parses link `href` attributes and assigns them to + * configuration options. + * + * @param {IPjax.IState} state + * Current state configuration + * + * @param {Element} target + * The link `href` element target + * + * @return {IPjax.IState} + * Returns an updated page state object + */ +export function getVisitConfig (state, target) { + + forEach(attrs, prop => { + + const value = target.getAttribute(`data-pjax-${prop}`) + + if (value === null) { + + if ( + prop === 'prefetch' && + value !== 'hover' && + value !== 'intersect') state[prop] = false + + } else { + + if (/\b(append|prepend|replace)\b/.test(prop)) { + + state.action[prop] = prop === 'replace' ? ( + value.match(regexp.ActionParams) + ) : ( + value.match(regexp.ActionParams).reduce(chunk(2), []) + ) + } else { + + state[prop] = prop === 'target' ? ( + value.split(regexp.isWhitespace) + ) : (prop === 'position' || prop === 'threshold') ? ( + value.match(regexp.inPosition).reduce(jsonAttrs, {}) + ) : regexp.isBoolean.test(value.trim()) ? ( + value === 'true' + ) : regexp.isNumber.test(value.trim()) ? ( + Number(value) + ) : ( + value.trim() + ) + } + } + }) + + return state + +} + +/** + * Executes a pjax navigation. + * + * @param {IPjax.IState} state + */ +export function navigate (state) { + + // console.log(state, cache.has(state.url)) + cache.get(state.location.lastPath).position = scroll.getPosition() + state.position = scroll.reset() + + if (cache.has(state.url)) { + cache.set(state.url, state) + return cacheVisit(state.url) + } + + return pjaxVisit(state) +} + +/** + * Executes a visit by fetching the the cached response + * from the session and passes it to the renderer. + * + * @param {string} url + * @exports + */ +export async function cacheVisit (url) { + + const state = cache.get(url) + + if (store.config.prefetch) prefetch.stop() + + // ensure we have state before updating + if (state) render.update(state) + + if (store.config.prefetch) prefetch.start() + +} + +/** + * Pjax link handler which dispatches a fetch request + * when `href` tag is clicked. If clicked link is in transit + * from prefetch it will pass to cache visit + * + * @param {IPjax.IState} state + */ +export async function pjaxVisit (state, async = false) { + + if (transit.has(state.url)) { + if ((await request.inFlight(state.url))) { + return cacheVisit(state.url) + } else { + request.cancel(state.url) + } + } + + if (store.config.prefetch) prefetch.stop() + + const cacheState = await request.get(state) + + if (cacheState) render.update(cacheState, async) + + if (store.config.prefetch) prefetch.start() + +} diff --git a/src/constants/common.js b/src/constants/common.js index 1555a82..fc05af1 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -1,12 +1,3 @@ -/** - * Array Slice Prototype - */ -export const ArraySlice = Array.prototype.slice - -/** - * Document Implentation - */ -export const Implementation = document.implementation /** * Link Selector @@ -28,11 +19,6 @@ export const LinkPrefetchIntersect = 'a[data-pjax-prefetch="intersect"]' */ export const Form = 'form:not([data-pjax-disable])' -/** - * DOM Parse - */ -export const DomParser = new DOMParser() - /** * Units */ diff --git a/src/constants/enums.js b/src/constants/enums.js deleted file mode 100644 index 1678b65..0000000 --- a/src/constants/enums.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * XMLHttp Request fetch was successful - */ -export const xhrSuccess = 1 - -/** - * XMLHttp Request fetch to url is in flight - */ -export const xhrExists = 2 - -/** - * XMLHttp Request was prevented from dispatched event - */ -export const xhrPrevented = 3 - -/** - * XMLHttp Request was prevented from dispatched event - */ -export const xhrEmpty = 4 - -/** - * XMLHttp Request failed to fetch - */ -export const xhrFailed = 5 diff --git a/src/index.js b/src/index.js index 63c8787..a587f8a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,8 @@ -import { Protocol, isReady } from './constants/regexp' -import { store } from './app/store' +import { Protocol } from './constants/regexp' +import { store, cache } from './app/store' +import { navigate } from './app/visit' +import { nanoid } from 'nanoid' +import { captureDOM } from './app/render' import * as controller from './app/controller' /** @@ -23,7 +26,7 @@ export const connect = options => { if (supported) { if (Protocol.test(window.location.protocol)) { - if (isReady.test(document.readyState)) controller.initialize() + addEventListener('DOMContentLoaded', controller.initialize) } else { console.error('Invalid protocol, pjax expects https or http protocol') } @@ -38,16 +41,33 @@ export const connect = options => { * * Reloads the current page */ -export const reload = () => { +export const reload = () => {} -} +/** + * UUID Generator + */ +export const uuid = (size = 12) => nanoid(size) + +/** + * Flush Cache + */ +export const flush = () => cache.clear() + +/** + * Capture DOM + * + * @param {string} url + * @param {object} action + */ +export const capture = (url, action) => captureDOM(url, action) /** * Visit * + * @param {string} url * @param {IPjax.IState} state */ -export const visit = state => controller.navigate(state) +export const visit = (url, state = store.page) => navigate({ ...state, url }) /** * Disconnect diff --git a/src/observers/history.js b/src/observers/history.js new file mode 100644 index 0000000..beca683 --- /dev/null +++ b/src/observers/history.js @@ -0,0 +1,97 @@ +import { store, cache } from '../app/store' +import { expandURL } from '../app/location' +import * as prefetch from '../app/prefetch' +import * as render from '../app/render' +import * as request from '../app/request' +import history from 'history/browser' + +/** + * @type {boolean} + */ +let started = false + +/** + * @type {function} + */ +let unlisten = null + +/** + * @type {string} + */ +let inTransit = null + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ + +/** + * Attached `history` event listener. + * + * @export + */ +export function start () { + + if (!started) { + unlisten = history.listen(listener) + started = false + } +} + +/** + * Removed `history` event listener. + * + * @export + */ +export function stop () { + + if (!started) { + unlisten() + started = true + } +} + +/** + * Popstate Navigations + * + * @param {string} url + */ +async function popstate (url) { + + prefetch.stop() + + if (url !== inTransit) request.cancel(inTransit) + + if (cache.has(url)) { + + render.update(cache.get(url), true) + + } else { + + inTransit = url + + const state = store.update.page({ location: expandURL(url), url }) + const response = await request.get(state) + + if (response && response.url === url) render.update(response, true) + + } + + prefetch.start() + +} + +/** + * Event History dispatch controller, handles popstate, + * push and replace events via third party module + * + * @param {import('history').BrowserHistory} event + */ +async function listener ({ action, location }) { + + const url = location.pathname + location.search + + if (action === 'POP') return popstate(url) + + console.log(action, location.state) + +} diff --git a/src/observers/hrefs.js b/src/observers/hrefs.js index 9f1a410..6a0dc36 100644 --- a/src/observers/hrefs.js +++ b/src/observers/hrefs.js @@ -1,170 +1,42 @@ -import { store } from '../app/store' -import { expandURL } from '../app/location' -import { navigate } from '../app/controller' -import { getActiveDOM } from '../app/render' -import { forEach, dispatchEvent, jsonAttrs, actionAttrs } from '../app/utils' +import { navigate, visitClickState } from '../app/visit' +import { dispatchEvent, linkEventValidate, linkLocator } from '../app/utils' import { Link } from '../constants/common' -import * as regexp from '../constants/regexp' +import { onMouseleave } from './mouseover' /** * @type {boolean} */ let started = false -/** - * @type {string[]} - */ -const attrs = [ - 'target', - 'method', - 'action', - 'prefetch', - 'cache', - 'progress', - 'throttle', - 'position', - 'reload' -] - /* -------------------------------------------- */ /* FUNCTIONS */ /* -------------------------------------------- */ /** - * Handles a clicked link, prevents special click types. - * - * @param {MouseEvent} event - * @return {boolean} - */ -export function linkEventValidate (event) { - - // @ts-ignore - return !((event.target && event.target.isContentEditable) || - event.defaultPrevented || - event.which > 1 || - event.altKey || - event.ctrlKey || - event.metaKey || - event.shiftKey) - -} - -/** - * Locted the closest link when click bubbles. - * - * @param {EventTarget} target - * The link `href` element target - * - * @param {string} selector - * The selector query name, eg: `[data-pjax]` - * - * @return {Element|false} - */ -export function linkLocator (target, selector) { - - return target instanceof Element ? target.closest(selector) : false -} - -/** - * Define state location navigation + * Attached `click` event listener. * * @export - * - * @param {Element} target - * The link `href` element target */ -function getLocation (target) { - - const location = expandURL(target.getAttribute('href')) +export function start () { - return { - location, - url: location.pathname + location.search + if (!started) { + addEventListener('click', captureClick, true) + started = true } -} - -/** - * Get State Page - * - * - * @param {Element} target - * The link `href` element target - * - * @return {IPjax.IState} - * Returns an updated page state object - */ -function getPageState (target) { - - const href = getLocation(target) - const url = href.location.pathname + href.location.search - - return store.cache.has(url) ? store.cache.get(url) : store.update.page(href) } /** - * Parses link `href` attributes and assigns them to - * configuration options. Each link target can define - * navigation options. - * - * @param {Element} target - * The link `href` element target - * - * @param {boolean} isPrefetch - * Boolean condition to determine is visit is a prefetch + * Removed `click` event listener. * - * @return {IPjax.IState} - * Returns an updated page state object + * @export */ -export function visitState (target, isPrefetch = false) { - - if (isPrefetch === false) getActiveDOM(store.page.url) - - const state = getPageState(target) - - forEach(attrs, prop => { - - const value = target.getAttribute(`data-pjax-${prop}`) - - if (value === null) { - - if ( - prop === 'prefetch' && - value !== 'hover' && - value !== 'intersect') state[prop] = false - - } else { - - state[prop] = prop === 'target' ? ( - - value.split(regexp.isWhitespace) - - ) : (prop === 'position' || prop === 'threshold') ? ( - - value.match(regexp.inPosition).reduce(jsonAttrs, {}) - - ) : regexp.isBoolean.test(value.trim()) ? ( - - value === 'true' - - ) : prop === 'action' ? ( - - actionAttrs(value) - - ) : regexp.isNumber.test(value.trim()) ? ( - - Number(value) - - ) : ( - - value.trim() - - ) - - } - }) +export function stop () { - return state + if (started) { + removeEventListener('click', captureClick, true) + started = false + } } @@ -175,7 +47,7 @@ export function visitState (target, isPrefetch = false) { * * @param {MouseEvent} event */ -function visitOnClick (event) { +function onClick (event) { if (linkEventValidate(event)) { @@ -183,10 +55,13 @@ function visitOnClick (event) { const target = linkLocator(event.target, Link) - if (target) { - if (target.tagName === 'A') { - const state = visitState(target, false) - if (dispatchEvent('pjax:click', state, true)) return navigate(state) + if (target && target.tagName === 'A') { + + const state = visitClickState(target) + + if (dispatchEvent('pjax:click', state, true)) { + if (state.method === 'prefetch') onMouseleave(event) + return navigate(state) } } } @@ -199,35 +74,7 @@ function visitOnClick (event) { */ function captureClick () { - removeEventListener('click', visitOnClick, false) - addEventListener('click', visitOnClick, false) - -} - -/** - * Attached `click` event listener. - * - * @export - */ -export function start () { - - if (!started) { - addEventListener('click', captureClick, true) - started = true - } - -} - -/** - * Removed `click` event listener. - * - * @export - */ -export function stop () { - - if (started) { - removeEventListener('click', captureClick, true) - started = false - } + removeEventListener('click', onClick, false) + addEventListener('click', onClick, false) } diff --git a/src/observers/intersect.js b/src/observers/intersect.js new file mode 100644 index 0000000..7a6583d --- /dev/null +++ b/src/observers/intersect.js @@ -0,0 +1,120 @@ +import { LinkPrefetchIntersect } from '../constants/common' +import { getURL } from '../app/location' +import { transit, cache, store } from '../app/store' +import { getPageState, getVisitConfig } from '../app/visit' +import * as request from '../app/request' + +/** + * @type IntersectionObserver + */ +let entries = null + +/** + * @type Boolean + */ +let started = false + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ + +/** + * Starts prefetch, will initialize `IntersectionObserver` and + * add event listeners and other logics. + * + * @export + */ +export function start () { + + if (store.config.prefetch) { + if (!started) { + entries = new IntersectionObserver(intersect) + getTargets(LinkPrefetchIntersect).forEach(observe) + started = true + } + } +} + +/** + * Stops prefetch, will disconnect `IntersectionObserver` and + * remove any event listeners or transits. + * + * @export + */ +export function stop () { + + if (store.config.prefetch) { + if (started) { + transit.clear() + entries.disconnect() + started = false + } + } +} + +/** + * Begin Observing `href` links + * + * @param {Element} target + * @memberof PrefetchObserver + */ +function observe (target) { + + return entries.observe(target) +} + +/** + * Start Intersection Observer and iterate over entries. + * + * @type {IntersectionObserverCallback} + */ +function intersect (entries) { + + return entries.forEach(onIntersection) +} + +/** + * Intersection callback when entries are in viewport. + * + * @param {IntersectionObserverEntry} params + */ +async function onIntersection ({ isIntersecting, target }) { + + if (isIntersecting) { + + const state = getVisitConfig(getPageState(target), target) + state.method = 'prefetch' + + const response = await request.get(state) + + if (response) { + entries.unobserve(target) + } else { + console.warn(`Pjax: Prefetch will retry at next intersect for: ${state.url}`) + entries.observe(target) + } + + } +} + +/** + * Link is not cached and can be fetched + * + * @param {Element} target + * @returns {boolean} + */ +function canFetch (target) { + + return !cache.has(getURL(target)) +} + +/** + * Returns a list of link elements to be prefetched. Filters out + * any links which exist in cache to prevent extrenous transit. + * + * @param {string} selector + */ +function getTargets (selector) { + + return [ ...document.body.querySelectorAll(selector) ].filter(canFetch) +} diff --git a/src/observers/mouseover.js b/src/observers/mouseover.js new file mode 100644 index 0000000..29d535f --- /dev/null +++ b/src/observers/mouseover.js @@ -0,0 +1,206 @@ +import { LinkPrefetchHover } from '../constants/common' +import { getCacheKeyFromTarget, getURL } from '../app/location' +import { transit, cache, store } from '../app/store' +import { linkEventValidate, linkLocator } from '../app/utils' +import { getPageState, getVisitConfig } from '../app/visit' +import * as request from '../app/request' + +/** + * @type Boolean + */ +let started = false + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ + +/** + * Starts prefetch, will initialize `IntersectionObserver` and + * add event listeners and other logics. + * + * @export + */ +export function start () { + + if (store.config.prefetch) { + if (!started) { + getTargets(LinkPrefetchHover).forEach(observe) + started = true + } + } +} + +/** + * Stops prefetch, will disconnect `IntersectionObserver` and + * remove any event listeners or transits. + * + * @export + */ +export function stop () { + + if (store.config.prefetch) { + if (started) { + transit.clear() + getTargets(LinkPrefetchHover).forEach(disconnect) + started = false + } + } +} + +/** + * Cancels prefetch, if mouse leaves target before threshold + * concludes. This prevents fetches being made for hovers that + * do not exceeds threshold. + * + * @param {MouseEvent} event + */ +export function onMouseleave (event) { + + if (linkEventValidate(event)) { + + const target = linkLocator(event.target, LinkPrefetchHover) + + if (target) { + cleanup(getURL(target)) + target.removeEventListener('mouseleave', onMouseleave, true) + // console.log('pjax: Prefetch cancelled, hover length too short') + } + } +} + +/** + * Attempts to visit location, Handles bubbled mousovers and + * Dispatches to the fetcher. Once item is cached, the mouseover + * event is removed. + * + * @param {MouseEvent} event + */ +function onMouseover (event) { + + if (linkEventValidate(event)) { + + const target = linkLocator(event.target, LinkPrefetchHover) + + if (target) { + + const state = getPageState(target) + + if (cache.has(state.url)) return disconnect(target) + + target.addEventListener('mouseleave', onMouseleave, true) + + throttle(state.url, () => { + + state.method = 'prefetch' + target.removeEventListener('mouseleave', onMouseleave, true) + + prefetch(getVisitConfig(state, target), newState => { + console.log('prefetch') + target.removeEventListener('mouseover', onMouseover, true) + }) + + }, store.config.threshold.hover) + + } + } +} + +/** + * Attach mouseover events to all defined element targets + * + * @param {EventTarget} target + */ +function observe (target) { + + target.addEventListener('mouseover', onMouseover, true) + +} + +/** + * Cleanup throttlers + * + * @param {string} url + * @memberof PrefetchObserver + */ +function cleanup (url) { + + clearTimeout(transit.get(url)) + transit.delete(url) + +} + +/** + * Fetch Throttle + * + * @param {string} url + * @param {function} fn + * @param {number} delay + */ +function throttle (url, fn, delay) { + + if (!cache.has(url) && !transit.has(url)) { + transit.set(url, setTimeout(fn, delay)) + } +} + +/** + * Fetch document and add the response to session cache. + * Lifecycle event `pjax:cache` will fire upon completion. + * + * @param {IPjax.IState} state + * The navigation configuration state for the requestd page + * + * @param {(status: IPjax.IState) => void} callback + * The `href` link target the prefetch was issued for + */ +async function prefetch (state, callback) { + + // console.log('prefetch', state.url) + try { + + const response = await request.get(state) + + callback(response) + + } catch (error) { + console.error(error) + console.info(`Endpoint "${state.url}" failed, will retry prefetch again`) + } + + cleanup(state.url) + +} + +/** + * Link is not cached and can be fetched + * + * @param {Element} target + * @returns {boolean} + */ +function canFetch (target) { + + return !cache.has(getCacheKeyFromTarget(target)) +} + +/** + * Returns a list of link elements to be prefetched. Filters out + * any links which exist in cache to prevent extrenous transit. + * + * @param {string} selector + */ +function getTargets (selector) { + + return [ ...document.body.querySelectorAll(selector) ].filter(canFetch) +} + +/** + * Adds and/or Removes click events. + * + * @param {EventTarget} target + */ +function disconnect (target) { + + target.removeEventListener('mouseleave', onMouseleave, false) + target.removeEventListener('mouseover', onMouseover, false) + +} diff --git a/src/observers/prefetch.js b/src/observers/prefetch.js deleted file mode 100644 index c48c162..0000000 --- a/src/observers/prefetch.js +++ /dev/null @@ -1,233 +0,0 @@ -import { xhrSuccess } from '../constants/enums' -import { LinkPrefetchHover, LinkPrefetchIntersect } from '../constants/common' -import { getCacheKeyFromTarget } from '../app/location' -import { store } from '../app/store' -import * as hrefs from './hrefs' -import * as request from '../app/request' - -/** - * @exports - * @type {Map} - */ -export const transit = new Map() - -/** - * @type IntersectionObserver - */ -let nodes - -/** - * @type Boolean - */ -let started = false - -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ - -/** - * Cleanup throttlers - * - * @param {string} url - * @memberof PrefetchObserver - */ -function cleanup (url) { - - clearTimeout(transit.get(url)) - - // remove request reference - transit.delete(url) - -} - -/** - * Fetch Throttle - * - * @param {string} url - * @param {function} fn - * @param {number} delay - */ -function fetchThrottle (url, fn, delay) { - - if (!store.cache.has(url) && !transit.has(url)) { - transit.set(url, setTimeout(fn, delay)) - } -} - -/** - * Fetch document and add the response to session cache. - * Lifecycle event `pjax:cache` will fire upon completion. - * - * @param {IPjax.IState} state - * The navigation configuration state for the requestd page - * - * @param {(status: number) => void} callback - * The `href` link target the prefetch was issued for - */ -async function prefetchRequest (state, callback) { - - // console.log('prefetch', state.url) - try { - - const response = await request.get(state) - - callback(response) - - } catch (error) { - console.error(error) - console.info(`Endpoint "${state.url}" failed, will retry prefetch again`) - } - - cleanup(state.url) - -} - -/** - * Attempts to visit location, Handles bubbled mousovers and - * Dispatches to the fetcher. Once item is cached, the mouseover - * event is removed. - * - * @param {MouseEvent} event - */ -function fetchOnHover (event) { - - if (hrefs.linkEventValidate(event)) { - - const target = hrefs.linkLocator(event.target, LinkPrefetchHover) - - if (target) { - - const state = hrefs.visitState(target, true) - - fetchThrottle(state.url, () => { - - prefetchRequest(state, status => { - if (status === xhrSuccess) { - target.removeEventListener('mouseover', fetchOnHover, true) - } - }) - - }, state.threshold.hover) - } - } -} - -/** - * Intersection callback when entries are in viewport. - * - * @param {IntersectionObserverEntry} params - */ -function OnIntersection ({ isIntersecting, target }) { - - if (isIntersecting) { - - const state = hrefs.visitState(target, true) - - fetchThrottle(state.url, () => { - - nodes.unobserve(target) - - prefetchRequest(state, status => { - if (status !== xhrSuccess) nodes.observe(target) - }) - - }, state.threshold.intersect) - } -} - -/** - * Begin Observing `href` links - * - * @param {Element} target - * @memberof PrefetchObserver - */ -function observeLinks (target) { - - return nodes.observe(target) -} - -/** - * Link is not cached and can be fetched - * - * @param {Element} target - * @returns {boolean} - */ -function canFetch (target) { - - return !store.cache.has(getCacheKeyFromTarget(target)) -} - -/** - * Returns a list of link elements to be prefetched. Filters out - * any links which exist in cache to prevent extrenous transit. - * - * @param {string} selector - */ -function getTargets (selector) { - - return Array.from(document.body.querySelectorAll(selector)).filter(canFetch) -} - -/** - * Adds and/or Removes click events. - * - * @param {EventTarget} target - */ -function disconnectHover (target) { - - return target.removeEventListener('mouseover', fetchOnHover, false) -} - -/** - * Adds and/or Removes click events. - * - * @param {EventTarget} target - */ -function observeHovers (target) { - - return target.addEventListener('mouseover', fetchOnHover, true) -} - -/** - * Start Intersection Observer and iterate over entries. - * - * @type {IntersectionObserverCallback} - */ -function observeIntersects (entries) { - - return entries.forEach(OnIntersection) -} - -/** - * Starts prefetch, will initialize `IntersectionObserver` and - * add event listeners and other logics. - * - * @export - */ -export function start () { - - if (!started) { - - nodes = new IntersectionObserver(observeIntersects) - getTargets(LinkPrefetchIntersect).forEach(observeLinks) - getTargets(LinkPrefetchHover).forEach(observeHovers) - started = true - } -} - -/** - * Stops prefetch, will disconnect `IntersectionObserver` and - * remove any event listeners or transits. - * - * @export - */ -export function stop () { - - if (started) { - - transit.clear() - nodes.disconnect() - getTargets(LinkPrefetchHover).forEach(disconnectHover) - started = false - } -} diff --git a/src/observers/scrolling.js b/src/observers/scrolling.js new file mode 100644 index 0000000..b11424a --- /dev/null +++ b/src/observers/scrolling.js @@ -0,0 +1,77 @@ + +/** + * @type {boolean} + */ +let started = false + +/** + * @type {IPjax.IPosition} + */ +const position = { x: 0, y: 0 } + +/* -------------------------------------------- */ +/* FUNCTIONS */ +/* -------------------------------------------- */ + +/** + * Attached `scroll` event listener. + * + * @export + */ +export function start () { + + if (!started) { + addEventListener('scroll', onScroll, false) + onScroll() + started = true + } + +} + +/** + * Removed `scroll` event listener. + * + * @export + */ +export function stop () { + if (started) { + removeEventListener('scroll', onScroll, false) + started = false + } +} + +/** + * Returns to current scroll position + * + * @export + */ +export function getPosition () { + + return position +} + +/** + * Resets scroll position + * + * @export + */ +export function reset () { + + position.x = 0 + position.y = 0 + + return position + +} + +/** + * onScroll event + * + * @export + */ +export function onScroll () { + + position.x = window.pageXOffset + position.y = window.pageYOffset + +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..2a4e2c4 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,46 @@ +declare module "@brixtol/pjax" { + + /** + * Pjax Support + */ + export const supported: boolean + + /** + * Fetches state page by url. Pass `false` to clear cache + */ + export function connect(options?: IPjax.IConfigPresets): IPjax.IState + + /** + * Fetches state page by url. Pass `false` to clear cache + */ + export function cache(ref?: string | false): IPjax.IState + + /** + * Reloads the current page + */ + export function reload(): void + + /** + * Shortcut helper function for generating a UUID using nanoid. + */ + export function uuid(size?: string): string + + /** + * Captures current `` element and upon next history visit + * will use the capture as replacement. + */ + export function capture(url: string): void + + /** + * Programmatic visit to location + */ + export function visit(url: string, options?: IPjax.IVisit): void + + /** + * Removes all pjax listeners + */ + export function disconnect(): void + +} + + diff --git a/types/pjax.d.ts b/types/pjax.d.ts deleted file mode 100644 index 7a57253..0000000 --- a/types/pjax.d.ts +++ /dev/null @@ -1,37 +0,0 @@ - -interface Pjax { - - /** - * Pjax Support - */ - supported: boolean - - /** - * Fetches state page by url. Pass `false` to clear cache - */ - connect(options: IPjax.IConfigPresets): IPjax.IState - - /** - * Fetches state page by url. Pass `false` to clear cache - */ - cache(ref?: string | false): IPjax.IState - - /** - * Reloads the current page - */ - reload(): void - - /** - * Programmatic visit to location - */ - visit(url: string, options: IPjax.IVisit): void - - /** - * Removes all pjax listeners - */ - disconnect(): void - - -} - -export default Pjax diff --git a/types/store.d.ts b/types/store.d.ts index 9c37af8..9230b74 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -3,6 +3,7 @@ */ export type IEvents = ( 'pjax:click' | + 'pjax:prefetch' | 'pjax:request' | 'pjax:cache' | 'pjax:render' | @@ -81,6 +82,14 @@ export interface ILocation { * '?param=foo&bar=baz' */ search: string + /** + * The previous path href URL. + * This is also the cache identifier + * + * @example + * '/pathname' OR '/pathname?foo=bar' + */ + lastPath: string } @@ -131,6 +140,38 @@ export type IConfigPresets = { */ throttle?: number + /** + * Threshold Controls + */ + threshold?: { + + /** + * Define an intersection threshold timeout from + * which intersected elements will begin fetching + * after being observed + * + * @default 250 + */ + intersect?: number, + + /** + * Define hover timeout from which fetching will begin + * after time spent on mouseover + * + * @default 100 + */ + hover?: number, + + /** + * Controls the progress bar threshold, where `1` equates + * to 25ms, maximum of `85` + * + * @default 2 + */ + progress?: number + + } + } @@ -239,15 +280,49 @@ export interface IConfig { * * (_Requests are instantaneous, generally you wont need this_) * - * @default false + * @default true */ progress?: boolean, + + /** + * Threshold Controls + */ + threshold?: { + + /** + * Define an intersection threshold timeout from + * which intersected elements will begin fetching + * after being observed + * + * @default 250 + */ + intersect?: number, + + /** + * Define hover timeout from which fetching will begin + * after time spent on mouseover + * + * @default 100 + */ + hover?: number, + + /** + * Controls the progress bar threshold, where `1` equates + * to 25ms, maximum of `85` + * + * @default 2 + */ + progress?: number + + } + } export interface IDom { readonly tracked?: Set, head?: object + snapshots: Map } @@ -289,11 +364,16 @@ export interface IState extends IConfig { */ snapshot?: string + /** + * Captured HTML response string + */ + captured?: string + /** * The fetched HTML response string */ - chunks?: { - [selector: string]: 'replace' | 'prepend' | 'append' + targets?: { + [selector: string]: Element[] } /** @@ -306,10 +386,16 @@ export interface IState extends IConfig { */ location?: ILocation + /** + * The Document title + */ + title?: string + /** * Action */ action?: { + replace?: [target:string] append?: Array<[from: string, to: string]>, prepend?: Array<[from: string, to: string]>, } From ae85e7ec5f113c3df34b3ec54f7e8d997fa287a8 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 16:08:16 +0100 Subject: [PATCH 11/60] set initial --- src/app/controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/controller.js b/src/app/controller.js index 113b15a..5b809ae 100644 --- a/src/app/controller.js +++ b/src/app/controller.js @@ -18,12 +18,14 @@ function setInitialCache (event) { const location = expandURL(window.location.href) const state = store.update.page({ - title: document.title, - url: location.pathname + location.search, + url: location.lastUrl, snapshot: render.DOMSnapshot(document), + title: document.title, location }) + console.log(state) + cache.set(state.url, state) } From e1008dfccf6d1a7a89ecb85222ae7ba2c5a7a254 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 16:08:36 +0100 Subject: [PATCH 12/60] modify location exports --- src/app/location.js | 94 ++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 61 deletions(-) diff --git a/src/app/location.js b/src/app/location.js index 4c6ed76..cdd85d4 100644 --- a/src/app/location.js +++ b/src/app/location.js @@ -3,79 +3,37 @@ import history from 'history/browser' /** * Expands URL href location. * - * @param {string} url + * @param {string} anchor * @returns {IPjax.ILocation} */ -export function expandURL (url) { +export function expandURL (anchor) { - const lastPath = history.createHref(window.location) + const lastUrl = history.createHref(window.location) const location = document.createElement('a') - location.href = url.toString() + + location.href = anchor.toString() const { - protocol - , origin - , hostname - , href - , pathname - , search + origin, + hostname, + href, + pathname, + search, + hash } = new URL(location.href) return { - protocol - , origin + origin , hostname , href , pathname , search - , lastPath + , hash + , lastUrl } } -/** - * Hash URL using DJB2A algorithm - * - * @export - * @param {string} url - * @return {string} - */ -export function getUID (url) { - - let i = 0 - let hash = 5381 - - for (; i < url.length; i++) hash = ((hash << 5) + hash) ^ url.charCodeAt(i) - - return (hash >>> 0).toString(16) - -} - -/** - * Returns last pathname value - * - * @param {URL} url - */ -export const getLocation = ( - { - href, - pathname, - search, - origin, - hostname, - protocol - } -) => ( - { - protocol - , origin - , hostname - , href - , pathname - , search - } -) - /** * Returns the current URL * @@ -84,8 +42,9 @@ export const getLocation = ( export function getURL (target) { const href = target instanceof Element ? target.getAttribute('href') : target - const { pathname, search } = expandURL(href) - return pathname + search + + return history.createHref(expandURL(href)) + } /** @@ -96,8 +55,21 @@ export function getURL (target) { export const getCacheKeyFromTarget = target => getURL(target) /** - * Returns the protocol and host + * Hash URL using DJB2A algorithm + * + * NOT IN USE - REMOVE IN NEXT RELEASE * - * @param {URL} location + * @export + * @param {string} url + * @return {string} */ -export const getProtocol = ({ protocol, host }) => protocol.replace(/:/g, `://${host}`) +export function getCacheKey (url) { + + let i = 0 + let hash = 5381 + + for (; i < url.length; i++) hash = ((hash << 5) + hash) ^ url.charCodeAt(i) + + return (hash >>> 0).toString(16) + +} From dc279a3cdd7c1c2b5055584194fdd0672f22f180 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 16:09:45 +0100 Subject: [PATCH 13/60] pass location object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit combat the fuckery issue from history.js – React devs smh. --- src/app/render.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/render.js b/src/app/render.js index c75c232..0db5c76 100644 --- a/src/app/render.js +++ b/src/app/render.js @@ -208,7 +208,7 @@ export function captureDOM (url, options) { } else if (options.action === 'capture') { state.captured = DOMSnapshot(target) - history.replace(state.location.href, state) + history.replace(state.location.url, state) console.info('Pjax: DOM Captured at: ' + state.captured) } @@ -246,19 +246,17 @@ export function update (state, popstate = false) { if (!popstate) { - const { pathname, search } = history.location - - if ((pathname + search) === state.url) { - history.replace(state.location.href, state) + if (history.createHref(history.location) === state.url) { + history.replace(state.location, state) } else { - history.push(state.location.href, state) + history.push(state.location, state) } } else if (typeof state?.captured === 'string') { if (snapshots.delete(uuid)) { state.captured = null - history.replace(state.location.href, state) + history.replace(state.location, state) console.info('Pjax: Captured snapshot removed at: ' + state.url) } From e40fb35fb6596a048f85eb7c7e68dfa411b82bbe Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 16:09:52 +0100 Subject: [PATCH 14/60] Update request.js --- src/app/request.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/request.js b/src/app/request.js index 61319ff..c4caeef 100644 --- a/src/app/request.js +++ b/src/app/request.js @@ -141,8 +141,6 @@ export async function inFlight (url, limit = 0) { if (limit === store.config.threshold.progress) progress.show() } - console.log('waiting', url, limit) - return asyncTimeout(() => inFlight(url, (limit + 1)), 25) } @@ -161,19 +159,19 @@ export async function inFlight (url, limit = 0) { * @param {boolean} [async=false] * The XHR request is a asynchronous request or not * - * @return {Promise} + * @return {Promise} * A boolean response representing a successful or failed fetch */ export async function get (state, async = true) { if (requests.has(state.url)) { console.warn(`Pjax: XHR Request is already in transit for: ${state.url}`) - return null + return false } if (!dispatchEvent('pjax:request', state.location, true)) { console.warn(`Pjax: Request cancelled via dispatched event for: ${state.url}`) - return null + return false } if (state.method !== 'prefetch') { @@ -205,10 +203,12 @@ export async function get (state, async = true) { } } catch (error) { + requests.delete(state.url) console.error(error) + } - return null + return false } From f7ee595c00c34970497e38c4481909eda0dcb964 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Thu, 18 Mar 2021 16:09:58 +0100 Subject: [PATCH 15/60] various --- src/app/store.js | 16 +++------------- src/app/utils.js | 7 +------ src/app/visit.js | 9 ++++++--- src/index.js | 3 ++- src/observers/history.js | 6 ++---- src/observers/hrefs.js | 6 +++--- types/store.d.ts | 18 +++++++++--------- 7 files changed, 26 insertions(+), 39 deletions(-) diff --git a/src/app/store.js b/src/app/store.js index 4cdb0d4..c30c2c9 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -82,14 +82,6 @@ export const store = ( } - , - - get location () { - - return state.page.url - - } - , /* -------------------------------------------- */ /* STORE GETTERS */ @@ -181,13 +173,13 @@ export const store = ( append: null }, location: { - protocol: '', origin: '', hostname: '', href: '', pathname: '', search: '', - lastPath: '' + hash: '', + lastUrl: '' }, position: { x: 0, @@ -200,6 +192,4 @@ export const store = ( }) -)( - Object.create(null) -) +)(Object.create(null)) diff --git a/src/app/utils.js b/src/app/utils.js index 3f47f64..9370d93 100644 --- a/src/app/utils.js +++ b/src/app/utils.js @@ -149,12 +149,7 @@ export function byteConvert (bytes) { */ export function asyncTimeout (callback, ms = 0) { - return new Promise( - resolve => setTimeout(() => { - const response = callback() - return resolve(response) - }, ms) - ) + return new Promise(resolve => setTimeout(() => resolve(callback()), ms)) } /** diff --git a/src/app/visit.js b/src/app/visit.js index ddec100..1804704 100644 --- a/src/app/visit.js +++ b/src/app/visit.js @@ -6,6 +6,7 @@ import * as scroll from '../observers/scrolling' import * as prefetch from './prefetch' import * as render from './render' import * as request from './request' +import history from 'history/browser' /** * @type {string[]} @@ -38,7 +39,7 @@ const attrs = [ export function getPageState (target) { const location = expandURL(target.getAttribute('href')) - const url = location.pathname + location.search + const url = history.createHref(location) return cache.has(url) ? cache.get(url) : store.update.page({ url, @@ -131,8 +132,8 @@ export function getVisitConfig (state, target) { */ export function navigate (state) { - // console.log(state, cache.has(state.url)) - cache.get(state.location.lastPath).position = scroll.getPosition() + console.log(state, state.location.lastUrl) + cache.get(state.location.lastUrl).position = scroll.getPosition() state.position = scroll.reset() if (cache.has(state.url)) { @@ -154,6 +155,8 @@ export async function cacheVisit (url) { const state = cache.get(url) + console.log(state) + if (store.config.prefetch) prefetch.stop() // ensure we have state before updating diff --git a/src/index.js b/src/index.js index a587f8a..d1af8a8 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,8 @@ import * as controller from './app/controller' export const supported = !!( window.history.pushState && window.requestAnimationFrame && - window.addEventListener + window.addEventListener && + window.DOMParser ) /** diff --git a/src/observers/history.js b/src/observers/history.js index beca683..882bbbb 100644 --- a/src/observers/history.js +++ b/src/observers/history.js @@ -59,7 +59,7 @@ async function popstate (url) { prefetch.stop() - if (url !== inTransit) request.cancel(inTransit) + if (inTransit && url !== inTransit) request.cancel(inTransit) if (cache.has(url)) { @@ -88,9 +88,7 @@ async function popstate (url) { */ async function listener ({ action, location }) { - const url = location.pathname + location.search - - if (action === 'POP') return popstate(url) + if (action === 'POP') return popstate(history.createHref(location)) console.log(action, location.state) diff --git a/src/observers/hrefs.js b/src/observers/hrefs.js index 6a0dc36..a3b9684 100644 --- a/src/observers/hrefs.js +++ b/src/observers/hrefs.js @@ -20,7 +20,7 @@ let started = false export function start () { if (!started) { - addEventListener('click', captureClick, true) + addEventListener('click', observe, true) started = true } @@ -34,7 +34,7 @@ export function start () { export function stop () { if (started) { - removeEventListener('click', captureClick, true) + removeEventListener('click', observe, true) started = false } @@ -72,7 +72,7 @@ function onClick (event) { * * @private */ -function captureClick () { +function observe () { removeEventListener('click', onClick, false) addEventListener('click', onClick, false) diff --git a/types/store.d.ts b/types/store.d.ts index 9230b74..12da4fe 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -37,14 +37,6 @@ export type IPosition = { * The URL location object */ export interface ILocation { - - /** - * The URL protocol - * - * @example - * 'https:' OR 'http:' - */ - protocol: string /** * The URL origin name * @@ -82,6 +74,13 @@ export interface ILocation { * '?param=foo&bar=baz' */ search: string + /** + * The URL Hash + * + * @example + * '#foo' + */ + hash: string /** * The previous path href URL. * This is also the cache identifier @@ -89,7 +88,7 @@ export interface ILocation { * @example * '/pathname' OR '/pathname?foo=bar' */ - lastPath: string + lastUrl: string } @@ -381,6 +380,7 @@ export interface IState extends IConfig { */ url?: string + /** * Location URL */ From 9d2b84556fd88ddcf972dcf46e14db1d08948e45 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Fri, 19 Mar 2021 07:47:30 +0100 Subject: [PATCH 16/60] overhaul entire project fixes all JSDoc comments and basic rework --- package.json | 5 +- src/app/controller.js | 18 ++-- src/app/location.js | 11 ++- src/app/prefetch.js | 6 +- src/app/progress.js | 4 +- src/app/render.js | 11 ++- src/app/request.js | 42 +++----- src/app/store.js | 32 +++--- src/app/utils.js | 83 +++++++++------- src/app/visit.js | 195 ++++++++++++++++++++++++------------- src/constants/common.js | 17 +++- src/constants/regexp.js | 74 +++++++++++--- src/observers/history.js | 35 ++++--- src/observers/hrefs.js | 35 ++++--- src/observers/intersect.js | 36 ++----- src/observers/mouseover.js | 90 +++++++---------- src/observers/scrolling.js | 69 +++++++++++-- types/state.d.ts | 194 ++++++++++++++++++++++++++++++++++++ types/store.d.ts | 14 ++- 19 files changed, 670 insertions(+), 301 deletions(-) create mode 100644 types/state.d.ts diff --git a/package.json b/package.json index 69ad864..88205a6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "eslintConfig": { "extends": [ "@brixtol/eslint-config-javascript" - ] + ], + "rules": { + "multiline-ternary": "off" + } }, "ava": { "files": [ diff --git a/src/app/controller.js b/src/app/controller.js index 5b809ae..815ea4b 100644 --- a/src/app/controller.js +++ b/src/app/controller.js @@ -9,10 +9,11 @@ import * as render from './render' /** * Sets initial page state on landing page and - * caches it so return navigation don't perform an extrenous - * request + * caches it so return navigation don't perform + * an extrenous request. * * @param {Event} event + * @returns {Map} */ function setInitialCache (event) { @@ -24,14 +25,15 @@ function setInitialCache (event) { location }) - console.log(state) - - cache.set(state.url, state) + return cache.set(state.url, state) } /** * Initialize + * + * @exports + * @returns {void} */ export function initialize () { @@ -45,10 +47,8 @@ export function initialize () { addEventListener('load', setInitialCache, false) - console.info('Pjax: Connection Established ⚡') - store.started = true - + console.info('Pjax: Connection Established ⚡') } } @@ -57,6 +57,7 @@ export function initialize () { * Destory Pjax instances * * @exports + * @returns {void} */ export function destroy () { @@ -68,7 +69,6 @@ export function destroy () { mouseover.start() intersect.start() cache.clear() - store.started = false console.warn('Pjax: Instance has been disconnected! 😔') diff --git a/src/app/location.js b/src/app/location.js index cdd85d4..b37e07e 100644 --- a/src/app/location.js +++ b/src/app/location.js @@ -3,15 +3,16 @@ import history from 'history/browser' /** * Expands URL href location. * - * @param {string} anchor + * @export + * @param {string} url * @returns {IPjax.ILocation} */ -export function expandURL (anchor) { +export function expandURL (url) { const lastUrl = history.createHref(window.location) const location = document.createElement('a') - location.href = anchor.toString() + location.href = url.toString() const { origin, @@ -37,7 +38,9 @@ export function expandURL (anchor) { /** * Returns the current URL * + * @export * @param {Element|string} target + * @return {string} */ export function getURL (target) { @@ -50,7 +53,9 @@ export function getURL (target) { /** * Returns the pathname from `href` target used for cache key. * + * @export * @param {Element} target + * @return {string} */ export const getCacheKeyFromTarget = target => getURL(target) diff --git a/src/app/prefetch.js b/src/app/prefetch.js index 32ac1ef..38824c1 100644 --- a/src/app/prefetch.js +++ b/src/app/prefetch.js @@ -6,7 +6,8 @@ import * as intersect from '../observers/intersect' * Starts prefetch, will initialize `IntersectionObserver` and * add event listeners and other logics. * - * @export + * @exports + * @returns {void} */ export function start () { @@ -20,7 +21,8 @@ export function start () { * Stops prefetch, will disconnect `IntersectionObserver` and * remove any event listeners or transits. * - * @export + * @exports + * @returns {void} */ export function stop () { diff --git a/src/app/progress.js b/src/app/progress.js index 00ae963..89e32f9 100644 --- a/src/app/progress.js +++ b/src/app/progress.js @@ -32,7 +32,7 @@ export let loading = false /** * Show Progress Bar * - * @export + * @exports */ export function show () { @@ -66,7 +66,7 @@ export function show () { /** * Hide Progress Bar * - * @export + * @exports */ export function hide () { diff --git a/src/app/render.js b/src/app/render.js index 0db5c76..a35f016 100644 --- a/src/app/render.js +++ b/src/app/render.js @@ -5,6 +5,7 @@ import { getURL } from './location' import { nanoid } from 'nanoid' import history from 'history/browser' import * as progress from './progress' +import * as prefetch from './prefetch' /* -------------------------------------------- */ /* FUNCTIONS */ @@ -147,6 +148,7 @@ function runActions () { /** * Get targets * + * @exports * @param {Document} element * @param {IPjax.IState} state */ @@ -169,7 +171,7 @@ export function getTargets ({ body }, state) { * Captures current document element and sets a * record to snapshot state * - * @export + * @exports * @param {Document} target * @returns {string} */ @@ -185,7 +187,7 @@ export function DOMSnapshot (target) { /** * Updates cached DOM * - * @export + * @exports * @param {string} url * @param {object} options */ @@ -221,6 +223,7 @@ export function captureDOM (url, options) { * using `DomParser()` method. Cached pages will pass * the saved response here. * + * @exports * @param {string} data * @return {Document} */ @@ -233,9 +236,9 @@ export function DOMParse (data) { * Update the DOM and execute page adjustments * to new navigation point * + * @exports * @param {IPjax.IState} state * @param {boolean} [popstate=false] - * @memberof Render */ export function update (state, popstate = false) { @@ -291,4 +294,6 @@ export function update (state, popstate = false) { dispatchEvent('pjax:load', state) + prefetch.start() + } diff --git a/src/app/request.js b/src/app/request.js index c4caeef..237a0c9 100644 --- a/src/app/request.js +++ b/src/app/request.js @@ -16,33 +16,27 @@ let storage = 0 /** * Executes on request end. Removes the XHR recrod and update * the response DOMString cache size record. - - * @param {string} url - * DOM string, equivelent to`document.documentElement.outerHTML` * + * @exports + * @param {string} url * @param {string} DOMString - * DOM string, equivelent to`document.documentElement.outerHTML` - * - * @returns {void} - * Executes asynchronously, to prevent any delayed between requests + * @returns {boolean} */ function HttpRequestEnd (url, DOMString) { storage = storage + byteSize(DOMString) - requests.delete(url) + + return requests.delete(url) } /** * Fetch XHR Request wrapper function * + * @exports * @param {IPjax.IState} state - * The `location.href`request address - * * @param {boolean} [async=false] - * The XHR request is a asynchronous request or not - * - * DOM string, equivelent to `document.documentElement.outerHTML` + * @returns {Promise} */ async function HttpRequest ({ url, @@ -86,6 +80,8 @@ async function HttpRequest ({ /** * Returns request cache metrics * + * @exports + * @returns {IPjax.ICacheSize} */ export function cacheSize () { @@ -100,11 +96,9 @@ export function cacheSize () { /** * Cancels the request in transit * + * @exports * @param {string} url - * The `cacheKey` url identifier - * * @returns {void} - * The request will either be aborted or warn in console if failed */ export function cancel (url) { @@ -124,15 +118,15 @@ export function cancel (url) { * event dispatched this will prevent multiple requests and * instead wait for initial fetch to complete. * - * @param {string} url - * The `cacheKey` url identifier - * - * @param {number} [limit=0] * Number of recursive runs to make, set this to 85 to disable, * else just leave it to execute as is. * - * @return {Promise} * Returns `true` if request resolved in `850ms` else `false` + * + * @exports + * @param {string} url + * @param {number} [limit=0] + * @return {Promise} */ export async function inFlight (url, limit = 0) { @@ -153,14 +147,10 @@ export async function inFlight (url, limit = 0) { * Fetches documents and guards from duplicated requests * from being dispatched if an indentical fetch is in flight. * + * @exports * @param {IPjax.IState} state - * The page state object acquired from link - * * @param {boolean} [async=false] - * The XHR request is a asynchronous request or not - * * @return {Promise} - * A boolean response representing a successful or failed fetch */ export async function get (state, async = true) { diff --git a/src/app/store.js b/src/app/store.js index c30c2c9..13564f9 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -118,9 +118,10 @@ export const store = ( /* CONFIG ------------------------------------- */ config: ( - initial => patch => ( - state.config = merge(initial, patch) - ) + initial => + patch => ( + state.config = merge(initial, patch) + ) )( { target: [ 'main', '#navbar' ], @@ -142,20 +143,21 @@ export const store = ( /* PAGE --------------------------------------- */ page: ( - initial => patch => ( - state.page = merge( - initial, - { - ...patch - , target: state.config.target - , action: { - replace: null, - append: null, - prepend: null + initial => + patch => ( + state.page = merge( + initial, + { + ...patch + , target: state.config.target + , action: { + replace: null, + append: null, + prepend: null + } } - } + ) ) - ) )( { url: '', diff --git a/src/app/utils.js b/src/app/utils.js index 9370d93..bb100c3 100644 --- a/src/app/utils.js +++ b/src/app/utils.js @@ -1,13 +1,16 @@ import { isNumber } from '../constants/regexp' import { Units } from './../constants/common' +import { getURL } from './location' +import { cache } from './store' /** * Handles a clicked link, prevents special click types. * + * @exports * @param {MouseEvent} event * @return {boolean} */ -export function linkEventValidate (event) { +export function linkEvent (event) { // @ts-ignore return !((event.target && event.target.isContentEditable) || @@ -23,15 +26,12 @@ export function linkEventValidate (event) { /** * Locted the closest link when click bubbles. * + * @exports * @param {EventTarget} target - * The link `href` element target - * * @param {string} selector - * The selector query name, eg: `[data-pjax]` - * * @return {Element|false} */ -export function linkLocator (target, selector) { +export function linkLocate (target, selector) { return target instanceof Element ? target.closest(selector) : false } @@ -40,6 +40,7 @@ export function linkLocator (target, selector) { * Constructs a JSON object from HTML `data-pjax-*` attributes. * Attributes are passed in as array items * + * @exports * @param {object} accumulator * @param {string} current * @param {number} index @@ -59,7 +60,7 @@ export function linkLocator (target, selector) { * { string: 'foo', number: 200 } * */ -export function jsonAttrs (accumulator, current, index, source) { +export function jsonattrs (accumulator, current, index, source) { return (index % 2 ? ({ ...accumulator @@ -73,7 +74,7 @@ export function jsonAttrs (accumulator, current, index, source) { /** * Array Chunk function * - * @export + * @exports * @param {number} [size=2] * @return {(acc: any[], value: string) => any[]} */ @@ -90,17 +91,10 @@ export function chunk (size = 2) { /** * Dispatches lifecycle events on the document. * - * @export - * + * @exports * @param {IPjax.IEvents} eventName - * The event name to be created - * * @param {object} detail - * Details to be passed to event dispatch - * * @param {boolean} cancelable - * Whether the event can be cancelled via `preventDefault()` - * * @return {boolean} */ export function dispatchEvent (eventName, detail, cancelable = false) { @@ -115,18 +109,47 @@ export function dispatchEvent (eventName, detail, cancelable = false) { /** * Returns the byte size of a string value * + * @exports * @param {string} string + * @returns {number} */ export function byteSize (string) { return new Blob([ string ]).size } +/** + * Link is not cached and can be fetched + * + * @exports + * @param {Element} target + * @returns {boolean} + */ +export function canFetch (target) { + + return !cache.has(getURL(target)) +} + +/** + * Returns a list of link elements to be prefetched. Filters out + * any links which exist in cache to prevent extrenous transit. + * + * @exports + * @param {string} selector + * @returns {Element[]} + */ +export function getTargets (selector) { + + return [ ...document.body.querySelectorAll(selector) ].filter(canFetch) +} + /** * Converts byte size to killobyte, megabyre, * gigabyte or terrabyte * + * @exports * @param {number} bytes + * @returns {string} */ export function byteConvert (bytes) { @@ -144,8 +167,10 @@ export function byteConvert (bytes) { /** * Async Timeout * + * @exports * @param {function} callback * @param {number} ms + * @returns {Promise} */ export function asyncTimeout (callback, ms = 0) { @@ -156,15 +181,10 @@ export function asyncTimeout (callback, ms = 0) { * Each iterator helper function. Provides a util function * for loop iterations * - * + * @exports * @param {any} list - * An array list of items to iterate over - * - * @param {(item: Element | any, index?: number) => any} fn - * Callback function to be executed for each iteration - * + * @param {(item: Element | any, index?: number) => any} fn * * @param {{index?: boolean }} [index=flase] - * * @return {void} */ export function forEach (list, fn, { index = false } = {}) { @@ -176,12 +196,10 @@ export function forEach (list, fn, { index = false } = {}) { /** * Get Element attributes * + * @exports * @param {Element} element - * The element to parse for attributes - * * @param {string[]} exclude - * List of attributes to be excluded - * + * @returns {[name:string, value: string][]} */ export function getElementAttrs ({ attributes }, exclude = []) { @@ -202,17 +220,14 @@ export function getElementAttrs ({ attributes }, exclude = []) { /** * Each Selector * + * @exports * @param {Document} document - * The document Element - * - * @param {string} query - * The element selector - * + * @param {string} query * * @param {(element: Element) => void} callback - * The callback function + * @returns {void} */ export function eachSelector ({ body }, query, callback) { - return [].slice.call(body.querySelectorAll(query)).forEach(callback) + return [ ...body.querySelectorAll(query) ].forEach(callback) } diff --git a/src/app/visit.js b/src/app/visit.js index 1804704..ce4e81e 100644 --- a/src/app/visit.js +++ b/src/app/visit.js @@ -1,6 +1,6 @@ import { cache, store, transit } from './store' import { expandURL } from '../app/location' -import { forEach, jsonAttrs, chunk } from '../app/utils' +import { forEach, jsonattrs, chunk } from '../app/utils' import * as regexp from '../constants/regexp' import * as scroll from '../observers/scrolling' import * as prefetch from './prefetch' @@ -29,12 +29,9 @@ const attrs = [ /** * Get State Page * - * + * @export * @param {Element} target - * The link `href` element target - * * @return {IPjax.IState} - * Returns an updated page state object */ export function getPageState (target) { @@ -53,11 +50,9 @@ export function getPageState (target) { * configuration options. Each link target can define * navigation options. * + * @export * @param {Element} target - * The link `href` element target - * * @return {IPjax.IState} - * Returns an updated page state object */ export function visitClickState (target) { @@ -73,14 +68,10 @@ export function visitClickState (target) { * Parses link `href` attributes and assigns them to * configuration options. * + * @export * @param {IPjax.IState} state - * Current state configuration - * * @param {Element} target - * The link `href` element target - * * @return {IPjax.IState} - * Returns an updated page state object */ export function getVisitConfig (state, target) { @@ -88,39 +79,113 @@ export function getVisitConfig (state, target) { const value = target.getAttribute(`data-pjax-${prop}`) - if (value === null) { + value === null ? ( - if ( - prop === 'prefetch' && - value !== 'hover' && - value !== 'intersect') state[prop] = false + // MONKEY PATCH + // Assert prefetch value to `false` if no prefetch attribute is defined + prop === 'prefetch' && value !== 'hover' && value !== 'intersect') || ( - } else { + state[prop] = false + + // data-pjax-prefetch="false" + + ) : regexp.isAction.test(prop) ? ( + + state.action[prop] = prop === 'replace' ? ( + + value.match(regexp.ActionParams) + + // data-pjax-replace="(['.foo'])" + // data-pjax-replace="(['.foo','.bar'])" + + ) : ( + + value.match(regexp.ActionParams).reduce(chunk(2), []) + + // data-pjax-append="(['.foo', '.bar'])" + // data-pjax-append="(['.foo', '.bar'],['.baz','.faz'])" + + // ---------- OR --------------- + + // data-pjax-prepend="(['.foo', '.bar'])" + // data-pjax-prepend="(['.foo', '.bar'],['.baz','.faz'])" + + ) + ) : ( + + // DEPRECATE IN FAVOR OF REPLACE, APPEND OR PREPEND ACTIONS + // + state[prop] = prop === 'target' ? ( + + value.split(regexp.isWhitespace) + + // data-pjax-target=".foo" + // data-pjax-target=".foo .bar #baz" + + ) : prop === 'progress' ? ( + + (value === 'false' || value === '0' || Number(value) > 85) + ? false + : (Number(value) < 85 || Number(value) > 1) ? Number(value) : state.progress + + // data-pjax-progress="50" + // data-pjax-progress="true" + // data-pjax-progress="false" + + ) : prop === 'cache' ? ( + + value === 'false' + ? false + : value === 'true' + ? true + : value.trim() + + // data-pjax-cache="true" + // data-pjax-cache="false" + // data-pjax-cache="flush" + // data-pjax-cache="reset" + + ) : prop === 'position' ? ( + + value.match(regexp.isPosition).reduce(jsonattrs, {}) + + // data-pjax-position="x:200" + // data-pjax-position="x:1000 y:0" + + ) : regexp.isBoolean.test(value.trim()) ? ( + + value === 'true' + + // data-pjax-*="true" + // data-pjax-*="false" + + ) : regexp.isNumber.test(value.trim()) ? ( + + Number(value) + + // data-pjax-*="1000" + // data-pjax-*="10.00" + + ) : ( + + value.trim() + + // data-pjax-*="string" + + ) + ) - if (/\b(append|prepend|replace)\b/.test(prop)) { - - state.action[prop] = prop === 'replace' ? ( - value.match(regexp.ActionParams) - ) : ( - value.match(regexp.ActionParams).reduce(chunk(2), []) - ) - } else { - - state[prop] = prop === 'target' ? ( - value.split(regexp.isWhitespace) - ) : (prop === 'position' || prop === 'threshold') ? ( - value.match(regexp.inPosition).reduce(jsonAttrs, {}) - ) : regexp.isBoolean.test(value.trim()) ? ( - value === 'true' - ) : regexp.isNumber.test(value.trim()) ? ( - Number(value) - ) : ( - value.trim() - ) - } - } }) + // THRESHOLD CONFIGURATION + // SET THE THRESHOLD STATE RELATING TO THE PREFETCH + if (target.hasAttribute('data-pjax-threshold')) { + if (regexp.isPrefetch.test(state.prefetch)) { + const threshold = target.getAttribute('data-pjax-threshold') + state.threshold[state.prefetch] = Number(threshold) + } + } + return state } @@ -128,41 +193,36 @@ export function getVisitConfig (state, target) { /** * Executes a pjax navigation. * + * @export * @param {IPjax.IState} state */ export function navigate (state) { - console.log(state, state.location.lastUrl) - cache.get(state.location.lastUrl).position = scroll.getPosition() - state.position = scroll.reset() + state.position = scroll.setPosition(state) - if (cache.has(state.url)) { - cache.set(state.url, state) - return cacheVisit(state.url) - } + return cache.has(state.url) + ? cacheVisit(state) + : pjaxVisit(state) - return pjaxVisit(state) } /** * Executes a visit by fetching the the cached response * from the session and passes it to the renderer. * - * @param {string} url - * @exports + * @export + * @param {IPjax.IState} state + * @returns {void} */ -export async function cacheVisit (url) { +export function cacheVisit (state) { - const state = cache.get(url) + prefetch.stop() - console.log(state) + const page = cache + .set(state.url, state) + .get(state.url) - if (store.config.prefetch) prefetch.stop() - - // ensure we have state before updating - if (state) render.update(state) - - if (store.config.prefetch) prefetch.start() + return render.update(page) } @@ -171,24 +231,27 @@ export async function cacheVisit (url) { * when `href` tag is clicked. If clicked link is in transit * from prefetch it will pass to cache visit * + * @export * @param {IPjax.IState} state + * @param {boolean} [async=false] + * @returns {Promise} */ export async function pjaxVisit (state, async = false) { if (transit.has(state.url)) { if ((await request.inFlight(state.url))) { - return cacheVisit(state.url) + return cacheVisit(state) } else { request.cancel(state.url) } } - if (store.config.prefetch) prefetch.stop() - - const cacheState = await request.get(state) + prefetch.stop() - if (cacheState) render.update(cacheState, async) + const page = await request.get(state) - if (store.config.prefetch) prefetch.start() + return page + ? render.update(page, async) + : window.location.replace(state.location.href) } diff --git a/src/constants/common.js b/src/constants/common.js index fc05af1..e9f977b 100644 --- a/src/constants/common.js +++ b/src/constants/common.js @@ -1,25 +1,40 @@ /** * Link Selector + * + * @exports + * @type {string} */ export const Link = 'a:not([data-pjax-disable]):not([href^="#"])' /** * Link Prefetch Hover Selector + * + * @exports + * @type {string} */ export const LinkPrefetchHover = 'a[data-pjax-prefetch="hover"]' /** * Link Prefetch Hover Selector + * + * @exports + * @type {string} */ export const LinkPrefetchIntersect = 'a[data-pjax-prefetch="intersect"]' /** * Form Selector + * + * @exports + * @type {string} */ export const Form = 'form:not([data-pjax-disable])' /** - * Units + * Units Array used for cache size + * + * @exports + * @type {string[]} */ export const Units = [ 'B', 'KB', 'MB', 'GB', 'TB' ] diff --git a/src/constants/regexp.js b/src/constants/regexp.js index 8a412b3..28dd93c 100644 --- a/src/constants/regexp.js +++ b/src/constants/regexp.js @@ -2,6 +2,9 @@ * Form Inputs * * Used to match Form Input elements + * + * @exports + * @type {RegExp} */ export const FormInputs = /^(input|textarea|select|datalist|button|output)$/i @@ -9,13 +12,19 @@ export const FormInputs = /^(input|textarea|select|datalist|button|output)$/i * Ready State * * Ready State Match + * + * @exports + * @type {RegExp} */ -export const isReady = /^(interactive|complete)$/ +export const isReady = /^(?:interactive|complete)$/ /** * Boolean Attribute value * * Used to Match 'true' or 'false' attribute + * + * @exports + * @type {RegExp} */ export const isBoolean = /\b(true|false)\b/ @@ -23,6 +32,9 @@ export const isBoolean = /\b(true|false)\b/ * Matches decimal number * * Used to Match number, respected negative numbers + * + * @exports + * @type {RegExp} */ export const isNumber = /^[+-]?\d*\.?\d+$/ @@ -30,28 +42,59 @@ export const isNumber = /^[+-]?\d*\.?\d+$/ * Matches whitespaces (greedy) * * Used to Match whitspaces + * + * @exports + * @type {RegExp} */ export const isWhitespace = /\s+/g +/** + * Attribute Action Caller + * + * Used to match the event caller for attribute actions + * + * @exports + * @type {RegExp} + */ +export const isAction = /\b(?:ap|pre)pend|replace/g + /** * Append or Prepend * * Used to match append or prepend insertion + * + * @exports + * @type {RegExp} */ -export const isReplace = /\b(append|prepend)\b/ +export const isReplace = /\b(?:append|prepend)\b/ /** - * Attribute Action Caller + * Threshold Attribute Value * - * Used to match the event caller for attribute actions + * Used to match threshold JSON attributes * + * @exports + * @type {RegExp} */ -export const ActionAttr = /\b(?:append|prepend|replace)/g +export const isPrefetch = /\b(?:intersect|mouseover|hover)\b/ + +/** + * Threshold Attribute Value + * + * Used to match threshold JSON attributes + * + * @exports + * @type {RegExp} + */ +export const isThreshold = /\b(?:intersect|mouseover|progress)\b|(?<=[:])[^\s][0-9.]+/ /** * Attribute Parameter Value * * Used to match a class event caller target attributes + * + * @exports + * @type {RegExp} */ export const ActionParams = /[^,'"[\]()\s]+/g @@ -59,26 +102,31 @@ export const ActionParams = /[^,'"[\]()\s]+/g * Mached Position Attributes * * Used to match `x:0` and `y:0` JSON space separated attributes + * + * @exports + * @type {RegExp} */ -export const inPosition = /[xy]|[0-9]+/g +export const isPosition = /[xy]|(?<=[:])[0-9]+/g /** * Protocol * * Used to match Protocol - */ -export const Protocol = /^https?:$/ - -/** - * DOM Parse Fallback * - * Used as a fallback to parse response text string + * @exports + * @type {RegExp} */ -export const DOMParseFallback = /^\s*<(!doctype|html)[^>]*>/i +export const Protocol = /^https?:$/ /** * XHR Headers * * Used for replacing headers in XHR Request util. + * + * @deprecated + * NOT IN USE - MAY USE IN FUTURE + * + * @exports + * @type {RegExp} */ export const XHRHeaders = /^(.*?):[^\S\n]*([\s\S]*?)$/gm diff --git a/src/observers/history.js b/src/observers/history.js index 882bbbb..b2fdb5b 100644 --- a/src/observers/history.js +++ b/src/observers/history.js @@ -1,9 +1,9 @@ +import history from 'history/browser' import { store, cache } from '../app/store' import { expandURL } from '../app/location' import * as prefetch from '../app/prefetch' import * as render from '../app/render' import * as request from '../app/request' -import history from 'history/browser' /** * @type {boolean} @@ -27,7 +27,8 @@ let inTransit = null /** * Attached `history` event listener. * - * @export + * @exports + * @returns {void} */ export function start () { @@ -41,6 +42,7 @@ export function start () { * Removed `history` event listener. * * @export + * @returns {void} */ export function stop () { @@ -50,10 +52,25 @@ export function stop () { } } +/** + * Event History dispatch controller, handles popstate, + * push and replace events via third party module + * + * @param {import('history').BrowserHistory} event + */ +function listener ({ action, location }) { + + if (action === 'POP') return popstate(history.createHref(location)) + + console.log(action, location.state) + +} + /** * Popstate Navigations * * @param {string} url + * @returns {Promise} */ async function popstate (url) { @@ -79,17 +96,3 @@ async function popstate (url) { prefetch.start() } - -/** - * Event History dispatch controller, handles popstate, - * push and replace events via third party module - * - * @param {import('history').BrowserHistory} event - */ -async function listener ({ action, location }) { - - if (action === 'POP') return popstate(history.createHref(location)) - - console.log(action, location.state) - -} diff --git a/src/observers/hrefs.js b/src/observers/hrefs.js index a3b9684..e735edc 100644 --- a/src/observers/hrefs.js +++ b/src/observers/hrefs.js @@ -1,5 +1,5 @@ import { navigate, visitClickState } from '../app/visit' -import { dispatchEvent, linkEventValidate, linkLocator } from '../app/utils' +import { dispatchEvent, linkEvent, linkLocate } from '../app/utils' import { Link } from '../constants/common' import { onMouseleave } from './mouseover' @@ -15,7 +15,8 @@ let started = false /** * Attached `click` event listener. * - * @export + * @exports + * @returns {void} */ export function start () { @@ -30,6 +31,7 @@ export function start () { * Removed `click` event listener. * * @export + * @returns {void} */ export function stop () { @@ -40,20 +42,33 @@ export function stop () { } +/** + * Adds and/or Removes click events. + * + * @returns {void} + */ +function observe () { + + removeEventListener('click', onClick, false) + addEventListener('click', onClick, false) + +} + /** * Attempts to visit href location, Handles click bubbles and * Dispatches a `pjax:click` event respecting the cancelable * `preventDefault()` from user event * * @param {MouseEvent} event + * @returns {Promise} */ function onClick (event) { - if (linkEventValidate(event)) { + if (linkEvent(event)) { event.preventDefault() - const target = linkLocator(event.target, Link) + const target = linkLocate(event.target, Link) if (target && target.tagName === 'A') { @@ -66,15 +81,3 @@ function onClick (event) { } } } - -/** - * Adds and/or Removes click events. - * - * @private - */ -function observe () { - - removeEventListener('click', onClick, false) - addEventListener('click', onClick, false) - -} diff --git a/src/observers/intersect.js b/src/observers/intersect.js index 7a6583d..90fab73 100644 --- a/src/observers/intersect.js +++ b/src/observers/intersect.js @@ -1,6 +1,6 @@ import { LinkPrefetchIntersect } from '../constants/common' -import { getURL } from '../app/location' -import { transit, cache, store } from '../app/store' +import { getTargets } from '../app/utils' +import { transit, store } from '../app/store' import { getPageState, getVisitConfig } from '../app/visit' import * as request from '../app/request' @@ -22,7 +22,8 @@ let started = false * Starts prefetch, will initialize `IntersectionObserver` and * add event listeners and other logics. * - * @export + * @exports + * @returns {void} */ export function start () { @@ -39,7 +40,8 @@ export function start () { * Stops prefetch, will disconnect `IntersectionObserver` and * remove any event listeners or transits. * - * @export + * @exports + * @returns {void} */ export function stop () { @@ -56,7 +58,7 @@ export function stop () { * Begin Observing `href` links * * @param {Element} target - * @memberof PrefetchObserver + * @returns {void} */ function observe (target) { @@ -67,6 +69,7 @@ function observe (target) { * Start Intersection Observer and iterate over entries. * * @type {IntersectionObserverCallback} + * @returns {void} */ function intersect (entries) { @@ -77,6 +80,7 @@ function intersect (entries) { * Intersection callback when entries are in viewport. * * @param {IntersectionObserverEntry} params + * @returns {Promise} */ async function onIntersection ({ isIntersecting, target }) { @@ -96,25 +100,3 @@ async function onIntersection ({ isIntersecting, target }) { } } - -/** - * Link is not cached and can be fetched - * - * @param {Element} target - * @returns {boolean} - */ -function canFetch (target) { - - return !cache.has(getURL(target)) -} - -/** - * Returns a list of link elements to be prefetched. Filters out - * any links which exist in cache to prevent extrenous transit. - * - * @param {string} selector - */ -function getTargets (selector) { - - return [ ...document.body.querySelectorAll(selector) ].filter(canFetch) -} diff --git a/src/observers/mouseover.js b/src/observers/mouseover.js index 29d535f..da12a35 100644 --- a/src/observers/mouseover.js +++ b/src/observers/mouseover.js @@ -1,8 +1,8 @@ import { LinkPrefetchHover } from '../constants/common' -import { getCacheKeyFromTarget, getURL } from '../app/location' +import { getURL } from '../app/location' import { transit, cache, store } from '../app/store' -import { linkEventValidate, linkLocator } from '../app/utils' -import { getPageState, getVisitConfig } from '../app/visit' +import { linkEvent, linkLocate, getTargets } from '../app/utils' +import * as visit from '../app/visit' import * as request from '../app/request' /** @@ -15,10 +15,12 @@ let started = false /* -------------------------------------------- */ /** - * Starts prefetch, will initialize `IntersectionObserver` and - * add event listeners and other logics. + * Starts mouseovers, will attach mouseover events + * to all elements which contain a `data-pjax-prefetch="hover"` + * data attribute * * @export + * @returns {void} */ export function start () { @@ -31,10 +33,12 @@ export function start () { } /** - * Stops prefetch, will disconnect `IntersectionObserver` and - * remove any event listeners or transits. + * Stops mouseovers, will remove all mouseover and mouseout + * events on elements which contains a `data-pjax-prefetch="hover"` + * unless target href already exists in cache. * * @export + * @returns {void} */ export function stop () { @@ -52,13 +56,14 @@ export function stop () { * concludes. This prevents fetches being made for hovers that * do not exceeds threshold. * + * @exports * @param {MouseEvent} event */ export function onMouseleave (event) { - if (linkEventValidate(event)) { + if (linkEvent(event)) { - const target = linkLocator(event.target, LinkPrefetchHover) + const target = linkLocate(event.target, LinkPrefetchHover) if (target) { cleanup(getURL(target)) @@ -77,13 +82,13 @@ export function onMouseleave (event) { */ function onMouseover (event) { - if (linkEventValidate(event)) { + if (linkEvent(event)) { - const target = linkLocator(event.target, LinkPrefetchHover) + const target = linkLocate(event.target, LinkPrefetchHover) if (target) { - const state = getPageState(target) + const state = visit.getPageState(target) if (cache.has(state.url)) return disconnect(target) @@ -92,12 +97,8 @@ function onMouseover (event) { throttle(state.url, () => { state.method = 'prefetch' - target.removeEventListener('mouseleave', onMouseleave, true) - prefetch(getVisitConfig(state, target), newState => { - console.log('prefetch') - target.removeEventListener('mouseover', onMouseover, true) - }) + prefetch(visit.getVisitConfig(state, target), target) }, store.config.threshold.hover) @@ -109,6 +110,7 @@ function onMouseover (event) { * Attach mouseover events to all defined element targets * * @param {EventTarget} target + * @returns {void} */ function observe (target) { @@ -120,12 +122,13 @@ function observe (target) { * Cleanup throttlers * * @param {string} url - * @memberof PrefetchObserver + * @returns {boolean} */ function cleanup (url) { clearTimeout(transit.get(url)) - transit.delete(url) + + return transit.delete(url) } @@ -135,11 +138,12 @@ function cleanup (url) { * @param {string} url * @param {function} fn * @param {number} delay + * @returns {Map} */ function throttle (url, fn, delay) { if (!cache.has(url) && !transit.has(url)) { - transit.set(url, setTimeout(fn, delay)) + return transit.set(url, setTimeout(fn, delay)) } } @@ -148,59 +152,31 @@ function throttle (url, fn, delay) { * Lifecycle event `pjax:cache` will fire upon completion. * * @param {IPjax.IState} state - * The navigation configuration state for the requestd page - * - * @param {(status: IPjax.IState) => void} callback - * The `href` link target the prefetch was issued for + * @param {Element} target + * @returns{Promise} */ -async function prefetch (state, callback) { - - // console.log('prefetch', state.url) - try { - - const response = await request.get(state) +async function prefetch (state, target) { - callback(response) + const response = await request.get(state) - } catch (error) { - console.error(error) - console.info(`Endpoint "${state.url}" failed, will retry prefetch again`) + if (response) { + disconnect(target) + } else { + console.warn(`Pjax: Prefetch will retry on next mouseover for: ${state.url}`) } cleanup(state.url) } -/** - * Link is not cached and can be fetched - * - * @param {Element} target - * @returns {boolean} - */ -function canFetch (target) { - - return !cache.has(getCacheKeyFromTarget(target)) -} - -/** - * Returns a list of link elements to be prefetched. Filters out - * any links which exist in cache to prevent extrenous transit. - * - * @param {string} selector - */ -function getTargets (selector) { - - return [ ...document.body.querySelectorAll(selector) ].filter(canFetch) -} - /** * Adds and/or Removes click events. * * @param {EventTarget} target + * @returns {void} */ function disconnect (target) { target.removeEventListener('mouseleave', onMouseleave, false) target.removeEventListener('mouseover', onMouseover, false) - } diff --git a/src/observers/scrolling.js b/src/observers/scrolling.js index b11424a..e949934 100644 --- a/src/observers/scrolling.js +++ b/src/observers/scrolling.js @@ -1,3 +1,8 @@ +import { cache } from '../app/store' + +/* -------------------------------------------- */ +/* LETTINGS */ +/* -------------------------------------------- */ /** * @type {boolean} @@ -7,7 +12,7 @@ let started = false /** * @type {IPjax.IPosition} */ -const position = { x: 0, y: 0 } +let position = { x: 0, y: 0 } /* -------------------------------------------- */ /* FUNCTIONS */ @@ -16,7 +21,8 @@ const position = { x: 0, y: 0 } /** * Attached `scroll` event listener. * - * @export + * @exports + * @returns {void} */ export function start () { @@ -31,19 +37,60 @@ export function start () { /** * Removed `scroll` event listener. * - * @export + * @exports + * @returns {void} */ export function stop () { if (started) { removeEventListener('scroll', onScroll, false) + position = { x: 0, y: 0 } started = false } } /** - * Returns to current scroll position + * Sets scroll position to the cache reference and + * returns a reset position. + * + * This function is called before a new page visit + * navigation begins, as it will assert the current + * position to the current page and return the reset + * position, ie: `{x: 0, y: 0 }`) to new page visit. + * + * If the passed in page state object position was modified + * via attributes, eg: `data-pjax-position="x:number y:number"` + * then position will be adjusted to match attribute config and + * additionally returned. + * + * + * @exports + * @param {IPjax.IState} state + * @returns {IPjax.IPosition} + */ +export function setPosition ({ + location: { lastUrl }, + position: { x, y } +}) { + + // We assert current position here + cache.get(lastUrl).position = getPosition() + + if ((x === 0 && y === 0)) return reset() + + position.x = x === 0 ? 0 : x + position.y = y === 0 ? 0 : x + + return position + +} + +/** + * Returns to current scroll position, the `reset()` + * function **MUST** be called after referencing this + * to reset position. * - * @export + * @exports + * @returns {IPjax.IPosition} */ export function getPosition () { @@ -51,9 +98,11 @@ export function getPosition () { } /** - * Resets scroll position + * Resets the scroll position`of the document, applying + * a `x`and `y` positions to `0` * - * @export + * @exports + * @returns {IPjax.IPosition} */ export function reset () { @@ -65,9 +114,11 @@ export function reset () { } /** - * onScroll event + * onScroll event, asserts the current X and Y page + * offset position of the document * - * @export + * @exports + * @returns {void} */ export function onScroll () { diff --git a/types/state.d.ts b/types/state.d.ts new file mode 100644 index 0000000..63dc5c4 --- /dev/null +++ b/types/state.d.ts @@ -0,0 +1,194 @@ +import { IPosition, ILocation } from './store' + + +export interface Page { + + /** + * The URL cache key + */ + url?: string + + /** + * UUID reference to the page snapshot HTML String + */ + snapshot?: string + + /** + * UUID reference to the captured snapshot HTML string + */ + captured?: string + + /** + * The fetched HTML response string + */ + targets?: { + [selector: string]: Element[] + } + + /** + * Location URL + */ + location?: ILocation + + /** + * The Document title + */ + title?: string + + /** + * Action + */ + action?: { + replace?: [target:string] + append?: Array<[from: string, to: string]>, + prepend?: Array<[from: string, to: string]>, + } + + /** + * List of target element selectors. Accepts any valid + * `querySelector()` string. + * + * @example + * ['#main', '.header', '[data-attr]', 'header'] + */ + target?: string[] + + /** + * Default method to be applied. + * --- + * `replace` - Navigation target will be replaced + * + * `append` - Navigation target will be appened + * + * `prepend` - Navigation target will be prepended + * + */ + method?: string + + /** + * Controls the caching engine for the link navigation. + * Option is enabled when `cache` preset config is `true`. + * Each pjax link can set a different cache option, see below: + * --- + * `false` + * + * Passing in __false__ will execute a pjax visit that will + * not be saved to cache and if the link exists in cache + * it will be removed. + * + * `reset` + * + * Passing in __reset__ the cache record will be removed, + * a new pjax visit will be executed and the result saved to cache. + * + * `save` + * + * Passing in __save__ will temporarily store the current + * cached state to session storage. It will be removed on your + * next navigation visit. + * + * > _The save option should be avoided unless you are executing a + * full page reload and wish to store your cached pages to prevent + * new requests being executed on next navigation. If your cache exceeds + * 3mb in size cache records will be removed starting from the earliest + * point on of entry. Use `save` in conjunction with the `data-pjax-disable` + * option, else do your upmost to avoid it._ + */ + cache?: boolean | 'false' | 'reset' | 'save' + + /** + * Scroll position of the next navigation. + * + * --- + * `x` - Equivalent to `scrollLeft` in pixels + * + * `y` - Equivalent to `scrollTop` in pixels + */ + position?: IPosition + + /** + * Prefetch option to execute for each link + * + * --- + * `intersect` + * + * Pages will be fetched upon `IntersectionObserve()` threshold. + * ie: when they become visible in viewport. + * + * `hover` + * + * Pages will be fetched upon `mouseover` on a pjax href link. + * Try and avoid this, just use __intersect__ instead. + * + * > _On mobile devices the hover value will execute on a + * touch event._ + */ + prefetch?: boolean | 'intersect' | 'mouseover' | 'hover' + + /** + * List array of tracked elements pretaining to this link page + * navigation visit (if any). + * + * @see https://github.com/panoply/pjax#data-pjax-track + */ + track?: Element[] + + /** + * Throttle delay between navigations, set this option if + * you want to delay the time between visits, helpful if + * navigation is too fast. + * + * @default 0 + */ + throttle?: number + + /** + * Enable or Disable progres bar indicator + * + * (_Requests are instantaneous, generally you wont need this_) + * + * @default true + */ + progress?: boolean, + + + /** + * Threshold Controls + */ + threshold?: { + + /** + * Define an intersection threshold timeout from + * which intersected elements will begin fetching + * after being observed + * + * @default 250 + */ + intersect?: number, + + /** + * Define mouseover timeout from which fetching will begin + * after time spent on mouseover + * + * @default 100 + */ + mouseover?: number, + + /** + * Define hover timeout from which fetching will begin + * after time spent on mouseover + * + * @default 100 + * + * @deprecated + * Use `mouseover` instead + */ + hover?: number, + + + } + +} + +export as namespace IPjaxState; + diff --git a/types/store.d.ts b/types/store.d.ts index 12da4fe..2b9e233 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -33,6 +33,15 @@ export type IPosition = { y: number } +/** + * Scroll position records + */ +export type ICacheSize = { + requests: number + total: number + weight: string +} + /** * The URL location object */ @@ -404,8 +413,11 @@ export interface IState extends IConfig { * Threshold */ threshold?: { - intersect?: number, + intersect?: number + mouseover?: number hover?: number + + } } From c23be8696c5aed36d769cd366abc8323ecb7e069 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 21 Mar 2021 20:28:27 +0100 Subject: [PATCH 17/60] Update readme.md --- readme.md | 127 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 30 deletions(-) diff --git a/readme.md b/readme.md index ba2ece6..c6c74e7 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,7 @@ pnpm i @brixtol/pjax You do not create a class instance, the module has no classes or any of that oop shit but you do need to call `connect` to initialize. + ```js import * as Pjax from "@brixtol/pjax"; @@ -40,49 +41,115 @@ import * as Pjax from "@brixtol/pjax"; /* -------------------------------------------- */ Pjax.connect({ - fragments: ["main"], - action: "replace", - prefetch: true, - cache: true, - throttle: 0, - progress: false, - threshold: { - intersect: 250, - hover: 100, + targets: ["body"], + cache: { + enable: true, + limit: 25, + }, + requests: { + timeout: 1500, + poll: 150, + async: true, + }, + prefetch: { + mouseover: { + enable: true, + threshold: 100, + proximity: 0, + }, + intersect: { + enable: true, + options: { + rootMargin: "0px", + threshold: 1.0, + }, + }, + }, + progress: { + enable: true, + threshold: 500, + options: { + minimum: 0.25, + easing: "ease", + speed: 200, + trickle: true, + trickleSpeed: 200, + showSpinner: false, + }, }, }); /* LIFECYCLE EVENTS /* -------------------------------------------- */ -document.addEventListener("pjax:load", ({ detail }) => {}); +document.addEventListener("pjax:click", ({ detail: { target, options } }) => {}); -document.addEventListener("pjax:click", (event) => {}); +document.addEventListener("pjax:request", ({ detail: { url, type } }) => {}); -document.addEventListener("pjax:request", (event) => {}); +document.addEventListener("pjax:cache", ({ detail: { record } }) => {}); -document.addEventListener("pjax:cache", (event) => {}); +document.addEventListener("pjax:render", ({ detail: { actions } }) => {}); -document.addEventListener("pjax:render", ({ detail }) => {}); -``` +document.addEventListener("pjax:load", ({ detail: { targets, location } }) => {}); + +/* HOOKS +/* -------------------------------------------- */ -You can also cherry-pick the export methods: +Pjax.on('request', (state) => {}) -```js -import { connect } from "@brixtol/pjax"; - -connect({ - target: ["main", "#navbar"], - action: "replace", - prefetch: true, - cache: true, - throttle: 0, - progress: false, - threshold: { - intersect: 250, - hover: 100, +Pjax.on('cache', (state) => {}) + +Pjax.on('render', (state) => {}) + +Pjax.on('load', (state) => {}) + +/* ROUTING +/* -------------------------------------------- */ + +Pjax.route({ + '/:path': { + initialize() {}, + connect() {}, + disconnect() {}, + } +}) + +/* METHODS +/* -------------------------------------------- */ + +Pjax.supported: boolean + +Pjax.metrics: object + +Pjax.visit(url?, { cache, position, action, progress }) + +Pjax.cache(url?) + +Pjax.flush() + +Pjax.capture(url?, { action }) + +Pjax.uuid(size = 16) + +Pjax.reload() + +Pjax.disconnect() + + +/* GLOBAL CONTEXT +/* -------------------------------------------- */ + +window.Pjax = { + session: { + '/': { + uuid: string, + cached: boolean, + history: boolean, + visits: number + } }, -}); +} + ``` ## Define Presets From 7c0930162c08fd7bf7b0b7dbd9bfd2ebd919b0db Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Wed, 24 Mar 2021 08:21:54 +0100 Subject: [PATCH 18/60] v1.0.0.beta --- .babelrc | 3 +- package.json | 3 + rollup.config.js | 8 +- src/app/controller.js | 58 ++-- src/app/location.js | 80 ----- src/app/path.js | 130 ++++++++ src/app/prefetch.js | 17 +- src/app/progress.js | 76 +---- src/app/render.js | 409 +++++++++++-------------- src/app/request.js | 315 ++++++++++--------- src/app/store.js | 366 +++++++++++++--------- src/app/utils.js | 43 +-- src/app/visit.js | 257 ---------------- src/constants/regexp.js | 75 ++++- src/index.js | 23 +- src/observers/history.js | 145 ++++----- src/observers/hover.js | 229 ++++++++++++++ src/observers/hrefs.js | 270 ++++++++++++---- src/observers/intersect.js | 161 +++++----- src/observers/mouseover.js | 182 ----------- src/observers/scroll.js | 114 +++++++ src/observers/scrolling.js | 128 -------- src/polyfills/getAttributeNames.js | 11 + types/state.d.ts | 473 ++++++++++++++++++++++------- types/store.d.ts | 84 ++--- 25 files changed, 1972 insertions(+), 1688 deletions(-) delete mode 100644 src/app/location.js create mode 100644 src/app/path.js delete mode 100644 src/app/visit.js create mode 100644 src/observers/hover.js delete mode 100644 src/observers/mouseover.js create mode 100644 src/observers/scroll.js delete mode 100644 src/observers/scrolling.js create mode 100644 src/polyfills/getAttributeNames.js diff --git a/.babelrc b/.babelrc index 0ab27a6..d816c8d 100644 --- a/.babelrc +++ b/.babelrc @@ -11,6 +11,7 @@ "retainLines": true, "plugins": [ "@babel/plugin-transform-runtime", - "@babel/plugin-syntax-dynamic-import" + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties" ] } diff --git a/package.json b/package.json index 88205a6..0c25268 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@babel/runtime": "^7.13.10", + "@types/nprogress": "^0.2.0", "custom-event-polyfill": "^1.0.7", "element-closest-polyfill": "^1.0.2", "history": "^5.0.0", @@ -49,11 +50,13 @@ "mdn-polyfills": "^5.20.0", "mergerino": "^0.4.0", "nanoid": "^3.1.21", + "nprogress": "^0.2.0", "regexp.prototype.match": "^0.1.0", "url-polyfill": "^1.1.12" }, "devDependencies": { "@babel/core": "^7.13.10", + "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-property-mutators": "^7.12.13", "@babel/plugin-transform-runtime": "^7.13.10", diff --git a/rollup.config.js b/rollup.config.js index 3000250..90db91f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -70,7 +70,8 @@ export default [ ], plugins: [ [ '@babel/plugin-transform-runtime' ], - [ '@babel/plugin-syntax-dynamic-import' ] + [ '@babel/plugin-syntax-dynamic-import' ], + [ '@babel/plugin-proposal-class-properties' ] ] }), filesize() @@ -119,8 +120,9 @@ export default [ compact: true, plugins: [ [ '@babel/plugin-transform-runtime', { absoluteRuntime: true } ], - '@babel/plugin-transform-property-mutators', - '@babel/plugin-syntax-dynamic-import' + [ '@babel/plugin-transform-property-mutators' ], + [ '@babel/plugin-syntax-dynamic-import' ], + [ '@babel/plugin-proposal-class-properties' ] ] }), filesize() diff --git a/src/app/controller.js b/src/app/controller.js index 815ea4b..39b5ce3 100644 --- a/src/app/controller.js +++ b/src/app/controller.js @@ -1,33 +1,11 @@ -import { store, cache } from './store' -import { expandURL } from './location' -import * as hrefs from '../observers/hrefs' -import * as mouseover from '../observers/mouseover' -import * as intersect from '../observers/intersect' -import * as scroll from '../observers/scrolling' -import * as history from '../observers/history' -import * as render from './render' +import hrefs from '../observers/hrefs' +import hover from '../observers/hover' +import intersect from '../observers/intersect' +import scroll from '../observers/scroll' +import history from '../observers/history' +import { store } from './store' -/** - * Sets initial page state on landing page and - * caches it so return navigation don't perform - * an extrenous request. - * - * @param {Event} event - * @returns {Map} - */ -function setInitialCache (event) { - - const location = expandURL(window.location.href) - const state = store.update.page({ - url: location.lastUrl, - snapshot: render.DOMSnapshot(document), - title: document.title, - location - }) - - return cache.set(state.url, state) - -} +let started = false /** * Initialize @@ -37,20 +15,19 @@ function setInitialCache (event) { */ export function initialize () { - if (!store.started) { + if (!started) { history.start() hrefs.start() scroll.start() - mouseover.start() - intersect.start() + hover.start() + intersect.stop() - addEventListener('load', setInitialCache, false) + addEventListener('load', store.initialize) + started = true - store.started = true console.info('Pjax: Connection Established ⚡') } - } /** @@ -61,15 +38,16 @@ export function initialize () { */ export function destroy () { - if (store.started) { + if (started) { history.stop() hrefs.stop() scroll.stop() - mouseover.start() - intersect.start() - cache.clear() - store.started = false + hover.stop() + intersect.stop() + store.clear() + + started = false console.warn('Pjax: Instance has been disconnected! 😔') } else { diff --git a/src/app/location.js b/src/app/location.js deleted file mode 100644 index b37e07e..0000000 --- a/src/app/location.js +++ /dev/null @@ -1,80 +0,0 @@ -import history from 'history/browser' - -/** - * Expands URL href location. - * - * @export - * @param {string} url - * @returns {IPjax.ILocation} - */ -export function expandURL (url) { - - const lastUrl = history.createHref(window.location) - const location = document.createElement('a') - - location.href = url.toString() - - const { - origin, - hostname, - href, - pathname, - search, - hash - } = new URL(location.href) - - return { - origin - , hostname - , href - , pathname - , search - , hash - , lastUrl - } - -} - -/** - * Returns the current URL - * - * @export - * @param {Element|string} target - * @return {string} - */ -export function getURL (target) { - - const href = target instanceof Element ? target.getAttribute('href') : target - - return history.createHref(expandURL(href)) - -} - -/** - * Returns the pathname from `href` target used for cache key. - * - * @export - * @param {Element} target - * @return {string} - */ -export const getCacheKeyFromTarget = target => getURL(target) - -/** - * Hash URL using DJB2A algorithm - * - * NOT IN USE - REMOVE IN NEXT RELEASE - * - * @export - * @param {string} url - * @return {string} - */ -export function getCacheKey (url) { - - let i = 0 - let hash = 5381 - - for (; i < url.length; i++) hash = ((hash << 5) + hash) ^ url.charCodeAt(i) - - return (hash >>> 0).toString(16) - -} diff --git a/src/app/path.js b/src/app/path.js new file mode 100644 index 0000000..14d095d --- /dev/null +++ b/src/app/path.js @@ -0,0 +1,130 @@ +import history from 'history/browser' +import { parsePath, createPath } from 'history' +import * as regexp from './../constants/regexp' + +/** + * Location URL path handler + * + * @param {Window} window + */ +export default (function ({ location, location: { origin, hostname } }) { + + let next = createPath(location) + let path = next + + return { + + /** + * Returns the last parsed url value. + * Prev URL is the current URL. Calling this will + * return the same value as it would `window.location.pathname` + * + * **BEWARE** + * + * Use this with caution, this value will change on new + * navigations. + * + * @returns {string} + */ + get url () { + + return path + + }, + + /** + * Returns the next parsed url value. + * Next URL is the new navigation URL key from + * which a navigation will render. This is set + * right before page replacements. + * + * **BEWARE** + * + * Use this with caution, this value will change only when + * a new navigation has began. Otherwise it returns + * the same value as `url` + * + * @returns {string} + */ + get next () { + + return next + + }, + + /** + * Parses link and returns a location. + * + * **IMPORTANT** + * + * This function will modify the next url value + * + * @export + * @param {Element|string} link + * @param {boolean} [isNext=true] + * @returns {string} + */ + get: (link, isNext = false) => { + + const href = link instanceof Element ? link.getAttribute('href') : link + + // 47 is unicode value for '/' + const url = href.charCodeAt(0) === 47 + ? href + : (href.match(regexp.Pathname) || [])[1] || '/' + + if (isNext) { + path = createPath(history.location) + next = url + } + + return url + + }, + + /** + * Returns the absolute URL + * + * @param {string} link + * @returns {string} + */ + absolute: link => { + + const location = document.createElement('a') + location.href = link.toString() + + return location.href + + }, + + /** + * Parses link and returns an ILocation. + * Accepts either a `href` target or `string`. + * If no parameter value is passed, the + * current location pathname (string) is used. + * + * + * @export + * @param {Element|string} link + * @returns {Store.ILocation} + */ + parse (link) { + + const location = parsePath( + link instanceof Element + ? link.getAttribute('href') + : link + ) + + return { + lastpath: createPath(history.location) + , search: '' + , origin + , hostname + , ...location + } + } + + } + +})(window) diff --git a/src/app/prefetch.js b/src/app/prefetch.js index 38824c1..2cf8677 100644 --- a/src/app/prefetch.js +++ b/src/app/prefetch.js @@ -1,6 +1,6 @@ import { store } from './store' -import * as mouseover from '../observers/mouseover' -import * as intersect from '../observers/intersect' +import mouseover from '../observers/hover' +import intersect from '../observers/intersect' /** * Starts prefetch, will initialize `IntersectionObserver` and @@ -11,10 +11,9 @@ import * as intersect from '../observers/intersect' */ export function start () { - if (store.config.prefetch) { - mouseover.start() - intersect.start() - } + if (store.config.prefetch.mouseover.enable) mouseover.start() + if (store.config.prefetch.intersect.enable) intersect.start() + } /** @@ -26,8 +25,6 @@ export function start () { */ export function stop () { - if (store.config.prefetch) { - mouseover.stop() - intersect.stop() - } + if (store.config.prefetch.mouseover.enable) mouseover.stop() + if (store.config.prefetch.intersect.enable) intersect.stop() } diff --git a/src/app/progress.js b/src/app/progress.js index 89e32f9..25acd2b 100644 --- a/src/app/progress.js +++ b/src/app/progress.js @@ -1,86 +1,26 @@ -import { store } from './store' +import nprogress from 'nprogress' /* -------------------------------------------- */ /* LETTINGS */ /* -------------------------------------------- */ /** - * Timer Reference - * - * @type {any} - */ -let timer - -/** - * Progress Element - * - * @type {Element} + * @type {nprogress.NProgress} */ -let element - -/** - * Loading - * - * @type {boolean} - */ -export let loading = false +export let progress = null /* -------------------------------------------- */ /* FUNCTIONS */ /* -------------------------------------------- */ /** - * Show Progress Bar - * - * @exports - */ -export function show () { - - if (store.config.progress) { - - if (timer) { - clearTimeout(timer) - timer = 0 - element.className = 'pjax-loader pjax-hide' - setTimeout(show, 25) - return - } - - if (!element) { - element = document.createElement('div') - element.innerHTML = '
' - element.setAttribute('data-pjax-track', 'true') - document.body.appendChild(element) - } - - element.className = 'pjax-loader pjax-start' - - timer = setTimeout(() => { - timer = 0 - loading = true - element.classList.add('pjax-inload') - }, 15) - } -} - -/** - * Hide Progress Bar + * Setup nprogress * - * @exports + * @export + * @param {IPjax.IProgress} options */ -export function hide () { - - if (store.config.progress) { - if (timer) clearTimeout(timer) - - element.classList.add('pjax-end') - - timer = setTimeout(() => { - timer = 0 - element.classList.add('pjax-hide') - }, 800) +export function config (options) { - loading = false - } + progress = nprogress.configure(options) } diff --git a/src/app/render.js b/src/app/render.js index a35f016..41d8f26 100644 --- a/src/app/render.js +++ b/src/app/render.js @@ -1,299 +1,250 @@ -import { isReplace } from '../constants/regexp' import { eachSelector, dispatchEvent, forEach } from './utils' -import { store, snapshots, cache, tracked } from './store' -import { getURL } from './location' -import { nanoid } from 'nanoid' +import { store, snapshots } from './store' +import { progress } from './progress' import history from 'history/browser' -import * as progress from './progress' +import { createPath } from 'history' +import { nanoid } from 'nanoid' import * as prefetch from './prefetch' -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ - /** - * DOM Head Nodes + * Renderer * - * @param {string[]} nodes - * @param {HTMLHeadElement} head - * @return {string} + * @param {boolean} connected */ -function DOMHeadNodes (nodes, { children }) { +export default (function () { + + /** + * Tracked Elements + * + * @type {Set} + */ + const tracked = new Set() + + /** + * Parse HTML document string from request response + * using `DomParser()` method. Cached pages will pass + * the saved response here. + * + * @param {string} data + * @return {Document} + */ + const DOMParse = data => new DOMParser().parseFromString(data, 'text/html') + + /** + * DOM Head Nodes + * + * @param {string[]} nodes + * @param {HTMLHeadElement} head + * @return {string} + */ + const DOMHeadNodes = (nodes, { children }) => { + + forEach(children, DOMNode => { + if (DOMNode.tagName === 'TITLE') return null + if (DOMNode.getAttribute('data-pjax-eval') !== 'false') { + const index = nodes.indexOf(DOMNode.outerHTML) + index === -1 ? DOMNode.parentNode.removeChild(DOMNode) : nodes.splice(index, 1) + } + }) - forEach(children, DOMNode => { - if (DOMNode.tagName === 'TITLE') return null - if (DOMNode.getAttribute('data-pjax-eval') !== 'false') { - const index = nodes.indexOf(DOMNode.outerHTML) - index === -1 ? DOMNode.parentNode.removeChild(DOMNode) : nodes.splice(index, 1) - } - }) + return nodes.join('') - return nodes.join('') + } -} + /** + * DOM Head + * + * @param {HTMLHeadElement} head + */ + const DOMHead = ({ children }) => { + + const targetNodes = Array.from(children).reduce((arr, node) => ( + node.tagName !== 'TITLE' ? ( + [ ...arr, node.outerHTML ] + ) : arr + ), []) + + const fragment = document.createElement('div') + fragment.innerHTML = DOMHeadNodes(targetNodes, document.head) + + forEach(fragment.children, DOMNode => { + if (!DOMNode.hasAttribute('data-pjax-eval')) { + document.head.appendChild(DOMNode) + } + }) -/** - * DOM Head - * - * @param {HTMLHeadElement} head - */ -function DOMHead ({ children }) { + } - const targetNodes = Array.from(children).reduce((arr, node) => ( - node.tagName !== 'TITLE' ? ( - [ ...arr, node.outerHTML ] - ) : arr - ), []) + /** + * Append Tracked Node + * + * @param {Element} node + */ + const appendTrackedNode = (node) => { - const fragment = document.createElement('div') - fragment.innerHTML = DOMHeadNodes(targetNodes, document.head) + // tracked element must contain id + if (!node.hasAttribute('id')) return - forEach(fragment.children, DOMNode => { - if (!DOMNode.hasAttribute('data-pjax-eval')) { - document.head.appendChild(DOMNode) + if (!tracked.has(node.id)) { + document.body.appendChild(node) + tracked.add(node.id) } - }) - -} - -/** - * Append Tracked Node - * - * @param {Element} node - */ -function appendTrackedNode (node) { - - // tracked element must contain id - if (!node.hasAttribute('id')) return - if (!tracked.has(node.id)) { - document.body.appendChild(node) - tracked.add(node.id) } -} + /** + * Apply actions to the documents target fragments + * with the request response. + * + * @param {Element} target + * @param {Store.IPage} state + * @returns {(DOM: Element) => void}} + */ + const replaceTarget = (target, state) => DOM => { -/** - * Apply actions to the documents target fragments - * with the request response. - * - * @param {Element} target - * @param {IPjax.IState} state - * @returns {(DOM: Element) => void}} - */ -const replaceTarget = (target, { method }) => DOM => { - - if (!isReplace.test(method)) { - - dispatchEvent('pjax:render', { method, fragment: target }, true) + dispatchEvent('pjax:render', { target }, true) - DOM.innerHTML = target.innerHTML + if (!state?.replace && !state?.prepend && !state?.append) { - } else { + DOM.innerHTML = target.innerHTML - let fragment = document.createElement('div') - const nodes = [].slice.call(target.childNodes) - - forEach(nodes, node => fragment.appendChild(node)) + } else { - if (method === 'append') { + const fragment = document.createElement('div') + const nodes = [].slice.call(target.childNodes) - dispatchEvent('pjax:render', { method, fragment }, true) - DOM.appendChild(fragment) + forEach(nodes, node => fragment.appendChild(node)) - console.log(fragment) - console.log('in append') + state.replace ? DOM.replaceWith(fragment) : state.append + ? DOM.appendChild(fragment) + : DOM.insertBefore(fragment, DOM.firstChild) - } else { - dispatchEvent('pjax:render', { method, fragment }, true) - DOM.insertBefore(fragment, DOM.firstChild) } - fragment = null - } -} - -function runActions () { - Object.entries(state.action).forEach(([ action, targets ]) => { - - targets.forEach((node) => { + /** + * Captures current document element and sets a + * record to snapshot state + * + * @param {Document} target + * @returns {string} + */ + const DOMSnapshot = (target) => { + const uuid = nanoid(16) + snapshots.set(uuid, target.documentElement.outerHTML) + return uuid + } - if (action === 'replace') { + /** + * Updates cached DOM + * + * @param {string} url + * @param {{ action: 'replace' | 'capture'}} options + */ + const captureDOM = (url, options) => { - const element = document.body.querySelector(`[data-pjax-target="${node}"]`) - const replace = target.body.querySelector(`[data-pjax-target="${node}"]`) + if (store.config.cache && store.has(url, { snapshot: true })) { - element.replaceWith(replace) - } + const state = store.cache(url) + const DOMString = store.snapshot(state.snapshot) + const target = DOMParse(DOMString) - if (action === 'append') { + target.body.innerHTML = document.documentElement.querySelector('body').innerHTML - const element = document.querySelector(`[data-pjax-target="${node[0]}"]`) - - state.targets[node[1]].forEach(newnode => { - element.appendChild(newnode) - dispatchEvent('pjax:render', { node: newnode }, true) - }) + if (options.action === 'replace') { + snapshots.set(state.snapshot, target.documentElement.outerHTML) + } else if (options.action === 'capture') { + state.captured = DOMSnapshot(target) + console.log(store.cache(url)) + history.replace(state.location, store.update(state)) + console.info('Pjax: DOM Captured at: ' + state.captured) } - }) - - }) - -} - -/** - * Get targets - * - * @exports - * @param {Document} element - * @param {IPjax.IState} state - */ -export function getTargets ({ body }, state) { - - body.querySelectorAll(Targets).forEach(node => { + return target.documentElement.outerHTML - const name = node.getAttribute('data-pjax-target') + } - if (!state.targets[name]) state.targets[name] = [] + } - state.targets[name].push(node) - // node.setAttribute('data-pjax-action', uuid) + /** + * Update the DOM and execute page adjustments + * to new navigation point + * + * @param {Store.IPage} state + * @param {boolean} [popstate=false] + */ + const update = (state, popstate = false) => { - }) + // window.performance.mark('render') -} + prefetch.stop() -/** - * Captures current document element and sets a - * record to snapshot state - * - * @exports - * @param {Document} target - * @returns {string} - */ -export function DOMSnapshot (target) { + console.log(state) - const uuid = nanoid(16) - snapshots.set(uuid, target.documentElement.outerHTML) + const uuid = (popstate && typeof state.captured === 'string') + ? state.captured + : state.snapshot - return uuid + const target = DOMParse(store.snapshot(uuid)) -} + state.title = document.title = target?.title || '' -/** - * Updates cached DOM - * - * @exports - * @param {string} url - * @param {object} options - */ -export function captureDOM (url, options) { - - url = getURL(url) - - if (store.config.cache && cache.has(url)) { + if (!popstate) { - const state = cache.get(url) - const DOMString = snapshots.get(state.snapshot) - const target = DOMParse(DOMString) - - console.log(url) - target.body.innerHTML = document.documentElement.querySelector('body').innerHTML + if (createPath(history.location) === state.url) { + history.replace(state.location, state) + } else { + history.push(state.location, state) + } - if (options.action === 'replace') { + } else if (state?.captured) { - snapshots.set(state.snapshot, target.documentElement.outerHTML) - } else if (options.action === 'capture') { + if (snapshots.delete(uuid)) { + state.captured = false + // history.replace(state.location, store.update(state)) + console.info('Pjax: Captured snapshot removed at: ' + state.url) + } - state.captured = DOMSnapshot(target) - history.replace(state.location.url, state) - console.info('Pjax: DOM Captured at: ' + state.captured) } - } - -} - -/** - * Parse HTML document string from request response - * using `DomParser()` method. Cached pages will pass - * the saved response here. - * - * @exports - * @param {string} data - * @return {Document} - */ -export function DOMParse (data) { - - return new DOMParser().parseFromString(data, 'text/html') -} - -/** - * Update the DOM and execute page adjustments - * to new navigation point - * - * @exports - * @param {IPjax.IState} state - * @param {boolean} [popstate=false] - */ -export function update (state, popstate = false) { + if (target?.head) DOMHead(target.head) - const uuid = (popstate && state?.captured) ? state.captured : state.snapshot - const target = DOMParse(snapshots.get(uuid)) + let fallback = 1 - state.title = document.title = target?.title || '' + forEach(state.targets, element => { - if (!popstate) { + const node = target.body.querySelector(element) - if (history.createHref(history.location) === state.url) { - history.replace(state.location, state) - } else { - history.push(state.location, state) - } + return node + ? eachSelector(document, element, replaceTarget(node, state)) + : fallback++ - } else if (typeof state?.captured === 'string') { + }) - if (snapshots.delete(uuid)) { - state.captured = null - history.replace(state.location, state) - console.info('Pjax: Captured snapshot removed at: ' + state.url) + if (fallback === state.targets.length) { + replaceTarget(target.body, state)(document.body) } - } - - if (target?.head) DOMHead(target.head) - - // APPEND TRACKED NODES - eachSelector(target, '[data-pjax-track]', appendTrackedNode) - - let fallback = 1 + // APPEND TRACKED NODES + eachSelector(target, '[data-pjax-track]', appendTrackedNode) - forEach(state.target, element => { + window.scrollTo(state.position.x, state.position.y) - const node = target.body.querySelector(element) + dispatchEvent('pjax:load', state) - return node ? eachSelector( - document, - element, - replaceTarget(node, state) - ) : fallback++ + progress.done() + prefetch.start() - }) + // console.log(window.performance.measure('Render Time', 'render')) + // console.log(window.performance.measure('Total', 'started')) - if (fallback === state.target.length) { - replaceTarget(target.body, state)(document.body) } - window.scrollTo(state.position.x, state.position.y) - - progress.hide() - - dispatchEvent('pjax:load', state) - - prefetch.start() + return { + update, + captureDOM + } -} +})() diff --git a/src/app/request.js b/src/app/request.js index 237a0c9..76fe43c 100644 --- a/src/app/request.js +++ b/src/app/request.js @@ -1,204 +1,203 @@ -import { store, snapshots, requests, cache } from './store' +import { store, snapshots } from './store' import { asyncTimeout, byteConvert, byteSize, dispatchEvent } from './utils' -import { nanoid } from 'nanoid' -import * as progress from './progress' - -/* -------------------------------------------- */ -/* LETTINGS */ -/* -------------------------------------------- */ - -let storage = 0 - -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ +import { progress } from './progress' /** - * Executes on request end. Removes the XHR recrod and update - * the response DOMString cache size record. + * Requests Handler * - * @exports - * @param {string} url - * @param {string} DOMString - * @returns {boolean} + * @param {boolean} connected */ -function HttpRequestEnd (url, DOMString) { - - storage = storage + byteSize(DOMString) - - return requests.delete(url) - -} +export default (function () { + + /** + * @type {number} + */ + let ratelimit = 0 + + /** + * @type {number} + */ + let storage = 0 + + /** + * XHR Requests + * + * @type {Map} + */ + const transit = new Map() + + /** + * Executes on request end. Removes the XHR recrod and update + * the response DOMString cache size record. + * + * @exports + * @param {string} url + * @param {string} DOMString + * @returns {boolean} + */ + const HttpRequestEnd = (url, DOMString) => { + + storage = storage + byteSize(DOMString) + + return transit.delete(url) -/** - * Fetch XHR Request wrapper function - * - * @exports - * @param {IPjax.IState} state - * @param {boolean} [async=false] - * @returns {Promise} - */ -async function HttpRequest ({ - url, - prefetch, - target, - location: { - href } -}, async) { - const xhr = new XMLHttpRequest() + /** + * Fetch XHR Request wrapper function + * + * @exports + * @param {string} url + * @param {boolean} [async=true] + * @returns {Promise} + */ + const HttpRequest = async (url, async = true) => { - return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() - // OPEN - // - xhr.open('GET', href, async) + return new Promise((resolve, reject) => { - // HEADERS - // - xhr.setRequestHeader('X-Pjax', 'true') - xhr.setRequestHeader('X-Pjax-Prefetch', `${prefetch}`) - xhr.setRequestHeader('X-Pjax-Target', JSON.stringify(target)) - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + // OPEN + // + xhr.open('GET', url, async) - // EVENTS - // - xhr.onloadstart = e => requests.set(url, xhr) - xhr.onload = e => xhr.status === 200 ? resolve(xhr.responseText) : null - xhr.onloadend = e => HttpRequestEnd(url, xhr.responseText) - xhr.onerror = reject + // HEADERS + // + xhr.setRequestHeader('X-Pjax', 'true') + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest') - // SEND - // - xhr.send(null) + // EVENTS + // + xhr.onloadstart = e => transit.set(url, xhr) + xhr.onload = e => xhr.status === 200 ? resolve(xhr.responseText) : null + xhr.onloadend = e => HttpRequestEnd(url, xhr.responseText) + xhr.onerror = reject - }) + // SEND + // + xhr.send(null) -} - -/** - * Returns request cache metrics - * - * @exports - * @returns {IPjax.ICacheSize} - */ -export function cacheSize () { + }) - return { - requests: snapshots.size, - total: storage, - weight: byteConvert(storage) } -} - -/** - * Cancels the request in transit - * - * @exports - * @param {string} url - * @returns {void} - */ -export function cancel (url) { + /** + * Cancels the request in transit + * + * @exports + * @param {string} url + * @returns {void} + */ + const cancel = (url) => { - if (requests.has(url)) { + if (transit.has(url)) { - requests.get(url).abort() - requests.delete(url) + transit.get(url).abort() + transit.delete(url) - console.warn(`Pjax: XHR Request was cancelled for url: ${url}`) + console.warn(`Pjax: XHR Request was cancelled for url: ${url}`) + } } -} - -/** - * Prevents repeated requests from being executed. - * When prefetching, if a request is in transit and a click - * event dispatched this will prevent multiple requests and - * instead wait for initial fetch to complete. - * - * Number of recursive runs to make, set this to 85 to disable, - * else just leave it to execute as is. - * - * Returns `true` if request resolved in `850ms` else `false` - * - * @exports - * @param {string} url - * @param {number} [limit=0] - * @return {Promise} - */ -export async function inFlight (url, limit = 0) { - if (requests.has(url) && limit <= 85) { - if (store.config.progress && !progress.loading) { - if (limit === store.config.threshold.progress) progress.show() + /** + * Prevents repeated requests from being executed. + * When prefetching, if a request is in transit and a click + * event dispatched this will prevent multiple requests and + * instead wait for initial fetch to complete. + * + * Number of recursive runs to make, set this to 85 to disable, + * else just leave it to execute as is. + * + * Returns `true` if request resolved in `850ms` else `false` + * + * @exports + * @param {string} url + * @return {Promise} + */ + const inFlight = async (url) => { + + if (transit.has(url) && ratelimit <= store.config.request.throttle) { + // console.log('Request in flight', ratelimit * 25) + + if ((ratelimit * 10) >= store.config.progress.threshold) progress.start() + + return asyncTimeout(() => { + ratelimit++ + return inFlight(url) + }, 10) } - return asyncTimeout(() => inFlight(url, (limit + 1)), 25) + ratelimit = 0 + + return !transit.has(url) } - return !requests.has(url) + /** + * Fetches documents and guards from duplicated requests + * from being dispatched if an indentical fetch is in flight. + * Requests will always save responses and snapshots. + * + * @exports + * @param {object} state + * @return {Promise} + */ + const get = async (state) => { + + if (transit.has(state.url)) { + console.warn(`Pjax: XHR Request is already in transit for: ${state.url}`) + return false + } -} + if (!dispatchEvent('pjax:request', state.url, true)) { + console.warn(`Pjax: Request cancelled via dispatched event for: ${state.url}`) + return false + } -/** - * Fetches documents and guards from duplicated requests - * from being dispatched if an indentical fetch is in flight. - * - * @exports - * @param {IPjax.IState} state - * @param {boolean} [async=false] - * @return {Promise} - */ -export async function get (state, async = true) { + try { - if (requests.has(state.url)) { - console.warn(`Pjax: XHR Request is already in transit for: ${state.url}`) - return false - } + const response = await HttpRequest(state.url) - if (!dispatchEvent('pjax:request', state.location, true)) { - console.warn(`Pjax: Request cancelled via dispatched event for: ${state.url}`) - return false - } + if (typeof response === 'string') { + return store.create(state, response) + } - if (state.method !== 'prefetch') { - if (store.config.progress && !progress.loading) progress.show() - } + console.warn(`Pjax: Failed to receive response at: ${state.url}`) - try { + } catch (error) { - const response = await HttpRequest(state, async) + transit.delete(state.url) + console.error(error) - if (typeof response === 'string' && response.length > 0) { + } - if (!state.snapshot) state.snapshot = nanoid(16) + return false - snapshots.set(state.snapshot, response) + } - if (store.config.cache && !cache.has(state.url)) { - if (dispatchEvent('pjax:cache', state, true)) { - cache.set(state.url, state) - } + return { + get, + inFlight, + cancel, + transit, + + /** + * Returns request cache metrics + * + * @exports + * @returns {IPjax.ICacheSize} + */ + get cacheSize () { + + return { + requests: snapshots.size, + total: storage, + weight: byteConvert(storage) } - if (store.config.progress && progress.loading) progress.hide() - - return Promise.resolve(state) - - } else { - console.warn(`Pjax: Failed to receive response at: ${state.url}`) } - } catch (error) { - - requests.delete(state.url) - console.error(error) - } - return false - -} +})() diff --git a/src/app/store.js b/src/app/store.js index 13564f9..909bf76 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,4 +1,10 @@ import merge from 'mergerino' +import history from 'history/browser' +import { dispatchEvent } from './utils' +import { nanoid } from 'nanoid' +import path from './path' +import scroll from '../observers/scroll' +import * as nprogress from './progress' /** * Snapshots Cache @@ -7,191 +13,267 @@ import merge from 'mergerino' */ export const snapshots = new Map() -/** - * @exports - * @type {Map} - */ -export const transit = new Map() - -/** - * Tracked Elements - * - * @exports - * @type {Set} - */ -export const tracked = new Set() - -/** - * XHR Requests - * - * @type {Map} - */ -export const requests = new Map() - -/** - * Cache - * - * @exports - * @type {Map} - */ -export const cache = new Map() - /** * store */ -export const store = ( +export const store = (() => { /** - * @param {IPjax.IStoreState} state + * Preset Configuration + * + * @type {object} */ - state => ({ + let presets + + /** + * Cache + * + * @exports + * @type {Map} + */ + const cache = new Map() + + /* CLOSURE IIFE ------------------------------- */ + + return ({ /** - * Connect Store + * Connects store and intialized the workable + * state management model. Connect MUST be called + * upon Pjax initialization. This function acts + * as a class `constructor` establishing an instance. * - * MUST BE CALLED TO UPON INITIALIZATION - * - * @param {IPjax.IConfigPresets} options + * @param {Store.IPresets} [options] */ - connect (options) { + connect (options = {}) { - this.update.config(options) - this.update.page(this.config) + presets = merge( + { + targets: [ 'main', '#navbar' ], + request: { + timeout: 1500, + throttle: 150, + dispatch: 'mousedown' + }, + prefetch: { + mouseover: { + enable: true, + threshold: 100 + }, + intersect: { + enable: true, + threshold: 250 + } + }, + cache: { + enable: true, + limit: 25, + save: false + }, + progress: { + enable: true, + threshold: 350, + options: { + minimum: 0.10, + easing: 'ease', + speed: 225, + trickle: true, + trickleSpeed: 225, + showSpinner: false + } + } + } + , + { - } + // PRESETS PATCH COPY - , + ...options - /* -------------------------------------------- */ - /* STARTED */ - /* -------------------------------------------- */ + , request: { - get started () { + ...options?.request - if (state?.started) state.started = false + // PREVENT DISPATCH PROPERTY FROM PATCH - return state.started + , dispatch: undefined + } + , cache: { - } + ...options?.cache - , + // PREVENT SAVE PROPERTY FROM PATCH - set started (status) { + , save: undefined + } + } + ) - state.started = status + nprogress.config(this.config.progress.options) - } + }, - , - /* -------------------------------------------- */ - /* STORE GETTERS */ - /* -------------------------------------------- */ + /** + * Sets initial page state executing on intial load. + * Caches page so a return navigation does not perform + * an extrenous request. + * + * @param {Event} event + * @returns {void} + */ + initialize (event) { + + history.replace(history.location, store.create({ + url: path.url, + location: path.parse(path.url), + position: scroll.position + }, document.documentElement.outerHTML)) + + removeEventListener('load', this.initialize) + + }, /** - * @return {IPjax.IConfigPresets} + * Returns the Pjax preset configuration object. Presets are global + * configurations. This getter will give us access to the defined + * settings for this Pjax instance. + * + * @type {Store.IPresets} + * @return {Store.IPresets} */ get config () { - return state.config + return presets - } + }, - , + clear: () => { + + cache.clear() + snapshots.clear() + + }, /** - * @return {IPjax.IState} + * Check if cache record exists with snapshot + * + * @param {string} url + * @param {{snapshot?: boolean}} has + * @return {boolean} */ - get page () { + has: (url, has = { snapshot: false }) => { - return state.page + return !has.snapshot ? cache.has(url) : ( + cache.has(url) || snapshots.has(cache.get(url)?.snapshot) + ) - } + }, - , + /** + * Saves a HTML Document String as snapshot + * + * @param {string} id + * @returns {string} + */ + snapshot: (id) => { - /* -------------------------------------------- */ - /* UPDATES */ - /* -------------------------------------------- */ + return snapshots.get(id) - update: { + }, - /* CONFIG ------------------------------------- */ + /** + * Inserts a page into cache. This function is called upon + * a new page instance being generated via `new()`. Optionally + * pass in an `object`to assert into new state. + * + * @param {string} url + * @returns {Store.IPage} + */ + cache (url) { - config: ( - initial => - patch => ( - state.config = merge(initial, patch) - ) - )( - { - target: [ 'main', '#navbar' ], - method: 'replace', - prefetch: true, - cache: true, - throttle: 0, - progress: false, - threshold: { - intersect: 250, - hover: 100, - progress: 10 - } - } - ) + return cache.get(url) - , - - /* PAGE --------------------------------------- */ - - page: ( - initial => - patch => ( - state.page = merge( - initial, - { - ...patch - , target: state.config.target - , action: { - replace: null, - append: null, - prepend: null - } - } - ) - ) - )( - { - url: '', - snapshot: '', - captured: null, - target: [], - title: '', - method: 'replace', - prefetch: 'hover', - cache: null, - progress: false, - action: { - replace: null, - prepend: null, - append: null - }, - location: { - origin: '', - hostname: '', - href: '', - pathname: '', - search: '', - hash: '', - lastUrl: '' - }, - position: { - x: 0, - y: 0 - } - } - ) + }, + + /** + * Update current pushState History + * + * @param {string} url + * @returns {void} + */ + history (url) { + + history.replace(history.location, { + ...history.location.state, + position: scroll.position + }) + + }, + + /** + * Updates page state, this function will run a merge + * on the current page instance and re-assign the `pageState` + * letting to updated config. + * + * If newState contains a different `ILocation.url` value from + * that of the current page instance `url` then it will be updated + * to match that of the `newState.url` value. + * + * The cache will e updated accordingly, so `this.page` will provide + * access to the updated instance. + * + * @param {Store.IPage} state + * @returns {Store.IPage} + */ + update: (state) => { + + const page = cache + .set(state.url, merge(cache.get(state.url), state)) + .get(state.url) + + return page + + }, + + /** + * Indicates a new page visit or a return page visit. New visits + * are defined by an event dispatched from a `href` link. Both a new + * new page visit or subsequent visit will call this function. + * + * **Breakdown** + * + * Subsequent visits calling this function will have their per-page + * specifics configs (generally those configs set with attributes) + * reset and merged into its existing records (if it has any), otherwise + * a new page instance will be generated including defult preset configs. + * + * @param {Store.IPage} state + * @param {string} snapshot + * @returns {Store.IPage} + */ + create (state, snapshot) { + + const page = { + captured: false, + append: false, + prepend: false, + snapshot: state?.snapshot || nanoid(16), + position: state?.position || scroll.reset(), + targets: this.config.targets, + cache: this.config.cache.enable, + progress: this.config.progress.threshold, + threshold: this.config.prefetch.mouseover.threshold, + ...state + } + + snapshots.set(page.snapshot, snapshot) + + if (dispatchEvent('pjax:cache', page, true)) cache.set(page.url, page) + + return page } }) -)(Object.create(null)) +})() diff --git a/src/app/utils.js b/src/app/utils.js index bb100c3..96b908d 100644 --- a/src/app/utils.js +++ b/src/app/utils.js @@ -1,39 +1,25 @@ import { isNumber } from '../constants/regexp' import { Units } from './../constants/common' -import { getURL } from './location' -import { cache } from './store' - -/** - * Handles a clicked link, prevents special click types. - * - * @exports - * @param {MouseEvent} event - * @return {boolean} - */ -export function linkEvent (event) { - - // @ts-ignore - return !((event.target && event.target.isContentEditable) || - event.defaultPrevented || - event.which > 1 || - event.altKey || - event.ctrlKey || - event.metaKey || - event.shiftKey) - -} +import path from './path' +import { store } from './store' /** * Locted the closest link when click bubbles. * * @exports - * @param {EventTarget} target + * @param {EventTarget|MouseEvent} target * @param {string} selector * @return {Element|false} */ -export function linkLocate (target, selector) { +export function getLink (target, selector) { + + if (target instanceof Element) { + const element = target.closest(selector) + if (element && element.tagName === 'A') return element + } + + return false - return target instanceof Element ? target.closest(selector) : false } /** @@ -127,7 +113,7 @@ export function byteSize (string) { */ export function canFetch (target) { - return !cache.has(getURL(target)) + return !store.has(path.get(target), { snapshot: true }) } /** @@ -174,7 +160,10 @@ export function byteConvert (bytes) { */ export function asyncTimeout (callback, ms = 0) { - return new Promise(resolve => setTimeout(() => resolve(callback()), ms)) + return new Promise(resolve => setTimeout(() => { + const fn = callback() + resolve(fn) + }, ms)) } /** diff --git a/src/app/visit.js b/src/app/visit.js deleted file mode 100644 index ce4e81e..0000000 --- a/src/app/visit.js +++ /dev/null @@ -1,257 +0,0 @@ -import { cache, store, transit } from './store' -import { expandURL } from '../app/location' -import { forEach, jsonattrs, chunk } from '../app/utils' -import * as regexp from '../constants/regexp' -import * as scroll from '../observers/scrolling' -import * as prefetch from './prefetch' -import * as render from './render' -import * as request from './request' -import history from 'history/browser' - -/** - * @type {string[]} - */ -const attrs = [ - 'target', - 'method', - 'action', - 'prepend', - 'append', - 'replace', - 'prefetch', - 'cache', - 'progress', - 'throttle', - 'position', - 'reload' -] - -/** - * Get State Page - * - * @export - * @param {Element} target - * @return {IPjax.IState} - */ -export function getPageState (target) { - - const location = expandURL(target.getAttribute('href')) - const url = history.createHref(location) - - return cache.has(url) ? cache.get(url) : store.update.page({ - url, - location - }) - -} - -/** - * Parses link `href` attributes and assigns them to - * configuration options. Each link target can define - * navigation options. - * - * @export - * @param {Element} target - * @return {IPjax.IState} - */ -export function visitClickState (target) { - - const state = getPageState(target) - - state.method = 'click' - - return getVisitConfig(state, target) - -} - -/** - * Parses link `href` attributes and assigns them to - * configuration options. - * - * @export - * @param {IPjax.IState} state - * @param {Element} target - * @return {IPjax.IState} - */ -export function getVisitConfig (state, target) { - - forEach(attrs, prop => { - - const value = target.getAttribute(`data-pjax-${prop}`) - - value === null ? ( - - // MONKEY PATCH - // Assert prefetch value to `false` if no prefetch attribute is defined - prop === 'prefetch' && value !== 'hover' && value !== 'intersect') || ( - - state[prop] = false - - // data-pjax-prefetch="false" - - ) : regexp.isAction.test(prop) ? ( - - state.action[prop] = prop === 'replace' ? ( - - value.match(regexp.ActionParams) - - // data-pjax-replace="(['.foo'])" - // data-pjax-replace="(['.foo','.bar'])" - - ) : ( - - value.match(regexp.ActionParams).reduce(chunk(2), []) - - // data-pjax-append="(['.foo', '.bar'])" - // data-pjax-append="(['.foo', '.bar'],['.baz','.faz'])" - - // ---------- OR --------------- - - // data-pjax-prepend="(['.foo', '.bar'])" - // data-pjax-prepend="(['.foo', '.bar'],['.baz','.faz'])" - - ) - ) : ( - - // DEPRECATE IN FAVOR OF REPLACE, APPEND OR PREPEND ACTIONS - // - state[prop] = prop === 'target' ? ( - - value.split(regexp.isWhitespace) - - // data-pjax-target=".foo" - // data-pjax-target=".foo .bar #baz" - - ) : prop === 'progress' ? ( - - (value === 'false' || value === '0' || Number(value) > 85) - ? false - : (Number(value) < 85 || Number(value) > 1) ? Number(value) : state.progress - - // data-pjax-progress="50" - // data-pjax-progress="true" - // data-pjax-progress="false" - - ) : prop === 'cache' ? ( - - value === 'false' - ? false - : value === 'true' - ? true - : value.trim() - - // data-pjax-cache="true" - // data-pjax-cache="false" - // data-pjax-cache="flush" - // data-pjax-cache="reset" - - ) : prop === 'position' ? ( - - value.match(regexp.isPosition).reduce(jsonattrs, {}) - - // data-pjax-position="x:200" - // data-pjax-position="x:1000 y:0" - - ) : regexp.isBoolean.test(value.trim()) ? ( - - value === 'true' - - // data-pjax-*="true" - // data-pjax-*="false" - - ) : regexp.isNumber.test(value.trim()) ? ( - - Number(value) - - // data-pjax-*="1000" - // data-pjax-*="10.00" - - ) : ( - - value.trim() - - // data-pjax-*="string" - - ) - ) - - }) - - // THRESHOLD CONFIGURATION - // SET THE THRESHOLD STATE RELATING TO THE PREFETCH - if (target.hasAttribute('data-pjax-threshold')) { - if (regexp.isPrefetch.test(state.prefetch)) { - const threshold = target.getAttribute('data-pjax-threshold') - state.threshold[state.prefetch] = Number(threshold) - } - } - - return state - -} - -/** - * Executes a pjax navigation. - * - * @export - * @param {IPjax.IState} state - */ -export function navigate (state) { - - state.position = scroll.setPosition(state) - - return cache.has(state.url) - ? cacheVisit(state) - : pjaxVisit(state) - -} - -/** - * Executes a visit by fetching the the cached response - * from the session and passes it to the renderer. - * - * @export - * @param {IPjax.IState} state - * @returns {void} - */ -export function cacheVisit (state) { - - prefetch.stop() - - const page = cache - .set(state.url, state) - .get(state.url) - - return render.update(page) - -} - -/** - * Pjax link handler which dispatches a fetch request - * when `href` tag is clicked. If clicked link is in transit - * from prefetch it will pass to cache visit - * - * @export - * @param {IPjax.IState} state - * @param {boolean} [async=false] - * @returns {Promise} - */ -export async function pjaxVisit (state, async = false) { - - if (transit.has(state.url)) { - if ((await request.inFlight(state.url))) { - return cacheVisit(state) - } else { - request.cancel(state.url) - } - } - - prefetch.stop() - - const page = await request.get(state) - - return page - ? render.update(page, async) - : window.location.replace(state.location.href) - -} diff --git a/src/constants/regexp.js b/src/constants/regexp.js index 28dd93c..c36d42d 100644 --- a/src/constants/regexp.js +++ b/src/constants/regexp.js @@ -1,3 +1,23 @@ +/** + * Attribute Configuration + * + * Used to match Pjax data attribute names + * + * @exports + * @type {RegExp} + */ +export const Attr = /^data-pjax-(append|prepend|replace|prefetch|progress|threshold|position)$/i + +/** + * URL Pathname + * + * Used to match first pathname from a URL (group 1) + * + * @exports + * @type {RegExp} + */ +export const Pathname = /\/\/[^/]*(\/[^;]*)/ + /** * Form Inputs * @@ -16,7 +36,7 @@ export const FormInputs = /^(input|textarea|select|datalist|button|output)$/i * @exports * @type {RegExp} */ -export const isReady = /^(?:interactive|complete)$/ +export const isReady = /^(interactive|complete)$/i /** * Boolean Attribute value @@ -26,7 +46,7 @@ export const isReady = /^(?:interactive|complete)$/ * @exports * @type {RegExp} */ -export const isBoolean = /\b(true|false)\b/ +export const isBoolean = /^(true|false)$/i /** * Matches decimal number @@ -68,6 +88,16 @@ export const isAction = /\b(?:ap|pre)pend|replace/g */ export const isReplace = /\b(?:append|prepend)\b/ +/** + * Cache Attribute + * + * Used to match and validate a cache attribute config + * + * @exports + * @type {RegExp} + */ +export const isCache = /\b(?:false|true|reset|flush)\b/ + /** * Threshold Attribute Value * @@ -98,6 +128,45 @@ export const isThreshold = /\b(?:intersect|mouseover|progress)\b|(?<=[:])[^\s][0 */ export const ActionParams = /[^,'"[\]()\s]+/g +/** + * Array Value + * + * Used to test value for a string array attribute value, like data-pjax-replace. + * + * @example + * https://regex101.com/r/I77U9B/1 + * + * @exports + * @type {RegExp} + */ +export const isArray = /\(?\[['"].*?['"],?\]\)?/ + +/** + * Append or Prepend attribute value + * + * Used to test value for append or prepend, array within array + * + * @example + * https://regex101.com/r/QDSRBK/1 + * + * @exports + * @type {RegExp} + */ +export const isPenderValue = /\(?(\[(['"].*?['"],?){2}\],?)\1?\)?/ + +/** + * Test Position Attributes + * + * Tests attribute values for a position config + * + * @example + * https://regex101.com/r/DG2LI1/1 + * + * @exports + * @type {RegExp} + */ +export const isPosition = /[xy]:[0-9.]+/ + /** * Mached Position Attributes * @@ -106,7 +175,7 @@ export const ActionParams = /[^,'"[\]()\s]+/g * @exports * @type {RegExp} */ -export const isPosition = /[xy]|(?<=[:])[0-9]+/g +export const inPosition = /[xy]|\d*\.?\d+/g /** * Protocol diff --git a/src/index.js b/src/index.js index d1af8a8..2c1beba 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,9 @@ import { Protocol } from './constants/regexp' -import { store, cache } from './app/store' -import { navigate } from './app/visit' +import { store } from './app/store' import { nanoid } from 'nanoid' -import { captureDOM } from './app/render' +import render from './app/render' +import path from './app/path' +import hrefs from './observers/hrefs' import * as controller from './app/controller' /** @@ -19,7 +20,7 @@ export const supported = !!( /** * Connect Pjax * - * @param {IPjax.IConfigPresets} options + * @param {Store.IPresets} options */ export const connect = options => { @@ -52,7 +53,7 @@ export const uuid = (size = 12) => nanoid(size) /** * Flush Cache */ -export const flush = () => cache.clear() +export const flush = () => store.clear() /** * Capture DOM @@ -60,15 +61,21 @@ export const flush = () => cache.clear() * @param {string} url * @param {object} action */ -export const capture = (url, action) => captureDOM(url, action) +export const capture = (url, action) => render.captureDOM(path.get(url), action) /** * Visit * * @param {string} url - * @param {IPjax.IState} state + * @param {Store.IPage} state */ -export const visit = (url, state = store.page) => navigate({ ...state, url }) +export const visit = (url, state) => { + + url = path.get(url, true) + + return hrefs.navigate(url, { ...state, url, location: path.parse(url) }) + +} /** * Disconnect diff --git a/src/observers/history.js b/src/observers/history.js index b2fdb5b..c074984 100644 --- a/src/observers/history.js +++ b/src/observers/history.js @@ -1,98 +1,101 @@ import history from 'history/browser' -import { store, cache } from '../app/store' -import { expandURL } from '../app/location' -import * as prefetch from '../app/prefetch' -import * as render from '../app/render' -import * as request from '../app/request' - -/** - * @type {boolean} - */ -let started = false - -/** - * @type {function} - */ -let unlisten = null - -/** - * @type {string} - */ -let inTransit = null +import { store } from '../app/store' +import { createPath } from 'history' +import render from '../app/render' +import request from '../app/request' /* -------------------------------------------- */ /* FUNCTIONS */ /* -------------------------------------------- */ - /** - * Attached `history` event listener. + * Link (href) handler * - * @exports - * @returns {void} + * @typedef {Store.IPage|string|boolean} click + * @param {boolean} connected */ -export function start () { +export default (function (connected) { - if (!started) { - unlisten = history.listen(listener) - started = false - } -} - -/** - * Removed `history` event listener. - * - * @export - * @returns {void} + /** + * @type {function} */ -export function stop () { + let unlisten = null - if (!started) { - unlisten() - started = true - } -} + /** + * @type {string} + */ + let inTransit = null -/** - * Event History dispatch controller, handles popstate, - * push and replace events via third party module - * - * @param {import('history').BrowserHistory} event - */ -function listener ({ action, location }) { + /** + * Popstate Navigation + * + * @param {string} url + * @param {Store.IPage} state + * @returns {Promise} + */ + const popstate = async (url, state) => { - if (action === 'POP') return popstate(history.createHref(location)) + // console.log(state) - console.log(action, location.state) + if (url !== inTransit) request.cancel(inTransit) -} + if (store.has(url, { snapshot: true })) { + return render.update(store.cache(url), true) + } -/** - * Popstate Navigations - * - * @param {string} url - * @returns {Promise} - */ -async function popstate (url) { + inTransit = url - prefetch.stop() + const page = await request.get(state) - if (inTransit && url !== inTransit) request.cancel(inTransit) + return page + ? render.update(page, true) + : window.location.replace(url) - if (cache.has(url)) { + } - render.update(cache.get(url), true) + /** + * Event History dispatch controller, handles popstate, + * push and replace events via third party module + * + * @param {import('history').BrowserHistory} event + */ + const listener = ({ action, location }) => { - } else { + // console.log(action, location) - inTransit = url + if (action === 'POP') { + return popstate(createPath(location), location.state) + } - const state = store.update.page({ location: expandURL(url), url }) - const response = await request.get(state) + } - if (response && response.url === url) render.update(response, true) + return { + + /** + * Attached `history` event listener. + * + * @returns {void} + */ + start: () => { + + if (!connected) { + unlisten = history.listen(listener) + connected = false + } + }, + + /** + * Removed `history` event listener. + * + * @returns {void} + */ + stop: () => { + + if (!connected) { + unlisten() + connected = true + } + } } - prefetch.start() - -} +})(false) diff --git a/src/observers/hover.js b/src/observers/hover.js new file mode 100644 index 0000000..66e77db --- /dev/null +++ b/src/observers/hover.js @@ -0,0 +1,229 @@ +import { LinkPrefetchHover } from '../constants/common' +import path from '../app/path' +import { store } from '../app/store' +import { getLink, getTargets } from '../app/utils' +import hrefs from './hrefs' +import request from '../app/request' + +/** + * Link (href) handler + * + * @typedef {string|Store.IPage} clickState + * @param {boolean} connected + */ +export default (function (connected) { + + /** + * @exports + * @type {Map} + */ + const transit = new Map() + + /** + * @type {IPjax.IPosition} + */ + const position = { x: 0, y: 0 } + + /** + * Cleanup throttlers + * + * @param {string} url + * @returns {boolean} + */ + const cleanup = (url) => { + + clearTimeout(transit.get(url)) + return transit.delete(url) + } + + /** + * Cancels prefetch, if mouse leaves target before threshold + * concludes. This prevents fetches being made for hovers that + * do not exceeds threshold. + * + * @exports + * @param {MouseEvent} event + */ + function onMouseleave (event) { + + const target = getLink(event.target, LinkPrefetchHover) + + if (target) { + cleanup(path.get(target)) + target.removeEventListener('mouseleave', onMouseleave, true) + } + + } + + /** + * Fetch Throttle + * + * @param {string} url + * @param {function} fn + * @param {number} delay + * @returns {void} + */ + const throttle = (url, fn, delay) => { + if (!store.has(url) && !transit.has(url)) { + transit.set(url, setTimeout(fn, delay)) + } + } + + /** + * Fetch document and add the response to session cache. + * Lifecycle event `pjax:cache` will fire upon completion. + * + * @param {Store.IPage} state + * @returns{Promise} + */ + const prefetch = async state => { + + if (!(await request.get(state))) { + console.warn(`Pjax: Prefetch will retry on next mouseover for: ${state.url}`) + } + + return cleanup(state.url) + } + + /** + * Attempts to visit location, Handles bubbled mousovers and + * Dispatches to the fetcher. Once item is cached, the mouseover + * event is removed. + * + * @param {MouseEvent} event + */ + const onMouseover = (event) => { + + const target = getLink(event.target, LinkPrefetchHover) + + if (!target) return undefined + + const url = path.get(target) + + if (store.has(url, { snapshot: true })) return disconnect(target) + + target.addEventListener('mouseleave', onMouseleave, true) + + const state = hrefs.attrparse(target, { + url, + location: path.parse(url), + position: { x: 0, y: 0 } + }) + + throttle(url, async () => { + if ((await prefetch(state))) { + target.removeEventListener('mouseover', onMouseover, false) + } + }, state?.threshold || store.config.prefetch.mouseover.threshold) + } + + /** + * Attempts to visit location, Handles bubbled mousovers and + * Dispatches to the fetcher. Once item is cached, the mouseover + * event is removed. + * + * @param {MouseEvent} event + */ + const onMouseMove = event => { + + position.x = event.pageX + position.y = event.pageY + + } + + /** + * + * @param {Element} target + * @param {number} index + */ + const proximity = (target, index) => { + + const { top, left } = target.getBoundingClientRect() + const { scrollTop, scrollLeft } = document.body + + // @ts-ignore + target.proximity = Math.floor( + Math.sqrt( + Math.pow(position.x - ((left + scrollLeft) + (target.clientWidth / 2)), 2) + + Math.pow(position.y - ((top + scrollTop) + (target.clientHeight / 2)), 2) + ) + ) + + // @ts-ignore + console.log(target.proximity) + + // @ts-ignore + if (target.proximity < 100) { + console.log(index, target) + // elements.splice(index, 1) + } + + } + + /** + * Attach mouseover events to all defined element targets + * + * @param {EventTarget} target + * @param {number} index + * @param {Element[]} items + * @returns {void} + */ + const handleHover = (target, index, items) => { + + // if (target instanceof Element) proximity(target, index) + + target.addEventListener('mouseover', onMouseover, false) + + } + + /** + * Adds and/or Removes click events. + * + * @param {EventTarget} target + * @returns {void} + */ + function disconnect (target) { + target.removeEventListener('mouseleave', onMouseleave, false) + target.removeEventListener('mouseover', onMouseover, false) + } + + return { + + /** + * Starts mouseovers, will attach mouseover events + * to all elements which contain a `data-pjax-prefetch="hover"` + * data attribute + * + * @export + * @returns {void} + */ + start: () => { + + if (!connected) { + getTargets(LinkPrefetchHover).forEach(handleHover) + // addEventListener('mousemove', onMouseMove, false) + connected = true + } + }, + + /** + * Stops mouseovers, will remove all mouseover and mouseout + * events on elements which contains a `data-pjax-prefetch="hover"` + * unless target href already exists in cache. + * + * @export + * @returns {void} + */ + stop: () => { + + if (connected) { + transit.clear() + getTargets(LinkPrefetchHover).forEach(disconnect) + // removeEventListener('mousemove', onMouseMove, false) + connected = false + } + } + + } + +})(false) diff --git a/src/observers/hrefs.js b/src/observers/hrefs.js index e735edc..7a0ad65 100644 --- a/src/observers/hrefs.js +++ b/src/observers/hrefs.js @@ -1,83 +1,239 @@ -import { navigate, visitClickState } from '../app/visit' -import { dispatchEvent, linkEvent, linkLocate } from '../app/utils' +import { dispatchEvent, getLink, chunk } from '../app/utils' import { Link } from '../constants/common' -import { onMouseleave } from './mouseover' +import { store } from '../app/store' +import path from '../app/path' +import scroll from './scroll' +import request from '../app/request' +import render from '../app/render' +import * as regexp from '../constants/regexp' /** - * @type {boolean} + * Link (href) handler + * + * @typedef {string|Store.IPage} clickState + * @param {boolean} connected */ -let started = false +export default (function (connected) { + + /** + * Constructs a JSON object from HTML `data-pjax-*` attributes. + * Attributes are passed in as array items + * + * @exports + * @param {object} accumulator + * @param {string} current + * @param {number} index + * @param {object} source + * + * @example + * + * // Attribute values are seperated by whitespace + * // For example, a HTML attribute would look like: + * + * + * // Attribute values are split into an Array + * // The array is passed to this reducer function + * ["string", "foo", "number", "200"] + * + * // This reducer function will return: + * { string: 'foo', number: 200 } + * + */ + const jsonattrs = (accumulator, current, index, source) => { + + return (index % 2 ? ({ + ...accumulator + , [source[(source.length - 1) >= index ? ( + index - 1 + ) : index]]: regexp.isNumber.test(current) ? Number(current) : current + }) : accumulator) -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ + } -/** - * Attached `click` event listener. - * - * @exports - * @returns {void} - */ -export function start () { + /** + * Handles a clicked link, prevents special click types. + * + * @exports + * @param {MouseEvent} event + * @return {boolean} + */ + const linkEvent = (event) => { + + // @ts-ignore + return !((event.target && event.target.isContentEditable) || + event.defaultPrevented || + event.which > 1 || + event.altKey || + event.ctrlKey || + event.metaKey || + event.shiftKey) - if (!started) { - addEventListener('click', observe, true) - started = true } -} + /** + * Executes a pjax navigation. + * + * @param {string} url + * @param {Store.IPage|false} [state=false] + * @export + */ + const navigate = async (url, state = false) => { -/** - * Removed `click` event listener. - * - * @export - * @returns {void} - */ -export function stop () { + if (state) { + + console.log(state) + if (store.has(url, { snapshot: true })) return render.update(state) + + const page = await request.get(state) + if (page) return render.update(page) + + } else { + if ((await request.inFlight(url))) { + return render.update(store.cache(url)) + } else { + request.cancel(url) + } + } + + return window.location.replace(url) - if (started) { - removeEventListener('click', observe, true) - started = false } -} + /** + * Parses link `href` attributes and assigns them to + * configuration options. + * + * @export + * @param {Element} target + * @param {Store.IPage} [state] + * @returns {Store.IPage} + */ + const attrparse = ( + { attributes } + , state = {} + ) => ([ ...attributes ].reduce(( + config + , { + nodeName, + nodeValue + } + ) => { + + if (!regexp.Attr.test(nodeName)) return config + + const value = nodeValue.replace(/\s+/g, '') + + config[nodeName.substring(10)] = regexp.isArray.test(value) ? ( + value.match(regexp.ActionParams) + ) : regexp.isPenderValue.test(value) ? ( + value.match(regexp.ActionParams).reduce(chunk(2), []) + ) : regexp.isPosition.test(value) ? ( + value.match(regexp.inPosition).reduce(jsonattrs, {}) + ) : regexp.isBoolean.test(value) ? ( + value === 'true' + ) : regexp.isNumber.test(value) ? ( + Number(value) + ) : ( + value + ) + + return config + + }, state)) + + /** + * Triggers click event + * + * @param {Element} target + * @returns {(state: clickState) => (event: MouseEvent) => void} + */ + const handleClick = target => state => function click (event) { -/** - * Adds and/or Removes click events. - * - * @returns {void} - */ -function observe () { + event.preventDefault() + event.stopPropagation() + target.removeEventListener('click', click, false) - removeEventListener('click', onClick, false) - addEventListener('click', onClick, false) + if (!dispatchEvent('pjax:click', {}, true)) return undefined -} + return typeof state === 'object' + ? render.update(state) + : typeof state === 'string' + ? navigate(state) + : window.location.replace(path.absolute(path.url)) -/** - * Attempts to visit href location, Handles click bubbles and - * Dispatches a `pjax:click` event respecting the cancelable - * `preventDefault()` from user event - * - * @param {MouseEvent} event - * @returns {Promise} - */ -function onClick (event) { + } - if (linkEvent(event)) { + /** + * Triggers a page fetch + * + * @param {MouseEvent} event + */ + const onMousedown = event => { - event.preventDefault() + // window.performance.mark('started') + + if (!linkEvent(event)) return undefined - const target = linkLocate(event.target, Link) + const target = getLink(event.target, Link) - if (target && target.tagName === 'A') { + if (!target) return undefined - const state = visitClickState(target) + store.history(path.url) - if (dispatchEvent('pjax:click', state, true)) { - if (state.method === 'prefetch') onMouseleave(event) - return navigate(state) + const url = path.get(target, true) + const click = handleClick(target) + + if (request.transit.has(url)) { + target.addEventListener('click', click(url), false) + } else { + + const state = attrparse(target, { + url, + location: path.parse(url), + position: scroll.set(path.url) + }) + + if (store.has(url, { snapshot: true })) { + target.addEventListener('click', click(store.update(state)), false) + } else { + request.get(state) // TRIGGER FETCH + target.addEventListener('click', click(url), false) } } + } -} + + return { + + attrparse, + navigate, + + /** + * Attached `click` event listener. + * + * @returns {void} + */ + start: () => { + + if (!connected) { + addEventListener('mousedown', onMousedown, true) + connected = true + } + }, + + /** + * Removed `click` event listener. + * + * @returns {void} + */ + stop: () => { + + if (connected) { + removeEventListener('mousedown', onMousedown, true) + connected = false + } + } + + } + +})(false) diff --git a/src/observers/intersect.js b/src/observers/intersect.js index 90fab73..3855a75 100644 --- a/src/observers/intersect.js +++ b/src/observers/intersect.js @@ -1,102 +1,95 @@ import { LinkPrefetchIntersect } from '../constants/common' import { getTargets } from '../app/utils' -import { transit, store } from '../app/store' -import { getPageState, getVisitConfig } from '../app/visit' -import * as request from '../app/request' +import hrefs from './hrefs' +import path from '../app/path' +import request from '../app/request' /** - * @type IntersectionObserver + * @param {boolean} connect */ -let entries = null +export default (function (connect) { -/** - * @type Boolean - */ -let started = false + /** + * @type IntersectionObserver + */ + let entries = null -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ + /** + * Intersection callback when entries are in viewport. + * + * @param {IntersectionObserverEntry} params + * @returns {Promise} + */ + const onIntersect = async ({ isIntersecting, target }) => { -/** - * Starts prefetch, will initialize `IntersectionObserver` and - * add event listeners and other logics. - * - * @exports - * @returns {void} - */ -export function start () { + if (isIntersecting) { - if (store.config.prefetch) { - if (!started) { - entries = new IntersectionObserver(intersect) - getTargets(LinkPrefetchIntersect).forEach(observe) - started = true - } - } -} + const url = path.get(target) + const state = hrefs.attrparse(target, { url, location: path.parse(target) }) -/** - * Stops prefetch, will disconnect `IntersectionObserver` and - * remove any event listeners or transits. - * - * @exports - * @returns {void} - */ -export function stop () { + const response = await request.get(state) + + if (response) { + entries.unobserve(target) + } else { + console.warn(`Pjax: Prefetch will retry at next intersect for: ${state.url}`) + entries.observe(target) + } - if (store.config.prefetch) { - if (started) { - transit.clear() - entries.disconnect() - started = false } } -} - -/** - * Begin Observing `href` links - * - * @param {Element} target - * @returns {void} - */ -function observe (target) { - return entries.observe(target) -} + /** + * Begin Observing `href` links + * + * @param {Element} target + * @returns {void} + */ + const observe = target => entries.observe(target) + + /** + * Start Intersection Observer and iterate over entries. + * + * @type {IntersectionObserverCallback} + * @returns {void} + */ + const intersect = entries => entries.forEach(onIntersect) + + return { + + /** + * Starts prefetch, will initialize `IntersectionObserver` and + * add event listeners and other logics. + * + * @exports + * @returns {void} + */ + start: () => { + + if (!connect) { + entries = new IntersectionObserver(intersect) + getTargets(LinkPrefetchIntersect).forEach(observe) + connect = true + } + + }, + + /** + * Stops prefetch, will disconnect `IntersectionObserver` and + * remove any event listeners or transits. + * + * @exports + * @returns {void} + */ + stop: () => { + + if (connect) { + entries.disconnect() + connect = false + } -/** - * Start Intersection Observer and iterate over entries. - * - * @type {IntersectionObserverCallback} - * @returns {void} - */ -function intersect (entries) { - - return entries.forEach(onIntersection) -} - -/** - * Intersection callback when entries are in viewport. - * - * @param {IntersectionObserverEntry} params - * @returns {Promise} - */ -async function onIntersection ({ isIntersecting, target }) { - - if (isIntersecting) { - - const state = getVisitConfig(getPageState(target), target) - state.method = 'prefetch' - - const response = await request.get(state) - - if (response) { - entries.unobserve(target) - } else { - console.warn(`Pjax: Prefetch will retry at next intersect for: ${state.url}`) - entries.observe(target) } } -} + +})(false) diff --git a/src/observers/mouseover.js b/src/observers/mouseover.js deleted file mode 100644 index da12a35..0000000 --- a/src/observers/mouseover.js +++ /dev/null @@ -1,182 +0,0 @@ -import { LinkPrefetchHover } from '../constants/common' -import { getURL } from '../app/location' -import { transit, cache, store } from '../app/store' -import { linkEvent, linkLocate, getTargets } from '../app/utils' -import * as visit from '../app/visit' -import * as request from '../app/request' - -/** - * @type Boolean - */ -let started = false - -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ - -/** - * Starts mouseovers, will attach mouseover events - * to all elements which contain a `data-pjax-prefetch="hover"` - * data attribute - * - * @export - * @returns {void} - */ -export function start () { - - if (store.config.prefetch) { - if (!started) { - getTargets(LinkPrefetchHover).forEach(observe) - started = true - } - } -} - -/** - * Stops mouseovers, will remove all mouseover and mouseout - * events on elements which contains a `data-pjax-prefetch="hover"` - * unless target href already exists in cache. - * - * @export - * @returns {void} - */ -export function stop () { - - if (store.config.prefetch) { - if (started) { - transit.clear() - getTargets(LinkPrefetchHover).forEach(disconnect) - started = false - } - } -} - -/** - * Cancels prefetch, if mouse leaves target before threshold - * concludes. This prevents fetches being made for hovers that - * do not exceeds threshold. - * - * @exports - * @param {MouseEvent} event - */ -export function onMouseleave (event) { - - if (linkEvent(event)) { - - const target = linkLocate(event.target, LinkPrefetchHover) - - if (target) { - cleanup(getURL(target)) - target.removeEventListener('mouseleave', onMouseleave, true) - // console.log('pjax: Prefetch cancelled, hover length too short') - } - } -} - -/** - * Attempts to visit location, Handles bubbled mousovers and - * Dispatches to the fetcher. Once item is cached, the mouseover - * event is removed. - * - * @param {MouseEvent} event - */ -function onMouseover (event) { - - if (linkEvent(event)) { - - const target = linkLocate(event.target, LinkPrefetchHover) - - if (target) { - - const state = visit.getPageState(target) - - if (cache.has(state.url)) return disconnect(target) - - target.addEventListener('mouseleave', onMouseleave, true) - - throttle(state.url, () => { - - state.method = 'prefetch' - - prefetch(visit.getVisitConfig(state, target), target) - - }, store.config.threshold.hover) - - } - } -} - -/** - * Attach mouseover events to all defined element targets - * - * @param {EventTarget} target - * @returns {void} - */ -function observe (target) { - - target.addEventListener('mouseover', onMouseover, true) - -} - -/** - * Cleanup throttlers - * - * @param {string} url - * @returns {boolean} - */ -function cleanup (url) { - - clearTimeout(transit.get(url)) - - return transit.delete(url) - -} - -/** - * Fetch Throttle - * - * @param {string} url - * @param {function} fn - * @param {number} delay - * @returns {Map} - */ -function throttle (url, fn, delay) { - - if (!cache.has(url) && !transit.has(url)) { - return transit.set(url, setTimeout(fn, delay)) - } -} - -/** - * Fetch document and add the response to session cache. - * Lifecycle event `pjax:cache` will fire upon completion. - * - * @param {IPjax.IState} state - * @param {Element} target - * @returns{Promise} - */ -async function prefetch (state, target) { - - const response = await request.get(state) - - if (response) { - disconnect(target) - } else { - console.warn(`Pjax: Prefetch will retry on next mouseover for: ${state.url}`) - } - - cleanup(state.url) - -} - -/** - * Adds and/or Removes click events. - * - * @param {EventTarget} target - * @returns {void} - */ -function disconnect (target) { - - target.removeEventListener('mouseleave', onMouseleave, false) - target.removeEventListener('mouseover', onMouseover, false) -} diff --git a/src/observers/scroll.js b/src/observers/scroll.js new file mode 100644 index 0000000..8c50571 --- /dev/null +++ b/src/observers/scroll.js @@ -0,0 +1,114 @@ +import { store } from '../app/store' + +/** + * Scroll position handler + * + * @param {boolean} connected + */ +export default (function (connected) { + + /** + * @type {IPjax.IPosition} + */ + let position = { x: 0, y: 0 } + + /** + * Resets the scroll position`of the document, applying + * a `x`and `y` positions to `0` + * + * @exports + * @returns {IPjax.IPosition} + */ + const reset = () => { + + position.x = 0 + position.y = 0 + + return position + + } + + /** + * onScroll event, asserts the current X and Y page + * offset position of the document + * + * @returns {void} + */ + const onScroll = () => { + position.x = window.pageXOffset + position.y = window.pageYOffset + } + + return { + + reset, + + /** + * Returns to current scroll position, the `reset()` + * function **MUST** be called after referencing this + * to reset position. + * + * @exports + * @returns {IPjax.IPosition} + */ + get position () { + + return position + }, + + /** + * Sets scroll position to the cache reference and + * returns a reset position. + * + * This function is called before a new page visit + * navigation begins, as it will assert the current + * position to the current page and return the reset + * position, ie: `{x: 0, y: 0 }`) to new page visit. + * + * + * @exports + * @param {string} url + * @returns {Store.IPosition} + */ + set (url) { + + // We assert current position here + + if (store.has(url)) { + store.cache(url).position = this.position + } + + return reset() + + }, + + /** + * Attached `scroll` event listener. + * + * @returns {void} + */ + start: () => { + + if (!connected) { + addEventListener('scroll', onScroll, false) + onScroll() + connected = true + } + + }, + + /** + * Removed `scroll` event listener. + * + * @returns {void} + */ + stop: () => { + if (connected) { + removeEventListener('scroll', onScroll, false) + position = { x: 0, y: 0 } + connected = false + } + } + } + +})(false) diff --git a/src/observers/scrolling.js b/src/observers/scrolling.js deleted file mode 100644 index e949934..0000000 --- a/src/observers/scrolling.js +++ /dev/null @@ -1,128 +0,0 @@ -import { cache } from '../app/store' - -/* -------------------------------------------- */ -/* LETTINGS */ -/* -------------------------------------------- */ - -/** - * @type {boolean} - */ -let started = false - -/** - * @type {IPjax.IPosition} - */ -let position = { x: 0, y: 0 } - -/* -------------------------------------------- */ -/* FUNCTIONS */ -/* -------------------------------------------- */ - -/** - * Attached `scroll` event listener. - * - * @exports - * @returns {void} - */ -export function start () { - - if (!started) { - addEventListener('scroll', onScroll, false) - onScroll() - started = true - } - -} - -/** - * Removed `scroll` event listener. - * - * @exports - * @returns {void} - */ -export function stop () { - if (started) { - removeEventListener('scroll', onScroll, false) - position = { x: 0, y: 0 } - started = false - } -} - -/** - * Sets scroll position to the cache reference and - * returns a reset position. - * - * This function is called before a new page visit - * navigation begins, as it will assert the current - * position to the current page and return the reset - * position, ie: `{x: 0, y: 0 }`) to new page visit. - * - * If the passed in page state object position was modified - * via attributes, eg: `data-pjax-position="x:number y:number"` - * then position will be adjusted to match attribute config and - * additionally returned. - * - * - * @exports - * @param {IPjax.IState} state - * @returns {IPjax.IPosition} - */ -export function setPosition ({ - location: { lastUrl }, - position: { x, y } -}) { - - // We assert current position here - cache.get(lastUrl).position = getPosition() - - if ((x === 0 && y === 0)) return reset() - - position.x = x === 0 ? 0 : x - position.y = y === 0 ? 0 : x - - return position - -} - -/** - * Returns to current scroll position, the `reset()` - * function **MUST** be called after referencing this - * to reset position. - * - * @exports - * @returns {IPjax.IPosition} - */ -export function getPosition () { - - return position -} - -/** - * Resets the scroll position`of the document, applying - * a `x`and `y` positions to `0` - * - * @exports - * @returns {IPjax.IPosition} - */ -export function reset () { - - position.x = 0 - position.y = 0 - - return position - -} - -/** - * onScroll event, asserts the current X and Y page - * offset position of the document - * - * @exports - * @returns {void} - */ -export function onScroll () { - - position.x = window.pageXOffset - position.y = window.pageYOffset - -} diff --git a/src/polyfills/getAttributeNames.js b/src/polyfills/getAttributeNames.js new file mode 100644 index 0000000..030d601 --- /dev/null +++ b/src/polyfills/getAttributeNames.js @@ -0,0 +1,11 @@ +if (Element.prototype.getAttributeNames == undefined) { + Element.prototype.getAttributeNames = function () { + const attributes = this.attributes + const length = attributes.length + const result = new Array(length) + for (let i = 0; i < length; i++) { + result[i] = attributes[i].name + } + return result + } +} diff --git a/types/state.d.ts b/types/state.d.ts index 63dc5c4..47aaaa8 100644 --- a/types/state.d.ts +++ b/types/state.d.ts @@ -1,30 +1,349 @@ -import { IPosition, ILocation } from './store' +import { PartialPath } from 'history' -export interface Page { + +/** + * Scroll position records + */ +export type IPosition = { + x: number, + y: number +} + +/** + * Methods + */ +export enum IMethods { /** - * The URL cache key + * Initialized */ - url?: string + Initial = 1, /** - * UUID reference to the page snapshot HTML String + * Initialized */ - snapshot?: string + Click = 2, + + /** + * Prefetch + */ + Prefetch = 3, + + /** + * Cache + */ + Cache = 4, + + /** + * Pop + */ + Pop = 5, + + /** + * Capture + */ + Capture = 6 +} + +/** + * The URL location object + */ +export interface ILocation extends PartialPath { + /** + * The URL Pathname + * + * @example + * '/pathname' OR '/pathname/foo/bar' + */ + pathname?: string + + /** + * The URL search params + * + * @example + * '?param=foo&bar=baz' + */ + search?: string + + /** + * The URL Hash + * + * @example + * '#foo' + */ + hash?: string + + /** + * The previous path href URL. + * This is also the cache identifier + * + * @example + * '/pathname' OR '/pathname?foo=bar' + */ + lastpath?: string + +} + +/** + * NProgress Exposed Configuration Options + */ +export interface IProgress { + /** + * Changes the minimum percentage used upon starting. + * + * @default 0.08 + */ + minimum?: number + /** + * CSS Easing String + * + * @default cubic-bezier(0,1,0,1) + */ + easing?: string + /** + * Animation Speed + * + * @default 200 + */ + speed?: number + /** + * Turn off the automatic incrementing behavior + * by setting this to false. + * + * @default true + */ + trickle?: boolean + /** + * Adjust how often to trickle/increment, in ms. + * + * @default 200 + */ + trickleSpeed?: number + /** + * Turn on loading spinner by setting it to `true` + * + * @default false + */ + showSpinner?: boolean +} + + +export interface IPresets { + + /** + * Default fallback and preset fragments. By default, this pjax module will replace the + * entire `` fragment. Its best to define specific fragments here to prevent replacing + * static elements upon each navigation. + * + * --- + * @default ['body'] + */ + targets?: string[], + + /** + * Request Configuration + */ + request?: { + + /** + * The timeout limit of the XHR request issued. If timeout limit is exceeded a + * normal page visit will executed. + * + * --- + * @default 1500 + */ + timeout?: number, + + /** + * Throttle rate limits used when a request is already in transit. If you are leveraging + * prefetch capabilities then throttle limit will prevent requests already in-flight from + * occuring and instead wait until the initial request completes. + * + * --- + * @default 150 + */ + throttle?: number, + + /** + * FEATURE NOT YET AVAILABLE + * + * Define the request dispatch. By default, request are fetched upon mousedown, this allows + * fetching to start sooner that it would from an click event. + * + * > Currently, fetches are executed on `mousedown` only. Future releases will provide click + * dispatches + * + * --- + * @default 'mousedown' + */ + readonly dispatch?: 'mousedown' + }, + + /** + * Prefetch configuration + */ + prefetch?: { + + /** + * Mouseover prefetching preset configuration + */ + mouseover?: { + /** + * Enable or Disable mouseover (hover) prefetching. When enabled, this option + * will allow you to fetch pages over the wire upon mouseover and saves them to + * cache. When `mouseover` prefetches are disabled, all `data-pjax-prefetch="mouseover"` + * attribute configs will be ignored. + * + * > _If cache if disabled then prefetches will be dispatched using HTML5 + * `` prefetches, else when cache is enabled it uses XHR._ + * + * --- + * @default true + */ + enable?: boolean, + + /** + * Controls the mouseover fetch delay threshold. Requests will fire on mouseover + * only after the threshold time has been exceeded. This helps limit extrenous + * requests from firing. + * + * --- + * @default 250 + */ + threshold?: number + }, + + /** + * Intersection prefetching preset configuration + */ + intersect?: { + /** + * Enable or Disable intersection prefetching. Intersect prefetching leverages the + * Intersection Observer API to fire requests when elements become visible in viewport. + * When intersect prefetches are disabled, all `data-pjax-prefetch="intersect"` + * attribute configs will be ignored. + * + * > _If cache if disabled then prefetches will be dispatched using HTML5 + * `` prefetches, else when cache is enabled it uses XHR._ + * + * --- + * @default true + */ + enable?: boolean, + + /** + * Threshold limit passed to the intersection observer instance + * + * --- + * @default 0 + */ + threshold?: number + } + }, /** - * UUID reference to the captured snapshot HTML string + * Caching engine configuration */ - captured?: string + cache?: { + + /** + * Enable or Disable caching. Each page visit request is cached and used in + * subsequent visits to the same location. By disabling cache, all visits will + * be fetched over the network and any `data-pjax-cache` attribute configs + * will be ignored. + * + * --- + * @default true + */ + enable?: boolean + + /** + * Cache size limit. This pjax variation limits cache size to `25mb`and once size + * exceeds that limit, records will be removed starting from the earliest point + * cache entry. + * + * _Generally speaking, leave this the fuck alone._ + * + * --- + * @default 50 + */ + limit?: number, + + /** + * FEATURE NOT YET AVAILABLE + * + * The save option will save snapshot cache to IndexedDB. + * This feature is not yet available. + * + * --- + * @default false + */ + readonly save?: boolean + }, /** - * The fetched HTML response string + * Progress Bar configuration */ - targets?: { - [selector: string]: Element[] + progress?: { + + /** + * Enable or Disables the progress bar globally. Setting this option + * to `false` will prevent progress from displaying. When disabled, + * all `data-pjax-progress` attribute configs will be ignored. + * + * --- + * @default true + */ + enable?: boolean, + + /** + * Controls the progress bar preset threshold. Defines the amount of + * time to delay before the progress bar is shown. + * + * --- + * @default 350 + */ + threshold?: number, + + /** + * [N Progress](https://github.com/rstacruz/nprogress) provides the + * progress bar feature which is displayed between page visits. + * + * > _This pjax module does not expose all configuration options of nprogress, + * but does allow control of some internals. Any configuration options + * defined here will be passed to the nprogress instance upon initialization._ + */ + options?: IProgress } +} + +/** + * Page Visit State + * + * Configuration from each page visit. For every page navigation + * the configuration object is generated in a immutable manner. + */ +export interface IPage { + + /** + * The URL cache key and current url path + */ + url?: string + + /** + * UUID reference to the page snapshot HTML Document element + */ + snapshot?: string + + /** + * UUID to a captured snapshot HTML string. Captures are temporary + * snapshots used to preserve the document when navigating between history + * popstate. Captured snapshots are removed upon subsequent visits to location. + */ + captured?: boolean | string + /** * Location URL */ @@ -36,34 +355,38 @@ export interface Page { title?: string /** - * Action + * List of target element selectors. Accepts any valid + * `querySelector()` string. + * + * @example + * ['#main', '.header', '[data-attr]', 'header'] */ - action?: { - replace?: [target:string] - append?: Array<[from: string, to: string]>, - prepend?: Array<[from: string, to: string]>, - } + targets?: string[] /** - * List of target element selectors. Accepts any valid + * List of fragment element selectors. Accepts any valid * `querySelector()` string. * * @example * ['#main', '.header', '[data-attr]', 'header'] */ - target?: string[] + replace?: boolean | string[] /** - * Default method to be applied. - * --- - * `replace` - Navigation target will be replaced + * List of fragments to be appened from and to. Accepts multiple. * - * `append` - Navigation target will be appened - * - * `prepend` - Navigation target will be prepended + * @example + * [['#main', '.header'], ['[data-attr]', 'header']] + */ + append?:boolean | Array<[from: string, to: string]>, + + /** + * List of fragments to be prepend from and to. Accepts multiple. * + * @example + * [['#main', '.header'], ['[data-attr]', 'header']] */ - method?: string + prepend?: boolean | Array<[from: string, to: string]> /** * Controls the caching engine for the link navigation. @@ -81,20 +404,12 @@ export interface Page { * Passing in __reset__ the cache record will be removed, * a new pjax visit will be executed and the result saved to cache. * - * `save` - * - * Passing in __save__ will temporarily store the current - * cached state to session storage. It will be removed on your - * next navigation visit. + * `flush` * - * > _The save option should be avoided unless you are executing a - * full page reload and wish to store your cached pages to prevent - * new requests being executed on next navigation. If your cache exceeds - * 3mb in size cache records will be removed starting from the earliest - * point on of entry. Use `save` in conjunction with the `data-pjax-disable` - * option, else do your upmost to avoid it._ + * Passing in __flush__ will completely flush cache, removing all + * saved records. */ - cache?: boolean | 'false' | 'reset' | 'save' + cache?: boolean | string /** * Scroll position of the next navigation. @@ -107,88 +422,36 @@ export interface Page { position?: IPosition /** - * Prefetch option to execute for each link + * Defines the * - * --- - * `intersect` - * - * Pages will be fetched upon `IntersectionObserve()` threshold. - * ie: when they become visible in viewport. - * - * `hover` - * - * Pages will be fetched upon `mouseover` on a pjax href link. - * Try and avoid this, just use __intersect__ instead. - * - * > _On mobile devices the hover value will execute on a - * touch event._ + * @default 250 */ - prefetch?: boolean | 'intersect' | 'mouseover' | 'hover' + intersect?: number, /** - * List array of tracked elements pretaining to this link page - * navigation visit (if any). + * Define mouseover timeout from which fetching will begin + * after time spent on mouseover * - * @see https://github.com/panoply/pjax#data-pjax-track + * @default 100 */ - track?: Element[] + threshold?: number, /** - * Throttle delay between navigations, set this option if - * you want to delay the time between visits, helpful if - * navigation is too fast. + * Define proximity prefetch distance from which fetching will + * begin relative to the cursor offset of href elements. * - * @default 0 + * @default 50 */ - throttle?: number + proximity?: number, /** - * Enable or Disable progres bar indicator - * - * (_Requests are instantaneous, generally you wont need this_) + * Progress bar threshold delay * - * @default true - */ - progress?: boolean, - - - /** - * Threshold Controls + * @default 350 */ - threshold?: { - - /** - * Define an intersection threshold timeout from - * which intersected elements will begin fetching - * after being observed - * - * @default 250 - */ - intersect?: number, - - /** - * Define mouseover timeout from which fetching will begin - * after time spent on mouseover - * - * @default 100 - */ - mouseover?: number, - - /** - * Define hover timeout from which fetching will begin - * after time spent on mouseover - * - * @default 100 - * - * @deprecated - * Use `mouseover` instead - */ - hover?: number, - - - } + progress?: boolean | number, } -export as namespace IPjaxState; +export as namespace Store; diff --git a/types/store.d.ts b/types/store.d.ts index 2b9e233..c4c4949 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -1,3 +1,5 @@ +import { NProgressOptions } from 'nprogress' + /** * Pjax Events */ @@ -42,6 +44,46 @@ export type ICacheSize = { weight: string } +export interface IProgress { + /** + * Changes the minimum percentage used upon starting. + * + * @default 0.08 + */ + minimum?: number + /** + * CSS Easing String + * + * @default cubic-bezier(0,1,0,1) + */ + easing?: string + /** + * Animation Speed + * + * @default 200 + */ + speed?: number + /** + * Turn off the automatic incrementing behavior + * by setting this to false. + * + * @default true + */ + trickle?: boolean + /** + * Adjust how often to trickle/increment, in ms. + * + * @default 200 + */ + trickleSpeed?: number + /** + * Turn on loading spinner by setting it to `true` + * + * @default false + */ + showSpinner?: boolean +} + /** * The URL location object */ @@ -132,13 +174,13 @@ export type IConfigPresets = { */ cache?: boolean, /** - * Enable or Disable progres bar indicator - * - * (_Requests are instantaneous, generally you wont need this_) - * - * @default false + * [NProgress](https://github.com/rstacruz/nprogress) provides the + * progress bar feature which is displayed between page visits. This pjax + * module does not expose all configuration options of nprogress, but does allow + * control of some internals. Any configuration options defined here will be + * passed to nprogress. */ - progress?: boolean, + progress?: IProgress, /** * Throttle delay between navigations, set this option if * you want to delay the time between visits, helpful if @@ -192,7 +234,7 @@ export interface IConfig { * @example * ['#main', '.header', '[data-attr]', 'header'] */ - target?: string[] + targets?: string[] /** * Default method to be applied. @@ -247,24 +289,6 @@ export interface IConfig { */ position?: IPosition - /** - * Prefetch option to execute for each link - * - * --- - * `intersect` - * - * Pages will be fetched upon `IntersectionObserve()` threshold. - * ie: when they become visible in viewport. - * - * `hover` - * - * Pages will be fetched upon `mouseover` on a pjax href link. - * Try and avoid this, just use __intersect__ instead. - * - * > _On mobile devices the hover value will execute on a - * touch event._ - */ - prefetch?: string /** * List array of tracked elements pretaining to this link page @@ -283,16 +307,6 @@ export interface IConfig { */ throttle?: number - /** - * Enable or Disable progres bar indicator - * - * (_Requests are instantaneous, generally you wont need this_) - * - * @default true - */ - progress?: boolean, - - /** * Threshold Controls */ From 1826231e175e7b91a9587f1acb96598f9acae233 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:15:03 +0100 Subject: [PATCH 19/60] add pointer / mouseover helpers --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 0c25268..4773cf9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "@babel/runtime": "^7.13.10", "@types/nprogress": "^0.2.0", "custom-event-polyfill": "^1.0.7", + "detect-it": "^4.0.1", "element-closest-polyfill": "^1.0.2", + "event-from": "^0.6.0", "history": "^5.0.0", "intersection-observer": "^0.12.0", "mdn-polyfills": "^5.20.0", From 0ff9988103b79e2e5c3000b51f9732b814bf135f Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:15:22 +0100 Subject: [PATCH 20/60] iife controller export --- src/app/controller.js | 117 ++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/src/app/controller.js b/src/app/controller.js index 39b5ce3..1dc25e2 100644 --- a/src/app/controller.js +++ b/src/app/controller.js @@ -3,55 +3,86 @@ import hover from '../observers/hover' import intersect from '../observers/intersect' import scroll from '../observers/scroll' import history from '../observers/history' -import { store } from './store' +import _history from 'history/browser' +import path from './path' +import store from './store' -let started = false +export default (function (connected) { -/** - * Initialize - * - * @exports - * @returns {void} - */ -export function initialize () { + /** + * Sets initial page state executing on intial load. + * Caches page so a return navigation does not perform + * an extrenous request. + * + * @returns {void} + */ + const onload = () => { - if (!started) { + const page = store.create({ + url: path.url, + location: path.parse(path.url), + position: scroll.position + }, document.documentElement.outerHTML) - history.start() - hrefs.start() - scroll.start() - hover.start() - intersect.stop() + _history.replace(history.location, page) - addEventListener('load', store.initialize) - started = true + removeEventListener('load', onload) - console.info('Pjax: Connection Established ⚡') } -} - -/** - * Destory Pjax instances - * - * @exports - * @returns {void} - */ -export function destroy () { - - if (started) { - - history.stop() - hrefs.stop() - scroll.stop() - hover.stop() - intersect.stop() - store.clear() - - started = false - - console.warn('Pjax: Instance has been disconnected! 😔') - } else { - console.warn('Pjax: No connection made, disconnection is void 🙃') + + /** + * Initialize + * + * @exports + * @returns {void} + */ + const initialize = () => { + + if (!connected) { + + history.start() + hrefs.start() + scroll.start() + hover.start() + intersect.stop() + + addEventListener('load', onload) + + connected = true + + console.info('Pjax: Connection Established ⚡') + } + } + + /** + * Destory Pjax instances + * + * @exports + * @returns {void} + */ + const destroy = () => { + + if (connected) { + + history.stop() + hrefs.stop() + scroll.stop() + hover.stop() + intersect.stop() + store.clear() + + connected = false + + console.warn('Pjax: Instance has been disconnected! 😔') + } else { + console.warn('Pjax: No connection made, disconnection is void 🙃') + } + + } + + return { + initialize, + destroy } -} +}(false)) From 8d438ad4e8d9d49a7a0ec1b5e0b028c4bf721b1f Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:15:46 +0100 Subject: [PATCH 21/60] implement private pattern --- src/app/path.js | 175 +++++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 83 deletions(-) diff --git a/src/app/path.js b/src/app/path.js index 14d095d..4470744 100644 --- a/src/app/path.js +++ b/src/app/path.js @@ -12,8 +12,98 @@ export default (function ({ location, location: { origin, hostname } }) { let next = createPath(location) let path = next + /** + * Returns the pathname cache key URL + * + * @param {string} link + * @returns {string} + */ + const key = link => { + + return link.charCodeAt(0) === 47 // 47 is unicode value for '/' + ? link + : (link.match(regexp.Pathname) || [])[1] || '/' + } + + /** + * Returns the absolute URL + * + * @param {string} link + * @returns {string} + */ + const absolute = link => { + + const location = document.createElement('a') + location.href = link.toString() + + return location.href + + } + + /** + * Parses link and returns an ILocation. + * Accepts either a `href` target or `string`. + * If no parameter value is passed, the + * current location pathname (string) is used. + * + * + * @export + * @param {Element|string} link + * @returns {Store.ILocation} + */ + const parse = (link) => { + + const location = parsePath( + link instanceof Element + ? link.getAttribute('href') + : link + ) + + return { + lastpath: createPath(history.location) + , search: '' + , origin + , hostname + , ...location + } + } + + /** + * Parses link and returns a location. + * + * **IMPORTANT** + * + * This function will modify the next url value + * + * @export + * @param {Element|string} link + * @param {{ update?: boolean, parse?: boolean }} options + * @returns {({url: string, location: Store.ILocation})} + */ + const get = (link, options = { update: false }) => { + + const url = key(link instanceof Element ? link.getAttribute('href') : link) + + if (options.update) { + path = createPath(history.location) + next = url + } + + return { url, location: parse(url) } + + } + return { + /* EXPORTS ------------------------------------ */ + + get + , key + , parse + , absolute + + /* GETTERS ------------------------------------ */ + /** * Returns the last parsed url value. * Prev URL is the current URL. Calling this will @@ -26,11 +116,7 @@ export default (function ({ location, location: { origin, hostname } }) { * * @returns {string} */ - get url () { - - return path - - }, + , get url () { return path }, /** * Returns the next parsed url value. @@ -46,84 +132,7 @@ export default (function ({ location, location: { origin, hostname } }) { * * @returns {string} */ - get next () { - - return next - - }, - - /** - * Parses link and returns a location. - * - * **IMPORTANT** - * - * This function will modify the next url value - * - * @export - * @param {Element|string} link - * @param {boolean} [isNext=true] - * @returns {string} - */ - get: (link, isNext = false) => { - - const href = link instanceof Element ? link.getAttribute('href') : link - - // 47 is unicode value for '/' - const url = href.charCodeAt(0) === 47 - ? href - : (href.match(regexp.Pathname) || [])[1] || '/' - - if (isNext) { - path = createPath(history.location) - next = url - } - - return url - - }, - - /** - * Returns the absolute URL - * - * @param {string} link - * @returns {string} - */ - absolute: link => { - - const location = document.createElement('a') - location.href = link.toString() - - return location.href - - }, - - /** - * Parses link and returns an ILocation. - * Accepts either a `href` target or `string`. - * If no parameter value is passed, the - * current location pathname (string) is used. - * - * - * @export - * @param {Element|string} link - * @returns {Store.ILocation} - */ - parse (link) { - - const location = parsePath( - link instanceof Element - ? link.getAttribute('href') - : link - ) - - return { - lastpath: createPath(history.location) - , search: '' - , origin - , hostname - , ...location - } - } + get next () { return next } } From 7b62511673c1e1be65b6b0acf2165bba760dd382 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:16:00 +0100 Subject: [PATCH 22/60] make store default export --- src/app/prefetch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/prefetch.js b/src/app/prefetch.js index 2cf8677..f84d883 100644 --- a/src/app/prefetch.js +++ b/src/app/prefetch.js @@ -1,4 +1,4 @@ -import { store } from './store' +import store from './store' import mouseover from '../observers/hover' import intersect from '../observers/intersect' From 6549a4955385645d32486f6c6f46888c667f0454 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:16:15 +0100 Subject: [PATCH 23/60] arrow functions --- src/app/progress.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/progress.js b/src/app/progress.js index 25acd2b..e07ff5d 100644 --- a/src/app/progress.js +++ b/src/app/progress.js @@ -17,9 +17,9 @@ export let progress = null * Setup nprogress * * @export - * @param {IPjax.IProgress} options + * @param {Store.IProgress} options */ -export function config (options) { +export const config = options => { progress = nprogress.configure(options) From c6598e946cec1c4eed6d5f78c20ac677f5dbae3b Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:16:55 +0100 Subject: [PATCH 24/60] fix replace, capture and update store controls --- src/app/render.js | 71 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/app/render.js b/src/app/render.js index 41d8f26..684710b 100644 --- a/src/app/render.js +++ b/src/app/render.js @@ -1,10 +1,11 @@ import { eachSelector, dispatchEvent, forEach } from './utils' -import { store, snapshots } from './store' import { progress } from './progress' import history from 'history/browser' import { createPath } from 'history' import { nanoid } from 'nanoid' import * as prefetch from './prefetch' +import scroll from '../observers/scroll' +import store from './store' /** * Renderer @@ -104,18 +105,16 @@ export default (function () { dispatchEvent('pjax:render', { target }, true) - if (!state?.replace && !state?.prepend && !state?.append) { + DOM.innerHTML = target.innerHTML - DOM.innerHTML = target.innerHTML - - } else { + if (state?.append || state?.prepend) { const fragment = document.createElement('div') const nodes = [].slice.call(target.childNodes) forEach(nodes, node => fragment.appendChild(node)) - state.replace ? DOM.replaceWith(fragment) : state.append + state.append ? DOM.appendChild(fragment) : DOM.insertBefore(fragment, DOM.firstChild) @@ -130,9 +129,9 @@ export default (function () { * @param {Document} target * @returns {string} */ - const DOMSnapshot = (target) => { + const DOMSnapshot = target => { const uuid = nanoid(16) - snapshots.set(uuid, target.documentElement.outerHTML) + store.set.snapshots(uuid, target.documentElement.outerHTML) return uuid } @@ -141,31 +140,30 @@ export default (function () { * * @param {string} url * @param {{ action: 'replace' | 'capture'}} options + * @returns {string} */ - const captureDOM = (url, options) => { + const captureDOM = (url, options = { action: 'capture' }) => { - if (store.config.cache && store.has(url, { snapshot: true })) { + if (!store.has(url, { snapshot: true })) return undefined - const state = store.cache(url) - const DOMString = store.snapshot(state.snapshot) - const target = DOMParse(DOMString) + const { snapshot, page } = store.get(url) + const target = DOMParse(snapshot) + target.body.innerHTML = document.documentElement.querySelector('body').innerHTML - target.body.innerHTML = document.documentElement.querySelector('body').innerHTML + if (options.action === 'capture') { - if (options.action === 'replace') { - snapshots.set(state.snapshot, target.documentElement.outerHTML) - } else if (options.action === 'capture') { - state.captured = DOMSnapshot(target) - - console.log(store.cache(url)) - history.replace(state.location, store.update(state)) - console.info('Pjax: DOM Captured at: ' + state.captured) - } + page.captured = DOMSnapshot(target) + page.position = scroll.position - return target.documentElement.outerHTML + history.replace(page.location, store.update(page)) + console.info('Pjax: DOM Captured at: ' + page.captured) + } else if (options.action === 'replace') { + store.snapshots.set(page.snapshot, target.documentElement.outerHTML) } + return target.documentElement.outerHTML + } /** @@ -174,6 +172,7 @@ export default (function () { * * @param {Store.IPage} state * @param {boolean} [popstate=false] + * @returns {Store.IPage} */ const update = (state, popstate = false) => { @@ -181,17 +180,12 @@ export default (function () { prefetch.stop() - console.log(state) - - const uuid = (popstate && typeof state.captured === 'string') - ? state.captured - : state.snapshot - + const uuid = (popstate && state.captured) ? state.captured : state.snapshot const target = DOMParse(store.snapshot(uuid)) state.title = document.title = target?.title || '' - if (!popstate) { + if (!popstate && state.history) { if (createPath(history.location) === state.url) { history.replace(state.location, state) @@ -201,8 +195,8 @@ export default (function () { } else if (state?.captured) { - if (snapshots.delete(uuid)) { - state.captured = false + if (store.delete.snapshots(uuid)) { + state.captured = null // history.replace(state.location, store.update(state)) console.info('Pjax: Captured snapshot removed at: ' + state.url) } @@ -213,7 +207,15 @@ export default (function () { let fallback = 1 - forEach(state.targets, element => { + console.log(state.replace ? [ + ...state.targets, + ...state.replace + ] : state.targets) + + forEach(state.replace ? [ + ...state.targets, + ...state.replace + ] : state.targets, element => { const node = target.body.querySelector(element) @@ -237,6 +239,7 @@ export default (function () { progress.done() prefetch.start() + return state // console.log(window.performance.measure('Render Time', 'render')) // console.log(window.performance.measure('Total', 'started')) From 0b1c980f4d07f2918a40ba9dc5e47c6dfe243e4e Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:17:18 +0100 Subject: [PATCH 25/60] add presets, timeout and store default export --- src/app/request.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/app/request.js b/src/app/request.js index 76fe43c..ef01afa 100644 --- a/src/app/request.js +++ b/src/app/request.js @@ -1,4 +1,4 @@ -import { store, snapshots } from './store' +import store from './store' import { asyncTimeout, byteConvert, byteSize, dispatchEvent } from './utils' import { progress } from './progress' @@ -48,10 +48,9 @@ export default (function () { * * @exports * @param {string} url - * @param {boolean} [async=true] * @returns {Promise} */ - const HttpRequest = async (url, async = true) => { + const HttpRequest = async (url) => { const xhr = new XMLHttpRequest() @@ -59,7 +58,7 @@ export default (function () { // OPEN // - xhr.open('GET', url, async) + xhr.open('GET', url, store.config.request.async) // HEADERS // @@ -72,6 +71,7 @@ export default (function () { xhr.onload = e => xhr.status === 200 ? resolve(xhr.responseText) : null xhr.onloadend = e => HttpRequestEnd(url, xhr.responseText) xhr.onerror = reject + xhr.timeout = store.config.request.timeout // SEND // @@ -117,7 +117,7 @@ export default (function () { */ const inFlight = async (url) => { - if (transit.has(url) && ratelimit <= store.config.request.throttle) { + if (transit.has(url) && ratelimit <= store.config.request.poll) { // console.log('Request in flight', ratelimit * 25) if ((ratelimit * 10) >= store.config.progress.threshold) progress.start() @@ -159,11 +159,9 @@ export default (function () { const response = await HttpRequest(state.url) - if (typeof response === 'string') { - return store.create(state, response) - } + if (typeof response === 'string') return store.create(state, response) - console.warn(`Pjax: Failed to receive response at: ${state.url}`) + console.warn(`Pjax: Failed to retrive response at: ${state.url}`) } catch (error) { @@ -186,12 +184,11 @@ export default (function () { * Returns request cache metrics * * @exports - * @returns {IPjax.ICacheSize} + * @returns {Store.ICacheSize} */ get cacheSize () { return { - requests: snapshots.size, total: storage, weight: byteConvert(storage) } From 039acd61a6470c51f55ec3321beb7b171f96aa74 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:17:31 +0100 Subject: [PATCH 26/60] various changes and updates. --- src/app/store.js | 361 +++++++++++++++++++++++++---------------------- 1 file changed, 193 insertions(+), 168 deletions(-) diff --git a/src/app/store.js b/src/app/store.js index 909bf76..909ed40 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -1,39 +1,37 @@ import merge from 'mergerino' import history from 'history/browser' -import { dispatchEvent } from './utils' import { nanoid } from 'nanoid' -import path from './path' +import { dispatchEvent } from './utils' import scroll from '../observers/scroll' import * as nprogress from './progress' -/** - * Snapshots Cache - * - * @exports - */ -export const snapshots = new Map() - /** * store */ -export const store = (() => { +export default ((config) => { /** - * Preset Configuration + * Cache * - * @type {object} + * @exports + * @type {Map} */ - let presets + const cache = new Map() /** * Cache * * @exports - * @type {Map} + * @type {Map} */ - const cache = new Map() + const snapshots = new Map() - /* CLOSURE IIFE ------------------------------- */ + /** + * Preset Configuration + * + * @type {object} + */ + let presets return ({ @@ -47,92 +45,17 @@ export const store = (() => { */ connect (options = {}) { - presets = merge( - { - targets: [ 'main', '#navbar' ], - request: { - timeout: 1500, - throttle: 150, - dispatch: 'mousedown' - }, - prefetch: { - mouseover: { - enable: true, - threshold: 100 - }, - intersect: { - enable: true, - threshold: 250 - } - }, - cache: { - enable: true, - limit: 25, - save: false - }, - progress: { - enable: true, - threshold: 350, - options: { - minimum: 0.10, - easing: 'ease', - speed: 225, - trickle: true, - trickleSpeed: 225, - showSpinner: false - } - } - } - , - { - - // PRESETS PATCH COPY - - ...options - - , request: { - - ...options?.request - - // PREVENT DISPATCH PROPERTY FROM PATCH - - , dispatch: undefined - } - , cache: { - - ...options?.cache - - // PREVENT SAVE PROPERTY FROM PATCH - - , save: undefined - } - } - ) + presets = merge(config, { + // PRESETS PATCH COPY + ...options + , request: { ...options?.request, dispatch: undefined } + , cache: { ...options?.cache, save: undefined } + }) nprogress.config(this.config.progress.options) }, - /** - * Sets initial page state executing on intial load. - * Caches page so a return navigation does not perform - * an extrenous request. - * - * @param {Event} event - * @returns {void} - */ - initialize (event) { - - history.replace(history.location, store.create({ - url: path.url, - location: path.parse(path.url), - position: scroll.position - }, document.documentElement.outerHTML)) - - removeEventListener('load', this.initialize) - - }, - /** * Returns the Pjax preset configuration object. Presets are global * configurations. This getter will give us access to the defined @@ -141,17 +64,83 @@ export const store = (() => { * @type {Store.IPresets} * @return {Store.IPresets} */ - get config () { + get config () { return presets }, - return presets + /** + * Indicates a new page visit or a return page visit. New visits + * are defined by an event dispatched from a `href` link. Both a new + * new page visit or subsequent visit will call this function. + * + * **Breakdown** + * + * Subsequent visits calling this function will have their per-page + * specifics configs (generally those configs set with attributes) + * reset and merged into its existing records (if it has any), otherwise + * a new page instance will be generated including defult preset configs. + * + * @param {Store.IPage} state + * @param {string} snapshot + * @returns {Store.IPage} + */ + create (state, snapshot) { - }, + const page = { + + // EDITABLE + // + history: true, + capture: false, + append: null, + prepend: null, + replace: null, + captured: null, + snapshot: state?.snapshot || nanoid(16), + position: state?.position || scroll.reset(), + cache: this.config.cache.enable, + progress: this.config.progress.threshold, + threshold: this.config.prefetch.mouseover.threshold, - clear: () => { + // USER OPTIONS + // + ...state, - cache.clear() - snapshots.clear() + // READ ONLY + // + targets: this.config.targets + + } + + if (page.cache && dispatchEvent('pjax:cache', page, true)) { + cache.set(page.url, page) + snapshots.set(page.snapshot, snapshot) + } + + return page + + }, + + /** + * Removes cached records. Optionally pass in URL + * to remove specific record. + * + * @param {string} id + */ + snapshot: id => snapshots.get(id), + /** + * Removes cached records. Optionally pass in URL + * to remove specific record. + * + * @param {string} [url] + */ + clear: url => { + if (typeof url === 'string') { + snapshots.delete(cache.get(url).snapshot) + cache.delete(url) + } else { + snapshots.clear() + cache.clear() + } }, /** @@ -159,41 +148,83 @@ export const store = (() => { * * @param {string} url * @param {{snapshot?: boolean}} has - * @return {boolean} */ - has: (url, has = { snapshot: false }) => { + get: url => ({ + + /** + * @returns {Store.IPage} + */ + get page () { + return cache.get(url) + }, + + /** + * @returns {string} + */ + get snapshot () { + return snapshots.get(this.page.snapshot) + } - return !has.snapshot ? cache.has(url) : ( - cache.has(url) || snapshots.has(cache.get(url)?.snapshot) - ) + }), + + /** + * Map setters + */ + get set () { + + return ({ + + /** + * @param {string} key + * @param {Store.IPage} value + */ + cache: (key, value) => cache.set(key, value), + + /** + * @param {string} key + * @param {string} value + */ + snapshots: (key, value) => snapshots.set(key, value) + + }) }, /** - * Saves a HTML Document String as snapshot - * - * @param {string} id - * @returns {string} + * Map Deletions */ - snapshot: (id) => { + get delete () { + + return ({ + + /** + * @param {string} url + */ + cache: url => cache.delete(url), - return snapshots.get(id) + /** + * @param {string} id + */ + snapshots: id => snapshots.delete(id) + + }) }, /** - * Inserts a page into cache. This function is called upon - * a new page instance being generated via `new()`. Optionally - * pass in an `object`to assert into new state. + * Check if cache record exists with snapshot * * @param {string} url - * @returns {Store.IPage} + * @param {{snapshot?: boolean}} has + * @return {boolean} */ - cache (url) { + has: (url, has = { snapshot: false }) => ( - return cache.get(url) + !has.snapshot ? cache.has(url) : ( + cache.has(url) || snapshots.has(cache.get(url)?.snapshot) + ) - }, + ), /** * Update current pushState History @@ -201,14 +232,14 @@ export const store = (() => { * @param {string} url * @returns {void} */ - history (url) { + history: () => ( history.replace(history.location, { ...history.location.state, position: scroll.position }) - }, + ), /** * Updates page state, this function will run a merge @@ -225,55 +256,49 @@ export const store = (() => { * @param {Store.IPage} state * @returns {Store.IPage} */ - update: (state) => { + update: state => ( - const page = cache + cache .set(state.url, merge(cache.get(state.url), state)) .get(state.url) - return page - - }, - - /** - * Indicates a new page visit or a return page visit. New visits - * are defined by an event dispatched from a `href` link. Both a new - * new page visit or subsequent visit will call this function. - * - * **Breakdown** - * - * Subsequent visits calling this function will have their per-page - * specifics configs (generally those configs set with attributes) - * reset and merged into its existing records (if it has any), otherwise - * a new page instance will be generated including defult preset configs. - * - * @param {Store.IPage} state - * @param {string} snapshot - * @returns {Store.IPage} - */ - create (state, snapshot) { - - const page = { - captured: false, - append: false, - prepend: false, - snapshot: state?.snapshot || nanoid(16), - position: state?.position || scroll.reset(), - targets: this.config.targets, - cache: this.config.cache.enable, - progress: this.config.progress.threshold, - threshold: this.config.prefetch.mouseover.threshold, - ...state - } - - snapshots.set(page.snapshot, snapshot) - - if (dispatchEvent('pjax:cache', page, true)) cache.set(page.url, page) - - return page - - } + ) }) -})() +})({ + targets: [ 'body' ], + request: { + timeout: 3000, + poll: 150, + async: true, + dispatch: 'mousedown' + }, + prefetch: { + mouseover: { + enable: true, + threshold: 100, + proximity: 0 + }, + intersect: { + enable: true + } + }, + cache: { + enable: true, + limit: 25, + save: false + }, + progress: { + enable: true, + threshold: 500, + options: { + minimum: 0.10, + easing: 'ease', + speed: 225, + trickle: true, + trickleSpeed: 225, + showSpinner: false + } + } +}) From 9e2682936f7a6c2a8840d0b19a853f734f7c9e83 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:17:38 +0100 Subject: [PATCH 27/60] mods --- src/app/utils.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/utils.js b/src/app/utils.js index 96b908d..dd78179 100644 --- a/src/app/utils.js +++ b/src/app/utils.js @@ -1,7 +1,7 @@ import { isNumber } from '../constants/regexp' import { Units } from './../constants/common' import path from './path' -import { store } from './store' +import store from './store' /** * Locted the closest link when click bubbles. @@ -78,7 +78,24 @@ export function chunk (size = 2) { * Dispatches lifecycle events on the document. * * @exports - * @param {IPjax.IEvents} eventName + * @param {Store.IEvents} eventName + * @param {Element} target + * @return {boolean} + */ +export function targetedEvent (eventName, target) { + + // create and dispatch the event + const newEvent = new CustomEvent(eventName, { cancelable: true }) + + return target.dispatchEvent(newEvent) + +} + +/** + * Dispatches lifecycle events on the document. + * + * @exports + * @param {Store.IEvents} eventName * @param {object} detail * @param {boolean} cancelable * @return {boolean} @@ -113,7 +130,7 @@ export function byteSize (string) { */ export function canFetch (target) { - return !store.has(path.get(target), { snapshot: true }) + return !store.has(path.get(target).url, { snapshot: true }) } /** From a27aecc72d55d5243e2aff436e56c1a6f8ec0125 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:17:57 +0100 Subject: [PATCH 28/60] add capture to expression Attr --- src/constants/regexp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/regexp.js b/src/constants/regexp.js index c36d42d..88131b8 100644 --- a/src/constants/regexp.js +++ b/src/constants/regexp.js @@ -6,7 +6,7 @@ * @exports * @type {RegExp} */ -export const Attr = /^data-pjax-(append|prepend|replace|prefetch|progress|threshold|position)$/i +export const Attr = /^data-pjax-(append|prepend|replace|history|capture|progress|threshold|position)$/i /** * URL Pathname From aa6263cbf5a6ae75d9b532173590af06ebd40a0c Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:18:09 +0100 Subject: [PATCH 29/60] store default export update --- src/observers/history.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/observers/history.js b/src/observers/history.js index c074984..6929c29 100644 --- a/src/observers/history.js +++ b/src/observers/history.js @@ -1,8 +1,8 @@ import history from 'history/browser' -import { store } from '../app/store' import { createPath } from 'history' import render from '../app/render' import request from '../app/request' +import store from '../app/store' /* -------------------------------------------- */ /* FUNCTIONS */ @@ -30,7 +30,7 @@ export default (function (connected) { * * @param {string} url * @param {Store.IPage} state - * @returns {Promise} + * @returns {Promise} */ const popstate = async (url, state) => { @@ -39,7 +39,7 @@ export default (function (connected) { if (url !== inTransit) request.cancel(inTransit) if (store.has(url, { snapshot: true })) { - return render.update(store.cache(url), true) + return render.update(store.get(url).page, true) } inTransit = url @@ -70,6 +70,8 @@ export default (function (connected) { return { + /* CONTROLS ----------------------------------- */ + /** * Attached `history` event listener. * From 6c3e6f60260df172ae9785fb1b8877e9719850b2 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:18:23 +0100 Subject: [PATCH 30/60] update pointer event dispatches --- src/observers/hover.js | 69 +++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/src/observers/hover.js b/src/observers/hover.js index 66e77db..db494a3 100644 --- a/src/observers/hover.js +++ b/src/observers/hover.js @@ -1,10 +1,11 @@ +import { supportsPointerEvents } from 'detect-it' +import { eventFrom } from 'event-from' import { LinkPrefetchHover } from '../constants/common' -import path from '../app/path' -import { store } from '../app/store' -import { getLink, getTargets } from '../app/utils' +import { getLink, getTargets, dispatchEvent } from '../app/utils' import hrefs from './hrefs' import request from '../app/request' - +import path from '../app/path' +import store from '../app/store' /** * Link (href) handler * @@ -20,7 +21,7 @@ export default (function (connected) { const transit = new Map() /** - * @type {IPjax.IPosition} + * @type {Store.IPosition} */ const position = { x: 0, y: 0 } @@ -44,13 +45,14 @@ export default (function (connected) { * @exports * @param {MouseEvent} event */ - function onMouseleave (event) { + const onMouseleave = (event) => { const target = getLink(event.target, LinkPrefetchHover) if (target) { - cleanup(path.get(target)) - target.removeEventListener('mouseleave', onMouseleave, true) + + cleanup(path.get(target).url) + handleLeave(target) } } @@ -98,22 +100,28 @@ export default (function (connected) { if (!target) return undefined - const url = path.get(target) + const { url, location } = path.get(target) + + if (!dispatchEvent('pjax:prefetch', { + target, + url, + location + }, true)) return disconnect(target) if (store.has(url, { snapshot: true })) return disconnect(target) - target.addEventListener('mouseleave', onMouseleave, true) + handleLeave(target) const state = hrefs.attrparse(target, { url, - location: path.parse(url), + location, position: { x: 0, y: 0 } }) throttle(url, async () => { - if ((await prefetch(state))) { - target.removeEventListener('mouseover', onMouseover, false) - } + + if ((await prefetch(state))) handleLeave(target) + }, state?.threshold || store.config.prefetch.mouseover.threshold) } @@ -172,8 +180,28 @@ export default (function (connected) { // if (target instanceof Element) proximity(target, index) - target.addEventListener('mouseover', onMouseover, false) + if (supportsPointerEvents) { + target.addEventListener('pointerover', onMouseover, false) + } else { + target.addEventListener('mouseover', onMouseover, false) + } + + } + + /** + * Cancels prefetch, if mouse leaves target before threshold + * concludes. This prevents fetches being made for hovers that + * do not exceeds threshold. + * + * @param {Element} target + */ + function handleLeave (target) { + if (supportsPointerEvents) { + target.removeEventListener('pointerout', onMouseleave, false) + } else { + target.removeEventListener('mouseleave', onMouseleave, false) + } } /** @@ -183,8 +211,15 @@ export default (function (connected) { * @returns {void} */ function disconnect (target) { - target.removeEventListener('mouseleave', onMouseleave, false) - target.removeEventListener('mouseover', onMouseover, false) + + if (supportsPointerEvents) { + target.removeEventListener('pointerover', onMouseleave, false) + target.removeEventListener('pointerout', onMouseleave, false) + } else { + target.removeEventListener('mouseleave', onMouseleave, false) + target.removeEventListener('mouseover', onMouseover, false) + } + } return { From 88430709eaa2c838985860ac33684b27c0a35448 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:18:45 +0100 Subject: [PATCH 31/60] provide cache controlling and render fixes --- src/observers/hrefs.js | 62 +++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/src/observers/hrefs.js b/src/observers/hrefs.js index 7a0ad65..2f5e5a6 100644 --- a/src/observers/hrefs.js +++ b/src/observers/hrefs.js @@ -1,6 +1,8 @@ +import { supportsPointerEvents } from 'detect-it' +import { eventFrom } from 'event-from' import { dispatchEvent, getLink, chunk } from '../app/utils' import { Link } from '../constants/common' -import { store } from '../app/store' +import store from '../app/store' import path from '../app/path' import scroll from './scroll' import request from '../app/request' @@ -81,21 +83,25 @@ export default (function (connected) { if (state) { - console.log(state) + if (state.cache === 'reset') store.clear(url) + if (state.cache === 'clear') store.clear() + if (store.has(url, { snapshot: true })) return render.update(state) const page = await request.get(state) + if (page) return render.update(page) } else { + if ((await request.inFlight(url))) { - return render.update(store.cache(url)) + return render.update(store.get(url).page) } else { request.cancel(url) } } - return window.location.replace(url) + return window.location.replace(path.absolute(url)) } @@ -150,8 +156,8 @@ export default (function (connected) { const handleClick = target => state => function click (event) { event.preventDefault() - event.stopPropagation() target.removeEventListener('click', click, false) + target.removeAttribute('style') if (!dispatchEvent('pjax:click', {}, true)) return undefined @@ -167,8 +173,9 @@ export default (function (connected) { * Triggers a page fetch * * @param {MouseEvent} event + * @returns {void} */ - const onMousedown = event => { + const handleTrigger = (event) => { // window.performance.mark('started') @@ -177,26 +184,29 @@ export default (function (connected) { const target = getLink(event.target, Link) if (!target) return undefined + if (!dispatchEvent('pjax:trigger', { target }, true)) return undefined + + const { url, location } = path.get(target, { update: true }) - store.history(path.url) + store.history() // PRESERVE CURRENT PAGE - const url = path.get(target, true) const click = handleClick(target) if (request.transit.has(url)) { + target.addEventListener('click', click(url), false) + } else { - const state = attrparse(target, { - url, - location: path.parse(url), - position: scroll.set(path.url) - }) + const state = attrparse(target, { url, location, position: scroll.set(path.url) }) + + if (state.capture) render.captureDOM(state.location.lastpath) if (store.has(url, { snapshot: true })) { target.addEventListener('click', click(store.update(state)), false) } else { - request.get(state) // TRIGGER FETCH + // TRIGGERS FETCH + request.get(state) target.addEventListener('click', click(url), false) } } @@ -205,9 +215,13 @@ export default (function (connected) { return { + /* EXPORTS ------------------------------------ */ + attrparse, navigate, + /* CONTROLS ----------------------------------- */ + /** * Attached `click` event listener. * @@ -216,8 +230,16 @@ export default (function (connected) { start: () => { if (!connected) { - addEventListener('mousedown', onMousedown, true) + + if (supportsPointerEvents) { + addEventListener('pointerdown', handleTrigger, false) + } else { + addEventListener('mousedown', handleTrigger, false) + addEventListener('touchstart', handleTrigger, false) + } + connected = true + } }, @@ -229,8 +251,16 @@ export default (function (connected) { stop: () => { if (connected) { - removeEventListener('mousedown', onMousedown, true) + + if (supportsPointerEvents) { + removeEventListener('pointerdown', handleTrigger, false) + } else { + removeEventListener('mousedown', handleTrigger, false) + removeEventListener('touchstart', handleTrigger, false) + } + connected = false + } } From 3a14c2319309078dd73ded70e63c2df6f8060ae6 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:19:06 +0100 Subject: [PATCH 32/60] update path parser --- src/observers/intersect.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/observers/intersect.js b/src/observers/intersect.js index 3855a75..f33c382 100644 --- a/src/observers/intersect.js +++ b/src/observers/intersect.js @@ -24,9 +24,7 @@ export default (function (connect) { if (isIntersecting) { - const url = path.get(target) - const state = hrefs.attrparse(target, { url, location: path.parse(target) }) - + const state = hrefs.attrparse(target, path.get(target)) const response = await request.get(state) if (response) { From 2424ae7264bc1841c27a7ebc690102f325c746a7 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:19:17 +0100 Subject: [PATCH 33/60] update store export --- src/observers/scroll.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/observers/scroll.js b/src/observers/scroll.js index 8c50571..dea5000 100644 --- a/src/observers/scroll.js +++ b/src/observers/scroll.js @@ -1,4 +1,4 @@ -import { store } from '../app/store' +import store from '../app/store' /** * Scroll position handler @@ -8,7 +8,7 @@ import { store } from '../app/store' export default (function (connected) { /** - * @type {IPjax.IPosition} + * @type {Store.IPosition} */ let position = { x: 0, y: 0 } @@ -17,7 +17,7 @@ export default (function (connected) { * a `x`and `y` positions to `0` * * @exports - * @returns {IPjax.IPosition} + * @returns {Store.IPosition} */ const reset = () => { @@ -49,12 +49,9 @@ export default (function (connected) { * to reset position. * * @exports - * @returns {IPjax.IPosition} + * @returns {Store.IPosition} */ - get position () { - - return position - }, + get position () { return position }, /** * Sets scroll position to the cache reference and @@ -74,9 +71,7 @@ export default (function (connected) { // We assert current position here - if (store.has(url)) { - store.cache(url).position = this.position - } + if (store.has(url)) store.get(url).page.position = this.position return reset() From ec7f4e1b63fc32bc42d07a1735aa0fcb5e791210 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:19:31 +0100 Subject: [PATCH 34/60] bring sanity to definitions --- types/index.d.ts | 28 +- types/state.d.ts | 457 -------------------------------- types/store.d.ts | 673 +++++++++++++++++++++++++---------------------- 3 files changed, 369 insertions(+), 789 deletions(-) delete mode 100644 types/state.d.ts diff --git a/types/index.d.ts b/types/index.d.ts index 2a4e2c4..f2b4d14 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,46 +1,50 @@ -declare module "@brixtol/pjax" { +import { IPage, IPresets } from "./store"; +declare module "@brixtol/pjax" { /** * Pjax Support */ - export const supported: boolean + export const supported: boolean; /** * Fetches state page by url. Pass `false` to clear cache */ - export function connect(options?: IPjax.IConfigPresets): IPjax.IState + export function connect(options?: IPresets): IPage; /** * Fetches state page by url. Pass `false` to clear cache */ - export function cache(ref?: string | false): IPjax.IState + export function cache(ref?: string | false): IPage; + + /** + * Clear cache. Pass in a `url` parameter to clear specific page + * else a complete cache clear will be triggered. + */ + export function clear(url?: string): IPage; /** * Reloads the current page */ - export function reload(): void + export function reload(): void; /** * Shortcut helper function for generating a UUID using nanoid. */ - export function uuid(size?: string): string + export function uuid(size?: string): string; /** * Captures current `` element and upon next history visit * will use the capture as replacement. */ - export function capture(url: string): void + export function capture(url: string): string; /** * Programmatic visit to location */ - export function visit(url: string, options?: IPjax.IVisit): void + export function visit(url: string, options?: IPage): Promise; /** * Removes all pjax listeners */ - export function disconnect(): void - + export function disconnect(): void; } - - diff --git a/types/state.d.ts b/types/state.d.ts deleted file mode 100644 index 47aaaa8..0000000 --- a/types/state.d.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { PartialPath } from 'history' - - - -/** - * Scroll position records - */ -export type IPosition = { - x: number, - y: number -} - -/** - * Methods - */ -export enum IMethods { - - /** - * Initialized - */ - Initial = 1, - - /** - * Initialized - */ - Click = 2, - - /** - * Prefetch - */ - Prefetch = 3, - - /** - * Cache - */ - Cache = 4, - - /** - * Pop - */ - Pop = 5, - - /** - * Capture - */ - Capture = 6 -} - -/** - * The URL location object - */ -export interface ILocation extends PartialPath { - /** - * The URL Pathname - * - * @example - * '/pathname' OR '/pathname/foo/bar' - */ - pathname?: string - - /** - * The URL search params - * - * @example - * '?param=foo&bar=baz' - */ - search?: string - - /** - * The URL Hash - * - * @example - * '#foo' - */ - hash?: string - - /** - * The previous path href URL. - * This is also the cache identifier - * - * @example - * '/pathname' OR '/pathname?foo=bar' - */ - lastpath?: string - -} - -/** - * NProgress Exposed Configuration Options - */ -export interface IProgress { - /** - * Changes the minimum percentage used upon starting. - * - * @default 0.08 - */ - minimum?: number - /** - * CSS Easing String - * - * @default cubic-bezier(0,1,0,1) - */ - easing?: string - /** - * Animation Speed - * - * @default 200 - */ - speed?: number - /** - * Turn off the automatic incrementing behavior - * by setting this to false. - * - * @default true - */ - trickle?: boolean - /** - * Adjust how often to trickle/increment, in ms. - * - * @default 200 - */ - trickleSpeed?: number - /** - * Turn on loading spinner by setting it to `true` - * - * @default false - */ - showSpinner?: boolean -} - - -export interface IPresets { - - /** - * Default fallback and preset fragments. By default, this pjax module will replace the - * entire `` fragment. Its best to define specific fragments here to prevent replacing - * static elements upon each navigation. - * - * --- - * @default ['body'] - */ - targets?: string[], - - /** - * Request Configuration - */ - request?: { - - /** - * The timeout limit of the XHR request issued. If timeout limit is exceeded a - * normal page visit will executed. - * - * --- - * @default 1500 - */ - timeout?: number, - - /** - * Throttle rate limits used when a request is already in transit. If you are leveraging - * prefetch capabilities then throttle limit will prevent requests already in-flight from - * occuring and instead wait until the initial request completes. - * - * --- - * @default 150 - */ - throttle?: number, - - /** - * FEATURE NOT YET AVAILABLE - * - * Define the request dispatch. By default, request are fetched upon mousedown, this allows - * fetching to start sooner that it would from an click event. - * - * > Currently, fetches are executed on `mousedown` only. Future releases will provide click - * dispatches - * - * --- - * @default 'mousedown' - */ - readonly dispatch?: 'mousedown' - }, - - /** - * Prefetch configuration - */ - prefetch?: { - - /** - * Mouseover prefetching preset configuration - */ - mouseover?: { - /** - * Enable or Disable mouseover (hover) prefetching. When enabled, this option - * will allow you to fetch pages over the wire upon mouseover and saves them to - * cache. When `mouseover` prefetches are disabled, all `data-pjax-prefetch="mouseover"` - * attribute configs will be ignored. - * - * > _If cache if disabled then prefetches will be dispatched using HTML5 - * `` prefetches, else when cache is enabled it uses XHR._ - * - * --- - * @default true - */ - enable?: boolean, - - /** - * Controls the mouseover fetch delay threshold. Requests will fire on mouseover - * only after the threshold time has been exceeded. This helps limit extrenous - * requests from firing. - * - * --- - * @default 250 - */ - threshold?: number - }, - - /** - * Intersection prefetching preset configuration - */ - intersect?: { - /** - * Enable or Disable intersection prefetching. Intersect prefetching leverages the - * Intersection Observer API to fire requests when elements become visible in viewport. - * When intersect prefetches are disabled, all `data-pjax-prefetch="intersect"` - * attribute configs will be ignored. - * - * > _If cache if disabled then prefetches will be dispatched using HTML5 - * `` prefetches, else when cache is enabled it uses XHR._ - * - * --- - * @default true - */ - enable?: boolean, - - /** - * Threshold limit passed to the intersection observer instance - * - * --- - * @default 0 - */ - threshold?: number - } - }, - - /** - * Caching engine configuration - */ - cache?: { - - /** - * Enable or Disable caching. Each page visit request is cached and used in - * subsequent visits to the same location. By disabling cache, all visits will - * be fetched over the network and any `data-pjax-cache` attribute configs - * will be ignored. - * - * --- - * @default true - */ - enable?: boolean - - /** - * Cache size limit. This pjax variation limits cache size to `25mb`and once size - * exceeds that limit, records will be removed starting from the earliest point - * cache entry. - * - * _Generally speaking, leave this the fuck alone._ - * - * --- - * @default 50 - */ - limit?: number, - - /** - * FEATURE NOT YET AVAILABLE - * - * The save option will save snapshot cache to IndexedDB. - * This feature is not yet available. - * - * --- - * @default false - */ - readonly save?: boolean - }, - - /** - * Progress Bar configuration - */ - progress?: { - - /** - * Enable or Disables the progress bar globally. Setting this option - * to `false` will prevent progress from displaying. When disabled, - * all `data-pjax-progress` attribute configs will be ignored. - * - * --- - * @default true - */ - enable?: boolean, - - /** - * Controls the progress bar preset threshold. Defines the amount of - * time to delay before the progress bar is shown. - * - * --- - * @default 350 - */ - threshold?: number, - - /** - * [N Progress](https://github.com/rstacruz/nprogress) provides the - * progress bar feature which is displayed between page visits. - * - * > _This pjax module does not expose all configuration options of nprogress, - * but does allow control of some internals. Any configuration options - * defined here will be passed to the nprogress instance upon initialization._ - */ - options?: IProgress - } - -} - -/** - * Page Visit State - * - * Configuration from each page visit. For every page navigation - * the configuration object is generated in a immutable manner. - */ -export interface IPage { - - /** - * The URL cache key and current url path - */ - url?: string - - /** - * UUID reference to the page snapshot HTML Document element - */ - snapshot?: string - - /** - * UUID to a captured snapshot HTML string. Captures are temporary - * snapshots used to preserve the document when navigating between history - * popstate. Captured snapshots are removed upon subsequent visits to location. - */ - captured?: boolean | string - - /** - * Location URL - */ - location?: ILocation - - /** - * The Document title - */ - title?: string - - /** - * List of target element selectors. Accepts any valid - * `querySelector()` string. - * - * @example - * ['#main', '.header', '[data-attr]', 'header'] - */ - targets?: string[] - - /** - * List of fragment element selectors. Accepts any valid - * `querySelector()` string. - * - * @example - * ['#main', '.header', '[data-attr]', 'header'] - */ - replace?: boolean | string[] - - /** - * List of fragments to be appened from and to. Accepts multiple. - * - * @example - * [['#main', '.header'], ['[data-attr]', 'header']] - */ - append?:boolean | Array<[from: string, to: string]>, - - /** - * List of fragments to be prepend from and to. Accepts multiple. - * - * @example - * [['#main', '.header'], ['[data-attr]', 'header']] - */ - prepend?: boolean | Array<[from: string, to: string]> - - /** - * Controls the caching engine for the link navigation. - * Option is enabled when `cache` preset config is `true`. - * Each pjax link can set a different cache option, see below: - * --- - * `false` - * - * Passing in __false__ will execute a pjax visit that will - * not be saved to cache and if the link exists in cache - * it will be removed. - * - * `reset` - * - * Passing in __reset__ the cache record will be removed, - * a new pjax visit will be executed and the result saved to cache. - * - * `flush` - * - * Passing in __flush__ will completely flush cache, removing all - * saved records. - */ - cache?: boolean | string - - /** - * Scroll position of the next navigation. - * - * --- - * `x` - Equivalent to `scrollLeft` in pixels - * - * `y` - Equivalent to `scrollTop` in pixels - */ - position?: IPosition - - /** - * Defines the - * - * @default 250 - */ - intersect?: number, - - /** - * Define mouseover timeout from which fetching will begin - * after time spent on mouseover - * - * @default 100 - */ - threshold?: number, - - /** - * Define proximity prefetch distance from which fetching will - * begin relative to the cursor offset of href elements. - * - * @default 50 - */ - proximity?: number, - - /** - * Progress bar threshold delay - * - * @default 350 - */ - progress?: boolean | number, - -} - -export as namespace Store; - diff --git a/types/store.d.ts b/types/store.d.ts index c4c4949..81d4070 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -1,257 +1,440 @@ -import { NProgressOptions } from 'nprogress' +import { PartialPath } from "history"; /** * Pjax Events */ -export type IEvents = ( - 'pjax:click' | - 'pjax:prefetch' | - 'pjax:request' | - 'pjax:cache' | - 'pjax:render' | - 'pjax:load' -) +export type IEvents = + | "pjax:prefetch" + | "pjax:trigger" + | "pjax:click" + | "pjax:request" + | "pjax:cache" + | "pjax:render" + | "pjax:load"; /** - * Action to be executed on navigation. + * Cache Size */ -export type IConfigMethod= 'replace' | 'prepend' | 'append' - -/** - * Prefetch operation on navigation - */ -export type IConfigPrefetch = 'intersect' | 'hover' - -/** - * Cache operation on navigation - */ -export type IConfigCache = 'false' | 'reset' | 'save' +export type ICacheSize = { + total: number; + weight: string; +}; /** * Scroll position records */ export type IPosition = { - x: number, - y: number -} - -/** - * Scroll position records - */ -export type ICacheSize = { - requests: number - total: number - weight: string -} - -export interface IProgress { - /** - * Changes the minimum percentage used upon starting. - * - * @default 0.08 - */ - minimum?: number - /** - * CSS Easing String - * - * @default cubic-bezier(0,1,0,1) - */ - easing?: string - /** - * Animation Speed - * - * @default 200 - */ - speed?: number - /** - * Turn off the automatic incrementing behavior - * by setting this to false. - * - * @default true - */ - trickle?: boolean - /** - * Adjust how often to trickle/increment, in ms. - * - * @default 200 - */ - trickleSpeed?: number - /** - * Turn on loading spinner by setting it to `true` - * - * @default false - */ - showSpinner?: boolean -} + x: number; + y: number; +}; /** * The URL location object */ -export interface ILocation { +export interface ILocation extends PartialPath { /** * The URL origin name * * @example * 'https://website.com' */ - origin: string + origin?: string; /** * The URL Hostname * * @example * 'website.com' */ - hostname: string - /** - * The URL href location name (full URL) - * - * @example - * 'https://website.com/pathname' - * OR - * 'https://website.com/pathname?param=foo&bar=baz' - */ - href: string + hostname?: string; + /** * The URL Pathname * * @example * '/pathname' OR '/pathname/foo/bar' */ - pathname: string + pathname?: string; + /** * The URL search params * * @example * '?param=foo&bar=baz' */ - search: string + search?: string; + /** * The URL Hash * * @example * '#foo' */ - hash: string + hash?: string; + /** - * The previous path href URL. - * This is also the cache identifier + * The previous page path URL, this is also the cache identifier * * @example * '/pathname' OR '/pathname?foo=bar' */ - lastUrl: string - + lastpath?: string; } -export type IConfigPresets = { +/** + * NProgress Exposed Configuration Options + */ +export interface IProgress { /** - * List of target element selectors. Accepts any valid - * `querySelector()` string. + * Changes the minimum percentage used upon starting. * - * @example - * ['#main', '.header', '[data-attr]', 'header'] + * @default 0.08 */ - target?: string[], + minimum?: number; /** - * Default method to be applied. + * CSS Easing String * - * @default 'replace' + * @default cubic-bezier(0,1,0,1) */ - method?: string, + easing?: string; /** - * Enable/disable prefetching. Settings this option to `false`will - * prevent prefetches from occuring and ignore all `data-pjax-prefetch="*"` - * attributes. + * Animation Speed * - * @default true + * @default 200 */ - prefetch?: boolean + speed?: number; /** - * Enable disable request caching, setting this option to `false` will - * prevent cached navigations and ignore all `data-pjax-cache="*"` attributes. + * Turn off the automatic incrementing behavior + * by setting this to false. * * @default true */ - cache?: boolean, + trickle?: boolean; + /** + * Adjust how often to trickle/increment, in ms. + * + * @default 200 + */ + trickleSpeed?: number; /** - * [NProgress](https://github.com/rstacruz/nprogress) provides the - * progress bar feature which is displayed between page visits. This pjax - * module does not expose all configuration options of nprogress, but does allow - * control of some internals. Any configuration options defined here will be - * passed to nprogress. + * Turn on loading spinner by setting it to `true` + * + * @default false */ - progress?: IProgress, + showSpinner?: boolean; +} + +export interface IPresets { /** - * Throttle delay between navigations, set this option if - * you want to delay the time between visits, helpful if - * navigation is too fast. + * Define page fragment targets. By default, this pjax module will replace the + * entire `` fragment, if undefined. Its best to define specific fragments. * - * @default 0 + * --- + * @default ['body'] */ - throttle?: number + targets?: string[]; /** - * Threshold Controls + * Request Configuration */ - threshold?: { + request?: { + /** + * The timeout limit of the XHR request issued. If timeout limit is exceeded a + * normal page visit will be executed. + * + * --- + * @default 3000 + */ + timeout?: number; + + /** + * Request polling limit is used when a request is already in transit. Request + * completion is checked every 10ms, by default this is set to 150 which means + * requests will wait 1500ms before being a new request is triggered. + * + * **BEWARE** + * + * Timeout limit will run precedence! + * + * --- + * @default 150 + */ + poll?: 150; /** - * Define an intersection threshold timeout from - * which intersected elements will begin fetching - * after being observed + * Determin if page requests should be fetched asynchronously or synchronously. + * Setting this to `false` is not reccomended. * - * @default 250 + * --- + * @default true */ - intersect?: number, + async?: boolean; /** - * Define hover timeout from which fetching will begin - * after time spent on mouseover - * - * @default 100 - */ - hover?: number, + * **FEATURE NOT YET AVAILABLE** + * + * Define the request dispatch. By default, request are fetched upon mousedown, this allows + * fetching to start sooner that it would from an click event. + * + * > Currently, fetches are executed on `mousedown` only. Future releases will provide click + * dispatches + * + * --- + * @default 'mousedown' + */ + readonly dispatch?: "mousedown"; + }; + /** + * Prefetch configuration + */ + prefetch?: { + /** + * Mouseover prefetching preset configuration + */ + mouseover?: { + /** + * Enable or Disable mouseover (hover) prefetching. When enabled, this option + * will allow you to fetch pages over the wire upon mouseover and saves them to + * cache. When `mouseover` prefetches are disabled, all `data-pjax-prefetch="mouseover"` + * attribute configs will be ignored. + * + * > _If cache if disabled then prefetches will be dispatched using HTML5 + * `` prefetches, else when cache is enabled it uses XHR._ + * + * --- + * @default true + */ + enable?: boolean; + + /** + * Controls the mouseover fetch delay threshold. Requests will fire on mouseover + * only after the threshold time has been exceeded. This helps limit extrenous + * requests from firing. + * + * --- + * @default 250 + */ + threshold?: number; + + /** + * **FEATURE NOT YET AVAILABLE** + * + * Proximity hovers allow for prefetch hovers to be dispatched when the cursor is within + * a proximity range of a href link element. Coupling proximity with mouseover prefetches + * enable predicative fetching to occur, so a request will trigger before any interaction. + * + * --- + * @default 0 + */ + readonly proximity?: number; + }; + + /** + * Intersection prefetching preset configuration + */ + intersect?: { + /** + * Enable or Disable intersection prefetching. Intersect prefetching leverages the + * Intersection Observer API to fire requests when elements become visible in viewport. + * When intersect prefetches are disabled, all `data-pjax-prefetch="intersect"` + * attribute configs will be ignored. + * + * > _If cache if disabled then prefetches will be dispatched using HTML5 + * `` prefetches, else when cache is enabled it uses XHR._ + * + * --- + * @default true + */ + enable?: boolean; + + /** + * Partial options passed to [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) + * + */ + options?: { + /** + * An offset rectangle applied to the root's href bounding box. + * + * --- + * @default '0px 0px 0px 0px' + */ + rootMargin?: string; + /** + * Threshold limit passed to the intersection observer instance + * + * --- + * @default 0 + */ + threshold?: number; + }; + }; + }; + + /** + * Caching engine configuration + */ + cache?: { /** - * Controls the progress bar threshold, where `1` equates - * to 25ms, maximum of `85` - * - * @default 2 - */ - progress?: number + * Enable or Disable caching. Each page visit request is cached and used in + * subsequent visits to the same location. By disabling cache, all visits will + * be fetched over the network and any `data-pjax-cache` attribute configs + * will be ignored. + * + * --- + * @default true + */ + enable?: boolean; - } + /** + * Cache size limit. This pjax variation limits cache size to `25mb`and once size + * exceeds that limit, records will be removed starting from the earliest point + * cache entry. + * + * _Generally speaking, leave this the fuck alone._ + * + * --- + * @default 50 + */ + limit?: number; + /** + * FEATURE NOT YET AVAILABLE + * + * The save option will save snapshot cache to IndexedDB. + * This feature is not yet available. + * + * --- + * @default false + */ + readonly save?: boolean; + }; + /** + * Progress Bar configuration + */ + progress?: { + /** + * Enable or Disables the progress bar globally. Setting this option + * to `false` will prevent progress from displaying. When disabled, + * all `data-pjax-progress` attribute configs will be ignored. + * + * --- + * @default true + */ + enable?: boolean; + + /** + * Controls the progress bar preset threshold. Defines the amount of + * time to delay before the progress bar is shown. + * + * --- + * @default 350 + */ + threshold?: number; + + /** + * [N Progress](https://github.com/rstacruz/nprogress) provides the + * progress bar feature which is displayed between page visits. + * + * > _This pjax module does not expose all configuration options of nprogress, + * but does allow control of some internals. Any configuration options + * defined here will be passed to the nprogress instance upon initialization._ + */ + options?: IProgress; + }; } -export interface IConfig { +/** + * Page Visit State + * + * Configuration from each page visit. For every page navigation + * the configuration object is generated in a immutable manner. + */ +export interface IPage { + /** + * The list of fragment target element selectors defined upon connection. + * + * @example + * ['#main', '.header', '[data-attr]', 'header'] + */ + readonly targets?: string[]; + + /** + * The URL cache key and current url path + */ + url?: string; + + /** + * UUID reference to the page snapshot HTML Document element + */ + snapshot?: string; + + /** + * UUID to a captured snapshot HTML string. Captures are temporary + * snapshots used to preserve the document when navigating between history + * popstate. Captured snapshots are removed upon subsequent visits to location. + */ + captured?: string; + + /** + * Location URL + */ + location?: ILocation; + + /** + * The Document title + */ + title?: string; + + /** + * Should this fetch be pushed to history + */ + history?: boolean; /** - * List of target element selectors. Accepts any valid + * Whether page should execute capture. When set to `true` + * the snapshot reference is created and its UUID value assigned + * to the `captured` property value. + * + * @default false + */ + capture?: boolean; + + /** + * List of fragment element selectors. Accepts any valid * `querySelector()` string. * * @example * ['#main', '.header', '[data-attr]', 'header'] */ - targets?: string[] + replace?: null | string[]; /** - * Default method to be applied. - * --- - * `replace` - Navigation target will be replaced - * - * `append` - Navigation target will be appened + * List of fragments to be appened from and to. Accepts multiple. * - * `prepend` - Navigation target will be prepended + * @example + * [['#main', '.header'], ['[data-attr]', 'header']] + */ + append?: null | Array<[from: string, to: string]>; + + /** + * List of fragments to be prepend from and to. Accepts multiple. * + * @example + * [['#main', '.header'], ['[data-attr]', 'header']] */ - method?: string + prepend?: null | Array<[from: string, to: string]>; /** * Controls the caching engine for the link navigation. * Option is enabled when `cache` preset config is `true`. - * Each pjax link can set a different cache option, see below: + * Each pjax link can set a different cache option. + * + * **IMPORTANT** + * + * Cache control is only operational on clicks, prefetches + * will not control cache. + * * --- * `false` * @@ -261,23 +444,15 @@ export interface IConfig { * * `reset` * - * Passing in __reset__ the cache record will be removed, - * a new pjax visit will be executed and the result saved to cache. - * - * `save` + * Passing in __reset__ will remove the requested page from cache + * (if it exsists) and the next navigation result will be saved. * - * Passing in __save__ will temporarily store the current - * cached state to session storage. It will be removed on your - * next navigation visit. + * `clear` * - * > _The save option should be avoided unless you are executing a - * full page reload and wish to store your cached pages to prevent - * new requests being executed on next navigation. If your cache exceeds - * 3mb in size cache records will be removed starting from the earliest - * point on of entry. Use `save` in conjunction with the `data-pjax-disable` - * option, else do your upmost to avoid it._ + * Passing in __clear__ will cleat the entire cache, removing all + * saved records. */ - cache?: false | 'false' | 'reset' | 'save' + cache?: boolean | string; /** * Scroll position of the next navigation. @@ -287,172 +462,30 @@ export interface IConfig { * * `y` - Equivalent to `scrollTop` in pixels */ - position?: IPosition - + position?: IPosition; /** - * List array of tracked elements pretaining to this link page - * navigation visit (if any). + * Define mouseover timeout from which fetching will begin + * after time spent on mouseover * - * @see https://github.com/panoply/pjax#data-pjax-track + * @default 100 */ - track?: Element[] + threshold?: number; /** - * Throttle delay between navigations, set this option if - * you want to delay the time between visits, helpful if - * navigation is too fast. + * Define proximity prefetch distance from which fetching will + * begin relative to the cursor offset of href elements. * * @default 0 */ - throttle?: number - - /** - * Threshold Controls - */ - threshold?: { - - /** - * Define an intersection threshold timeout from - * which intersected elements will begin fetching - * after being observed - * - * @default 250 - */ - intersect?: number, - - /** - * Define hover timeout from which fetching will begin - * after time spent on mouseover - * - * @default 100 - */ - hover?: number, - - /** - * Controls the progress bar threshold, where `1` equates - * to 25ms, maximum of `85` - * - * @default 2 - */ - progress?: number - - } - -} - -export interface IDom { - readonly tracked?: Set, - head?: object - snapshots: Map -} - - -export type IAttrs = { - [P in T as string]?: string[] -} - - -export interface IRequest { - readonly xhr?: Map, - cache?: { - weight?: string, - total?: number, - readonly limit?: number - } -} - -export interface IStoreState { - started: boolean - cache: Map - config: IConfigPresets; - page: IState; - dom: IDom; - request: IRequest; -} - - -export type IStoreUpdate = { - config: (patch?: IConfigPresets) => IConfigPresets; - page: (patch?: IState) => IState; - dom: (patch?: IDom) => IDom; - request: (patch?: IRequest) => IRequest; -} - -export interface IState extends IConfig { - - /** - * The fetched HTML response string - */ - snapshot?: string - - /** - * Captured HTML response string - */ - captured?: string - - /** - * The fetched HTML response string - */ - targets?: { - [selector: string]: Element[] - } + proximity?: number; /** - * The URL cache key - */ - url?: string - - - /** - * Location URL - */ - location?: ILocation - - /** - * The Document title - */ - title?: string - - /** - * Action + * Progress bar threshold delay + * + * @default 350 */ - action?: { - replace?: [target:string] - append?: Array<[from: string, to: string]>, - prepend?: Array<[from: string, to: string]>, - } - - /** - * Threshold - */ - threshold?: { - intersect?: number - mouseover?: number - hover?: number - - - } - -} - - -/** - * Event Details dispatched per lifecycle - */ -export interface IEventDetails { - target?: Element, - state?: IConfig, - data?: any + progress?: boolean | number; } - - -export type IVisit = { - replace: boolean -} - - - -export as namespace IPjax; - +export as namespace Store; From e9b54dcf2132bd2ace9971bf8cc4b9741ef968a4 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:19:41 +0100 Subject: [PATCH 35/60] adjust configs --- tsconfig.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 3de6352..3e5b916 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,6 @@ { - "exclude": [ - "node_modules" - ], "compilerOptions": { - "target": "es6", + "target": "ES6", "lib": [ "es2020", "dom", @@ -11,6 +8,7 @@ ], "module": "esnext", "checkJs": true, + "allowJs": true, "allowSyntheticDefaultImports": true, "keyofStringsOnly": true, "esModuleInterop": true, @@ -31,7 +29,7 @@ "src/*", "tests/*" ], - "~application": [ + "~app": [ "./src/app/*" ], "~constant": [ From 227d9c00dd9cd0f30c7e067e0cbd1ca0f209f084 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 01:20:00 +0100 Subject: [PATCH 36/60] update exports and fix broken exports --- src/index.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 2c1beba..b54bf68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,10 @@ import { Protocol } from './constants/regexp' -import { store } from './app/store' import { nanoid } from 'nanoid' +import store from './app/store' import render from './app/render' import path from './app/path' import hrefs from './observers/hrefs' -import * as controller from './app/controller' +import controller from './app/controller' /** * @export @@ -52,8 +52,10 @@ export const uuid = (size = 12) => nanoid(size) /** * Flush Cache + * + * @param {string} [url] */ -export const flush = () => store.clear() +export const clear = (url) => store.clear(url) /** * Capture DOM @@ -61,20 +63,20 @@ export const flush = () => store.clear() * @param {string} url * @param {object} action */ -export const capture = (url, action) => render.captureDOM(path.get(url), action) +export const capture = (url, action) => render.captureDOM(path.key(url), action) /** * Visit * - * @param {string} url + * @param {string|Element} link * @param {Store.IPage} state + * @returns {Promise} */ -export const visit = (url, state) => { - - url = path.get(url, true) +export const visit = (link, state = {}) => { - return hrefs.navigate(url, { ...state, url, location: path.parse(url) }) + const { url, location } = path.get(link, { update: true }) + return hrefs.navigate(url, { ...state, url, location }) } /** From aeff21cec0e1174c5c30ee9540f81ea2c5015ee1 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 19:50:25 +0100 Subject: [PATCH 37/60] v1.0.0.beta --- readme.md | 426 ++++++++++++++++------------- src/polyfills/getAttributeNames.js | 11 - 2 files changed, 239 insertions(+), 198 deletions(-) delete mode 100644 src/polyfills/getAttributeNames.js diff --git a/readme.md b/readme.md index c6c74e7..9a57094 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,15 @@ ## @brixtol/pjax -A modern next generation drop-in pjax solution for SSR web applications. +A new generation pjax solution for SSR web applications. ### Key Features ✓ Drop-in solution
-✓ Supports multiple fragments
-✓ Per-page configuration
-✓ Lifecycle event hooks
-✓ Intersection caching engine
+✓ Multiple fragments
+✓ Lifecycle events
✓ Pre-fetching capabilities
-✓ Tiny! Only 4.2kb minified and gzipped
-✓ Integrates seamlessly with Stimulus
- -### Why? - -The landscape of pjax orientated solutions has become rather scarce and all current bread winners are over engineered or offer the same basic shit. We wanted a size appropriate, fast and effective alternative that we could integrate seamlessly into our SSR SaaS based web apps. - -### Differences - -This pjax solution will cache each request using an immutable state management pattern. It provides opt-in prefetch capabilities using mouseover events and/or the Intersection Observer API. Each response is stored and rendered with the native DOM Parser and you can set per-page options via data attributes. +✓ Snapshot caching
+✓ Lightweight, 9kb gzipped
## Install @@ -29,9 +19,9 @@ pnpm i @brixtol/pjax > Because [pnpm](https://pnpm.js.org/en/cli/install) is dope and does dope shit. -## Get Started +## Usage -You do not create a class instance, the module has no classes or any of that oop shit but you do need to call `connect` to initialize. +To initialize, call `Pjax.connect()` in your bundle and optionally pass preset configuration. By default Pjax will replace the entire `` fragments upon each navigation, so it is recommended that you define a set of `targets[]` whose inner contents will change on per-page basis. ```js @@ -79,115 +69,65 @@ Pjax.connect({ }, }); -/* LIFECYCLE EVENTS -/* -------------------------------------------- */ - -document.addEventListener("pjax:click", ({ detail: { target, options } }) => {}); - -document.addEventListener("pjax:request", ({ detail: { url, type } }) => {}); - -document.addEventListener("pjax:cache", ({ detail: { record } }) => {}); - -document.addEventListener("pjax:render", ({ detail: { actions } }) => {}); - -document.addEventListener("pjax:load", ({ detail: { targets, location } }) => {}); - -/* HOOKS -/* -------------------------------------------- */ - -Pjax.on('request', (state) => {}) - -Pjax.on('cache', (state) => {}) - -Pjax.on('render', (state) => {}) - -Pjax.on('load', (state) => {}) - -/* ROUTING -/* -------------------------------------------- */ - -Pjax.route({ - '/:path': { - initialize() {}, - connect() {}, - disconnect() {}, - } -}) - -/* METHODS -/* -------------------------------------------- */ - -Pjax.supported: boolean - -Pjax.metrics: object +``` -Pjax.visit(url?, { cache, position, action, progress }) +## Lifecycle Events -Pjax.cache(url?) +Lifecycle events are dispatched to the document upon each navigation. You access context information from within `event.detail` and also cancel events with `preventDefault()` if you wish to prevent execution. -Pjax.flush() + +```javascript -Pjax.capture(url?, { action }) +// called when a prefetch is triggered +document.addEventListener("pjax:prefetch"); -Pjax.uuid(size = 16) +// called when a mousedown event occurs on a link +document.addEventListener("pjax:trigger"); -Pjax.reload() +// called before a page is fetched over XHR +document.addEventListener("pjax:request"); -Pjax.disconnect() +// called before a page is cached +document.addEventListener("pjax:cache"); +// called before a page is rendered +document.addEventListener("pjax:render"); -/* GLOBAL CONTEXT -/* -------------------------------------------- */ - -window.Pjax = { - session: { - '/': { - uuid: string, - cached: boolean, - history: boolean, - visits: number - } - }, -} - +// called after a page has rendered +document.addEventListener("pjax:load"); ``` -## Define Presets - -The below options will be used as the global default presets. Pass these options within `Pjax.connect()` and they will be inherited and applied to each page navigation. Once initialized you can control each page visit using attributes. You can omit the options and just use the defaults if you would rather that. - -| Option | Type | Default | -| --------- | ---------- | -------------------------------- | -| target | `string[]` | `['body']` | -| method | `string` | `replace` | -| throttle | `number` | `0` | -| cache | `boolean` | `true` | -| progress | `boolean` | `false` | -| threshold | `object{}` | `{ intersect: 250, hover: 100 }` | - ## Methods -#### `Pjax.connect(options?)` +In addition to Lifecycle events, a list of methods are available. Methods will allow you some basic programmatic control. -This is the initializer method. Call this to activate pjax. Pass in preset configuration options. +```javascript -#### `Pjax.visit(url, options?)` +// Check to see if Pjax is supported by the browser +Pjax.supported: boolean -Programmatic navigation visit to a URL. You can optionally pass in options for the visit. +// Connects Pjax, called upon initialization +Pjax.connect(options?): void -#### `Pjax.cache(url?)` +// Execute a programmatic visit +Pjax.visit(url?, options?): Promise -Returns cache `Map` session. All methods available to `Map` can be accessed via this method. +// Access the cache, pass in href for specific record +Pjax.cache(url?): Page{} -## Terminology +// Clears the cache, pass in href to clear specific record +Pjax.clear(url?): void -###### Targets +// Returns a UUID string via nanoid +Pjax.uuid(size = 16): string -Targets are fragment elements which contain a `data-pjax-target="*"` attribute. +// Reloads the current page +Pjax.reload(): Page{} -###### Actions +// Disconnects Pjax +Pjax.disconnect(): void -Actions are manipulations executed by pjax. +``` ## Navigation @@ -195,9 +135,9 @@ Actions are manipulations executed by pjax. #### `data-pjax-eval="false"` -Used on resources contained in the `` like styles and scripts. Use this attribute if you want pjax the evaluate scripts and/or stylesheets. This option accepts a `false` value so you can define which scripts to execute on each navigation. By default, pjax will run and evaluate all `` tags it detects each page visit but will not re-evaluate `` tags. +Used on resources contained within `` fragment like styles and scripts. Use this attribute if you want pjax the evaluate scripts and/or stylesheets. This option accepts a `false` value so you can define which scripts to execute on each navigation. By default, pjax will run and evaluate all `` tags. -> If a script tag is detected on pjax navigation and is using `data-pjax-eval="false"` it will execute only once, one the first visit and never again. +> If a script tag is detected on pjax navigation and is using `data-pjax-eval="false"` it will execute only once upon the first visit but never again after that.
@@ -219,25 +159,14 @@ Example #### `data-pjax-disable` -###### Options - -- `true` -- `history` - -Place on `href`elements you don't want pjax navigation to be executed. When present a normal page navigation will be executed and cache will be cleared unless combined with a `cache` option. +Place on `href` elements you don't want pjax navigation to be executed. When a link element is annotated with `data-pjax-disable` a normal page navigation will be executed and cache will be cleared.
Example -Clicking this link will clear cache and normal page navigation will be executed. - -```html - -``` - -Clicking this link will clear cache and normal page navigation will be executed. +Clicking this link will clear cache and a normal page navigation will be executed. ```html @@ -254,20 +183,18 @@ Place on elements to track on a per-page basis that might otherwise not be conta Example
-Lets assume you are navigating from `Page 1` to `Page 2` and `#main` is your defined target. When you navigate from `Page 1` only the `#main` target will be replaced and any other dom elements will be skipped that are not contained within that target. In order for Pjax to work as efficiently as possible any elements located outside of a target/s does not exist on the initialization page it will be added a new page navigation. +Lets assume you are navigating from `Page 1` to `Page 2` and `#main` is your defined target. When you navigate from `Page 1` only the `#main` target will be replaced and any other dom elements will be skipped which are not contained within `#main`. Element located outside of target/s that do no exist on previous or future pages will be added. ###### Page 1 ```html
- I will be replaced, I am active on every page. -
+
I will be replaced, I am active on every page.
``` @@ -276,36 +203,33 @@ Lets assume you are navigating from `Page 1` to `Page 2` and `#main` is your def ```html
- I will be replaced, I am active on every page. -
- -
- I am outside of target and will be tracked if Pjax was initialized on Page - 1 -
- - -
I will not be tracked unless Pjax was initialized on Page 2
-
+
I will be replaced, I am active on every page.
+
+ + +
+ I am outside of target and will be tracked if Pjax was initialized on Page 1
+ + +
I will not be tracked unless Pjax was initialized on Page 2
``` -> If pjax was initialized on `Page 2` the tracked element pjax would have knowledge of the tracked element before navigation as reference to the element exists on initialization. In such a situation, pjax will mark the tracked element internally. +> If pjax was initialized on `Page 2` then Pjax would have knowledge of its existence before navigation. In such a situation, pjax will mark the tracked element internally.

-#### `data-pjax-replace` +#### `data-pjax-replace="([])"` -Executes a replacement to single or multiple fragments. +Executes a replacement of defined targets, where each target defined in the array is replaced. -###### ATTRIBUTE +###### VALUE ATTRIBUTE - `(['target'])` - `(['target' , 'target'])` @@ -338,11 +262,11 @@ Example
-#### `data-pjax-prepend` +#### `data-pjax-prepend="([])"` -Executes a prepend visit. Locates target, then prepends it another target. A Prepend navigation will have its action recorded. +Executes a prepend visit, where `[0]` will prepend itself to `[1]` defined in that value. Multiple prepend actions can be defined. Each prepend action is recorded are marked. -###### ATTRIBUTE +###### VALUE EXPRESSION - `(['target' , 'target'])` - `(['target' , 'target'], ['target' , 'target'])` @@ -399,26 +323,11 @@ Example -#### `data-pjax-method="*"` - -The navigation method to execute on navigation. Accepts `replace`, `append` or `prepend`. When multiple target selectors are defined, space separate actions in accordance with target order. - -
- -Example - - -```html - -``` - -
- #### `data-pjax-prefetch="*"` -Prefetch option to execute for each link. Accepts either `intersect` or `hover` value. When `intersect` is provided a request will be dispatched and cached on visibility. +Prefetch option to execute. Accepts either `intersect` or `hover` value. When `intersect` is provided a request will be dispatched and cached upon visibility via Intersection Observer, whereas `hover` will dispatch a request upon a pointerover (mouseover) event. -> On mobile devices the `hover` value will execute on a `touch` event +> On mobile devices the `hover` value will execute on a `touchstart` event
@@ -426,6 +335,10 @@ Example ```html + + + + ``` @@ -433,7 +346,7 @@ Example #### `data-pjax-threshold="*"` -Set the threshold timeouts for pre-fetches. By default these options are `250ms` for `intersect` and `100` for `hover` elements. You can optionally set to a preferred defaults on preset. +Set the threshold delay timeout for hover prefetches. By default, this will be set to `100` or whatever preset configuration was defined in `Pjax.connect()` but you can override those settings by annotating the link with this attribute.
@@ -441,11 +354,11 @@ Example ```html - - - + + + - + ``` @@ -455,6 +368,11 @@ Example Scroll position of the next navigation. Space separated expression with colon separated prop and value. +###### VALUE EXPRESSION + +- `x:0 y:100` +- `y:200` +
Example @@ -470,9 +388,7 @@ Example #### `data-pjax-cache="*"` -Controls the caching engine for the link navigation. Accepts `false`, `reset` or `save` value. Passing in `false` will execute a pjax visit that will not be saved to cache and if the link exists in cache it will be removed. When passing `reset` the cache record will be removed, a new pjax visit will be executed and the result saved to cache. The `save` option will save temporarily save the current cached session to local or session storage (depending on your configuration presets) and will be removed on your next navigation visit. - -> The `save` option should be avoided unless you are executing a full page reload and wish to store your cached pages in order to prevent new requests being executed. If your cache exceeds 3mb in size cache records will be removed start from earliest point on of entry. Use `save` in conjunction with the `data-pjax-disable` option, else do your upmost to avoid it. +Controls the caching engine for the link navigation. Accepts `false`, `reset` or `clear` value. Passing in `false` will execute a pjax visit that will not be saved to cache and if the link exists in cache it will be removed. When passing `reset` the cache record will be removed, a new pjax visit will be executed and its result saved to cache. The `clear` option will clear the entire cache.
@@ -485,9 +401,9 @@ Example
-#### `data-pjax-throttle="*"` +#### `data-pjax-history="*"` -Navigation can be very fast when there is a cached record available in the browsing session. Each link click with cache is instantaneous and thus there might be some cases where you might like to throttle each navigation. +Controls the history pushstate for the navigation. Accepts `false`, `replace` or `push` value. Passing in `false`will prevent this navigation from being added to history. Passing in `replace` or `push` will execute its respective value to pushstate to history.
@@ -495,36 +411,172 @@ Example ```html - - + + ```
-## Events - -Each events can accessed via `document`and allows you to hook into each lifecycle. - -#### `pjax:click` +#### `data-pjax-progress="*"` -Fires when when a link has been clicked. You can cancel the pjax navigation with `preventDefault()`. +Controls the progress bar delay. By default, progress will use the threshold defined in configuration presets defined upon connection, else it will use the value defined on link attributes. Passing in a value of `false` will disable the progress from showing. -#### `pjax:request` +
+ +Example + -Fires after a request has completed. You can access the parsed response document via `target`and make adjustments where necessary. +```html + + +``` -#### `pjax:cache` +
-Fires on pre-fetches after caching a response. If you are leveraging `intersect` it will fire for each request encountered. +## State + +Each page has an object state value that is added to its pertaining History stack. Page state is immutable and created for every unique url `/path` or `/pathname?query=param` value encountered in the navigation session. + +> Navigation sessions begin once a Pjax connection (`Pjax.connect()`) has been established and ends when a browser refresh is executed or url origin changes. + +### Read + +You can access a readonly copy of page state via the `event.details.state` property within dispatched lifecycle events or via the `Pjax.cache()` method. The caching engine used by this Pjax variation acts as mediator when a session begins, when you access page state via the `Pjax.cache()` method you are given a bridge to the Map object where all the active current session page state exists. + +### Write + +State modifications are carried out via link attributes or when executing a programmatic visit using the `Pjax.visit()` method which provides an `options` parameter where you can send adjustments to be merged. This method will only allow you to modify the next navigation. You should avoid modifying state and instead treat it as readonly. + +```typescript +interface IPage { + /** + * The list of fragment target element selectors defined upon connection. + * Targets are inherited from `Pjax.connect()` presets. + * + * @example + * ['#main', '.header', '[data-attr]', 'header'] + */ + readonly targets?: string[]; + + /** + * The URL cache key and current url path + */ + url?: string; + + /** + * UUID reference to the page snapshot HTML Document element + */ + snapshot?: string; + + /** + * UUID to a captured snapshot HTML string. Captures are temporary + * snapshots used to preserve the document when navigating between history + * popstate. Captured snapshots are removed upon subsequent visits to location. + */ + captured?: string; + + /** + * Location URL + */ + location?: ILocation; + + /** + * The Document title + */ + title?: string; + + /** + * Should this fetch be pushed to history + */ + history?: boolean; + + /** + * Whether page should execute capture. When set to `true` + * the snapshot reference is created and its UUID value assigned + * to the `captured` property value. + * + * @default false + */ + capture?: boolean; + + /** + * List of fragment element selectors. Accepts any valid + * `querySelector()` string. + * + * @example + * ['#main', '.header', '[data-attr]', 'header'] + */ + replace?: null | string[]; + + /** + * List of fragments to append from and to. Accepts multiple. + * + * @example + * [['#main', '.header'], ['[data-attr]', 'header']] + */ + append?: null | Array<[from: string, to: string]>; + + /** + * List of fragments to be prepend from and to. Accepts multiple. + * + * @example + * [['#main', '.header'], ['[data-attr]', 'header']] + */ + prepend?: null | Array<[from: string, to: string]>; + + /** + * Controls the caching engine for the link navigation. + * Option is enabled when `cache` preset config is `true`. + * Each pjax link can set a different cache option. + */ + cache?: boolean | "reset" | "clear"; + + /** + * Scroll position of the next navigation. + * + * --- + * `x` - Equivalent to `scrollLeft` in pixels + * + * `y` - Equivalent to `scrollTop` in pixels + */ + position?: { + y: number; + x: number; + }; + + /** + * Define mouseover timeout from which fetching will begin + * after time spent on mouseover + * + * @default 100 + */ + threshold?: number; + + /** + * Define proximity prefetch distance from which fetching will + * begin relative to the cursor offset of href elements. + * + * @default 0 + */ + proximity?: number; + + /** + * Progress bar threshold delay + * + * @default 350 + */ + progress?: boolean | number; +} +``` -#### `pjax:render` +## Development -Fires before rendering document targets in the dom. When you are replacing multiple targets, it will fire for each replacement. +This module is written in ES2020 format JavaScript. Production bundles export in ES6 format. Legacy support is provided as an ES5 UMD bundle. This project leverages JSDocs and Type Definition files for its typechecking. If you have an editor that supports IntelliSense features, all TS features are available using this method. -#### `pjax:load` +### Contributing -Fires on initialization and on each page navigation. Treat this event as you would the `DOMContentLoaded` event. +Generally speaking, this module is consumed by us for a couple of our projects, we will update it according to what we need it for. If you wish to contribute, have a feature suggestion or bug, PR's are welcome! ### Licence -[MIT](#) +[MIT](LICENCE) diff --git a/src/polyfills/getAttributeNames.js b/src/polyfills/getAttributeNames.js deleted file mode 100644 index 030d601..0000000 --- a/src/polyfills/getAttributeNames.js +++ /dev/null @@ -1,11 +0,0 @@ -if (Element.prototype.getAttributeNames == undefined) { - Element.prototype.getAttributeNames = function () { - const attributes = this.attributes - const length = attributes.length - const result = new Array(length) - for (let i = 0; i < length; i++) { - result[i] = attributes[i].name - } - return result - } -} From 04038a758062eba649a8e47d0050bda97d165317 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 19:52:46 +0100 Subject: [PATCH 38/60] v1.0.0.beta --- readme.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index 9a57094..685269c 100644 --- a/readme.md +++ b/readme.md @@ -129,9 +129,7 @@ Pjax.disconnect(): void ``` -## Navigation - -
+## Attributes #### `data-pjax-eval="false"` @@ -223,8 +221,6 @@ Lets assume you are navigating from `Page 1` to `Page 2` and `#main` is your def
-
- #### `data-pjax-replace="([])"` Executes a replacement of defined targets, where each target defined in the array is replaced. @@ -260,8 +256,6 @@ Example
-
- #### `data-pjax-prepend="([])"` Executes a prepend visit, where `[0]` will prepend itself to `[1]` defined in that value. Multiple prepend actions can be defined. Each prepend action is recorded are marked. @@ -437,7 +431,7 @@ Example Each page has an object state value that is added to its pertaining History stack. Page state is immutable and created for every unique url `/path` or `/pathname?query=param` value encountered in the navigation session. -> Navigation sessions begin once a Pjax connection (`Pjax.connect()`) has been established and ends when a browser refresh is executed or url origin changes. +> Navigation sessions begin once a Pjax connection has been established and ends when a browser refresh is executed or url origin changes. ### Read From a5e6113f63780e3a1906b64f9d9c634c5825f1c2 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 19:53:19 +0100 Subject: [PATCH 39/60] v1.0.0.beta --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 685269c..0f7ead8 100644 --- a/readme.md +++ b/readme.md @@ -433,11 +433,11 @@ Each page has an object state value that is added to its pertaining History stac > Navigation sessions begin once a Pjax connection has been established and ends when a browser refresh is executed or url origin changes. -### Read +#### Read You can access a readonly copy of page state via the `event.details.state` property within dispatched lifecycle events or via the `Pjax.cache()` method. The caching engine used by this Pjax variation acts as mediator when a session begins, when you access page state via the `Pjax.cache()` method you are given a bridge to the Map object where all the active current session page state exists. -### Write +#### Write State modifications are carried out via link attributes or when executing a programmatic visit using the `Pjax.visit()` method which provides an `options` parameter where you can send adjustments to be merged. This method will only allow you to modify the next navigation. You should avoid modifying state and instead treat it as readonly. From c756ea1ade3f535fe892b7d48173de6f3bdc8960 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sat, 27 Mar 2021 20:18:43 +0100 Subject: [PATCH 40/60] v1.0.0.beta --- readme.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/readme.md b/readme.md index 0f7ead8..eaf0145 100644 --- a/readme.md +++ b/readme.md @@ -1,15 +1,6 @@ ## @brixtol/pjax -A new generation pjax solution for SSR web applications. - -### Key Features - -✓ Drop-in solution
-✓ Multiple fragments
-✓ Lifecycle events
-✓ Pre-fetching capabilities
-✓ Snapshot caching
-✓ Lightweight, 9kb gzipped
+A blazing fast, lightweight (5kb gzipped), feature full drop-in new generation pjax solution for SSR web applications. Supports multiple fragment replacements, appends and prepends, pre-fetching capabilities via mouse, pointer, touch and intersection event and snapshot caching which prevent subsequent requests for occurring resulting in instantaneous navigation. ## Install @@ -27,9 +18,6 @@ To initialize, call `Pjax.connect()` in your bundle and optionally pass preset c ```js import * as Pjax from "@brixtol/pjax"; -/* CONNECT -/* -------------------------------------------- */ - Pjax.connect({ targets: ["body"], cache: { From 66bfd327d5638056055dd45d68b4071023e5db76 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:15:36 +0200 Subject: [PATCH 41/60] Update readme.md --- readme.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/readme.md b/readme.md index eaf0145..4e9f8c4 100644 --- a/readme.md +++ b/readme.md @@ -450,13 +450,6 @@ interface IPage { */ snapshot?: string; - /** - * UUID to a captured snapshot HTML string. Captures are temporary - * snapshots used to preserve the document when navigating between history - * popstate. Captured snapshots are removed upon subsequent visits to location. - */ - captured?: string; - /** * Location URL */ @@ -472,15 +465,6 @@ interface IPage { */ history?: boolean; - /** - * Whether page should execute capture. When set to `true` - * the snapshot reference is created and its UUID value assigned - * to the `captured` property value. - * - * @default false - */ - capture?: boolean; - /** * List of fragment element selectors. Accepts any valid * `querySelector()` string. From b486668a1ba76767e6d6827b3ce8d9d226bae256 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:15:48 +0200 Subject: [PATCH 42/60] Update controller.js --- src/app/controller.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/controller.js b/src/app/controller.js index 1dc25e2..2873e37 100644 --- a/src/app/controller.js +++ b/src/app/controller.js @@ -81,8 +81,12 @@ export default (function (connected) { } return { - initialize, - destroy + + /* EXPORTS ------------------------------------ */ + + initialize + , destroy + } }(false)) From 11c62fd0e2bea22b2e2d4493f089a045cf328731 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:16:34 +0200 Subject: [PATCH 43/60] run capture each visit + tidy up --- src/app/render.js | 133 ++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 87 deletions(-) diff --git a/src/app/render.js b/src/app/render.js index 684710b..35ade32 100644 --- a/src/app/render.js +++ b/src/app/render.js @@ -1,10 +1,7 @@ -import { eachSelector, dispatchEvent, forEach } from './utils' +import { dispatchEvent, forEach } from './utils' import { progress } from './progress' import history from 'history/browser' -import { createPath } from 'history' -import { nanoid } from 'nanoid' import * as prefetch from './prefetch' -import scroll from '../observers/scroll' import store from './store' /** @@ -14,6 +11,11 @@ import store from './store' */ export default (function () { + /** + * @type{DOMParser} data + */ + const DOMParse = new DOMParser() + /** * Tracked Elements * @@ -23,13 +25,13 @@ export default (function () { /** * Parse HTML document string from request response - * using `DomParser()` method. Cached pages will pass + * using `parser()` method. Cached pages will pass * the saved response here. * - * @param {string} data + * @param {string} HTMLString * @return {Document} */ - const DOMParse = data => new DOMParser().parseFromString(data, 'text/html') + const parse = HTMLString => DOMParse.parseFromString(HTMLString, 'text/html') /** * DOM Head Nodes @@ -57,7 +59,7 @@ export default (function () { * * @param {HTMLHeadElement} head */ - const DOMHead = ({ children }) => { + const DOMHead = async ({ children }) => { const targetNodes = Array.from(children).reduce((arr, node) => ( node.tagName !== 'TITLE' ? ( @@ -103,67 +105,38 @@ export default (function () { */ const replaceTarget = (target, state) => DOM => { - dispatchEvent('pjax:render', { target }, true) + if (dispatchEvent('pjax:render', { target }, true)) { - DOM.innerHTML = target.innerHTML + DOM.innerHTML = target.innerHTML - if (state?.append || state?.prepend) { + if (state?.append || state?.prepend) { - const fragment = document.createElement('div') - const nodes = [].slice.call(target.childNodes) + const fragment = document.createElement('div') - forEach(nodes, node => fragment.appendChild(node)) + forEach([ ...target.childNodes ], node => fragment.appendChild(node)) - state.append - ? DOM.appendChild(fragment) - : DOM.insertBefore(fragment, DOM.firstChild) + state.append + ? DOM.appendChild(fragment) + : DOM.insertBefore(fragment, DOM.firstChild) + } } - } /** * Captures current document element and sets a * record to snapshot state * - * @param {Document} target - * @returns {string} - */ - const DOMSnapshot = target => { - const uuid = nanoid(16) - store.set.snapshots(uuid, target.documentElement.outerHTML) - return uuid - } - - /** - * Updates cached DOM - * - * @param {string} url - * @param {{ action: 'replace' | 'capture'}} options - * @returns {string} + * @param {Store.IPage} state */ - const captureDOM = (url, options = { action: 'capture' }) => { - - if (!store.has(url, { snapshot: true })) return undefined - - const { snapshot, page } = store.get(url) - const target = DOMParse(snapshot) - target.body.innerHTML = document.documentElement.querySelector('body').innerHTML - - if (options.action === 'capture') { - - page.captured = DOMSnapshot(target) - page.position = scroll.position - - history.replace(page.location, store.update(page)) - console.info('Pjax: DOM Captured at: ' + page.captured) + const capture = async ({ url, snapshot }) => { - } else if (options.action === 'replace') { - store.snapshots.set(page.snapshot, target.documentElement.outerHTML) + if (store.has(url, { snapshot: true })) { + const target = parse(store.snapshot(snapshot)) + target.body.innerHTML = document.body.innerHTML + store.set.snapshots(snapshot, target.documentElement.outerHTML) } - return target.documentElement.outerHTML - } /** @@ -172,57 +145,37 @@ export default (function () { * * @param {Store.IPage} state * @param {boolean} [popstate=false] - * @returns {Store.IPage} */ const update = (state, popstate = false) => { // window.performance.mark('render') + // console.log(window.performance.measure('time', 'start')) - prefetch.stop() - - const uuid = (popstate && state.captured) ? state.captured : state.snapshot - const target = DOMParse(store.snapshot(uuid)) + const target = parse(store.snapshot(state.snapshot)) state.title = document.title = target?.title || '' if (!popstate && state.history) { - - if (createPath(history.location) === state.url) { + if (state.url === state.location.lastpath) { history.replace(state.location, state) } else { history.push(state.location, state) } - - } else if (state?.captured) { - - if (store.delete.snapshots(uuid)) { - state.captured = null - // history.replace(state.location, store.update(state)) - console.info('Pjax: Captured snapshot removed at: ' + state.url) - } - } if (target?.head) DOMHead(target.head) let fallback = 1 - console.log(state.replace ? [ - ...state.targets, - ...state.replace - ] : state.targets) - - forEach(state.replace ? [ - ...state.targets, - ...state.replace - ] : state.targets, element => { + forEach(state?.replace + ? [ ...state.targets, ...state.replace ] + : state.targets, element => { const node = target.body.querySelector(element) - return node - ? eachSelector(document, element, replaceTarget(node, state)) - : fallback++ - + return node ? document.body + .querySelectorAll(element) + .forEach(replaceTarget(node, state)) : fallback++ }) if (fallback === state.targets.length) { @@ -230,24 +183,30 @@ export default (function () { } // APPEND TRACKED NODES - eachSelector(target, '[data-pjax-track]', appendTrackedNode) + target.body + .querySelectorAll('[data-pjax-track]') + .forEach(appendTrackedNode) window.scrollTo(state.position.x, state.position.y) - dispatchEvent('pjax:load', state) - progress.done() prefetch.start() - return state + dispatchEvent('pjax:load', state) + // console.log(window.performance.measure('Render Time', 'render')) // console.log(window.performance.measure('Total', 'started')) } return { - update, - captureDOM + + /* EXPORTS ------------------------------------ */ + + update + , parse + , capture + } })() From cfad4df81158505c2536f1f3790e28775b58eb93 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:16:57 +0200 Subject: [PATCH 44/60] progress bar and move async timeout --- src/app/request.js | 55 +++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/app/request.js b/src/app/request.js index ef01afa..b81d352 100644 --- a/src/app/request.js +++ b/src/app/request.js @@ -1,5 +1,5 @@ import store from './store' -import { asyncTimeout, byteConvert, byteSize, dispatchEvent } from './utils' +import { byteConvert, byteSize, dispatchEvent } from './utils' import { progress } from './progress' /** @@ -19,6 +19,11 @@ export default (function () { */ let storage = 0 + /** + * @type {boolean} + */ + let showprogress = false + /** * XHR Requests * @@ -26,27 +31,37 @@ export default (function () { */ const transit = new Map() + /** + * Async Timeout + * + * @param {function} callback + * @param {number} ms + * @returns {Promise} + */ + const asyncTimeout = (callback, ms = 0) => { + return new Promise(resolve => setTimeout(() => { + const fn = callback() + resolve(fn) + }, ms)) + } + /** * Executes on request end. Removes the XHR recrod and update * the response DOMString cache size record. * - * @exports * @param {string} url * @param {string} DOMString - * @returns {boolean} */ const HttpRequestEnd = (url, DOMString) => { + transit.delete(url) storage = storage + byteSize(DOMString) - return transit.delete(url) - } /** * Fetch XHR Request wrapper function * - * @exports * @param {string} url * @returns {Promise} */ @@ -72,6 +87,7 @@ export default (function () { xhr.onloadend = e => HttpRequestEnd(url, xhr.responseText) xhr.onerror = reject xhr.timeout = store.config.request.timeout + xhr.responseType = 'text' // SEND // @@ -117,18 +133,22 @@ export default (function () { */ const inFlight = async (url) => { - if (transit.has(url) && ratelimit <= store.config.request.poll) { - // console.log('Request in flight', ratelimit * 25) + if (transit.has(url) && ratelimit <= store.config.request.timeout) { - if ((ratelimit * 10) >= store.config.progress.threshold) progress.start() + if (!showprogress && (ratelimit * 10) === store.config.progress.threshold) { + progress.start() + showprogress = true + } return asyncTimeout(() => { ratelimit++ return inFlight(url) - }, 10) + }, 1) + } ratelimit = 0 + showprogress = false return !transit.has(url) @@ -175,10 +195,15 @@ export default (function () { } return { - get, - inFlight, - cancel, - transit, + + /* EXPORTS ------------------------------------ */ + + get + , cancel + , transit + , inFlight + + /* GETTERS ------------------------------------ */ /** * Returns request cache metrics @@ -186,7 +211,7 @@ export default (function () { * @exports * @returns {Store.ICacheSize} */ - get cacheSize () { + , get cacheSize () { return { total: storage, From e96ff0c1038cf6fa6dd899c4de1f28fedd593ae9 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:17:36 +0200 Subject: [PATCH 45/60] update store generator, improve snapshot logic --- src/app/store.js | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/app/store.js b/src/app/store.js index 909ed40..4ced087 100644 --- a/src/app/store.js +++ b/src/app/store.js @@ -4,6 +4,7 @@ import { nanoid } from 'nanoid' import { dispatchEvent } from './utils' import scroll from '../observers/scroll' import * as nprogress from './progress' +import render from './render' /** * store @@ -89,11 +90,6 @@ export default ((config) => { // EDITABLE // history: true, - capture: false, - append: null, - prepend: null, - replace: null, - captured: null, snapshot: state?.snapshot || nanoid(16), position: state?.position || scroll.reset(), cache: this.config.cache.enable, @@ -162,7 +158,16 @@ export default ((config) => { * @returns {string} */ get snapshot () { - return snapshots.get(this.page.snapshot) + if (this.page?.snapshot) { + return snapshots.get(this.page.snapshot) + } + }, + + /** + * @returns {Document} + */ + get target () { + return render.parse(this.snapshot) } }), @@ -178,13 +183,19 @@ export default ((config) => { * @param {string} key * @param {Store.IPage} value */ - cache: (key, value) => cache.set(key, value), + cache: (key, value) => { + cache.set(key, value) + return value + }, /** * @param {string} key * @param {string} value */ - snapshots: (key, value) => snapshots.set(key, value) + snapshots: (key, value) => { + snapshots.set(key, value) + return key + } }) @@ -230,16 +241,19 @@ export default ((config) => { * Update current pushState History * * @param {string} url - * @returns {void} + * @returns {string} */ - history: () => ( + history: () => { history.replace(history.location, { ...history.location.state, position: scroll.position }) - ), + // @ts-ignore + return history.location.state.url + + }, /** * Updates page state, this function will run a merge @@ -269,8 +283,8 @@ export default ((config) => { })({ targets: [ 'body' ], request: { - timeout: 3000, - poll: 150, + timeout: 30000, + poll: 250, async: true, dispatch: 'mousedown' }, @@ -291,7 +305,7 @@ export default ((config) => { }, progress: { enable: true, - threshold: 500, + threshold: 850, options: { minimum: 0.10, easing: 'ease', From f0e3d415b109fc5830938bf57bdb4a9593a2b567 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:17:56 +0200 Subject: [PATCH 46/60] remove single use utils to relative closures --- src/app/utils.js | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/app/utils.js b/src/app/utils.js index dd78179..de02464 100644 --- a/src/app/utils.js +++ b/src/app/utils.js @@ -167,22 +167,6 @@ export function byteConvert (bytes) { ) } -/** - * Async Timeout - * - * @exports - * @param {function} callback - * @param {number} ms - * @returns {Promise} - */ -export function asyncTimeout (callback, ms = 0) { - - return new Promise(resolve => setTimeout(() => { - const fn = callback() - resolve(fn) - }, ms)) -} - /** * Each iterator helper function. Provides a util function * for loop iterations @@ -194,7 +178,6 @@ export function asyncTimeout (callback, ms = 0) { * @return {void} */ export function forEach (list, fn, { index = false } = {}) { - let i = list.length - 1 for (; i >= 0; i--) index ? fn(list[i], i) : fn(list[i]) } @@ -222,18 +205,3 @@ export function getElementAttrs ({ attributes }, exclude = []) { ] ) : accumulator, []) } - -/** - * Each Selector - * - * @exports - * @param {Document} document - * @param {string} query * - * @param {(element: Element) => void} callback - * @returns {void} - */ -export function eachSelector ({ body }, query, callback) { - - return [ ...body.querySelectorAll(query) ].forEach(callback) - -} From d4de34f195311b4305c08bc3551f5f6505cf3ff6 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:18:27 +0200 Subject: [PATCH 47/60] remove capture attribute and add CacheValue matcher --- src/constants/regexp.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/constants/regexp.js b/src/constants/regexp.js index 88131b8..c117739 100644 --- a/src/constants/regexp.js +++ b/src/constants/regexp.js @@ -6,7 +6,17 @@ * @exports * @type {RegExp} */ -export const Attr = /^data-pjax-(append|prepend|replace|history|capture|progress|threshold|position)$/i +export const Attr = /^data-pjax-(append|prepend|replace|history|progress|threshold|position)$/i + +/** + * Form Inputs + * + * Used to match Form Input elements + * + * @exports + * @type {RegExp} + */ +export const CacheValue = /^(reset|clear)$/i /** * URL Pathname From 0f77184a0ce75628bb3600cdc2cad9d519081741 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:18:45 +0200 Subject: [PATCH 48/60] location.assign and updateState getter --- src/observers/history.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/observers/history.js b/src/observers/history.js index 6929c29..74ab9ea 100644 --- a/src/observers/history.js +++ b/src/observers/history.js @@ -3,6 +3,7 @@ import { createPath } from 'history' import render from '../app/render' import request from '../app/request' import store from '../app/store' +import scroll from './scroll' /* -------------------------------------------- */ /* FUNCTIONS */ @@ -48,7 +49,7 @@ export default (function (connected) { return page ? render.update(page, true) - : window.location.replace(url) + : location.assign(url) } @@ -61,7 +62,6 @@ export default (function (connected) { const listener = ({ action, location }) => { // console.log(action, location) - if (action === 'POP') { return popstate(createPath(location), location.state) } @@ -70,6 +70,27 @@ export default (function (connected) { return { + /* GETTERS ------------------------------------ */ + + /** + * Execute a history state replacement for the current + * page location. Its intended use is to update the + * current scroll position and any other values stored + * in history state. + * + * @returns {Store.IPage} url + */ + get updateState () { + + history.replace(history.location, { + ...history.location.state + , position: scroll.position + }) + + return history.location.state + + }, + /* CONTROLS ----------------------------------- */ /** From 9a484675bd9a913768af1cd9b96b2f98a30cfc2e Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:18:57 +0200 Subject: [PATCH 49/60] y0x0 position --- src/observers/hover.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/observers/hover.js b/src/observers/hover.js index db494a3..0479d44 100644 --- a/src/observers/hover.js +++ b/src/observers/hover.js @@ -3,6 +3,7 @@ import { eventFrom } from 'event-from' import { LinkPrefetchHover } from '../constants/common' import { getLink, getTargets, dispatchEvent } from '../app/utils' import hrefs from './hrefs' +import scroll from './scroll' import request from '../app/request' import path from '../app/path' import store from '../app/store' @@ -115,7 +116,7 @@ export default (function (connected) { const state = hrefs.attrparse(target, { url, location, - position: { x: 0, y: 0 } + position: scroll.y0x0 }) throttle(url, async () => { @@ -224,6 +225,8 @@ export default (function (connected) { return { + /* CONTROLS ----------------------------------- */ + /** * Starts mouseovers, will attach mouseover events * to all elements which contain a `data-pjax-prefetch="hover"` From 4940114c946db8c9ccb42264719b1ec671dbd4a7 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:19:11 +0200 Subject: [PATCH 50/60] improve basic logics --- src/observers/hrefs.js | 46 ++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/observers/hrefs.js b/src/observers/hrefs.js index 2f5e5a6..5c08e33 100644 --- a/src/observers/hrefs.js +++ b/src/observers/hrefs.js @@ -1,13 +1,14 @@ import { supportsPointerEvents } from 'detect-it' -import { eventFrom } from 'event-from' import { dispatchEvent, getLink, chunk } from '../app/utils' import { Link } from '../constants/common' +import * as prefetch from './../app/prefetch' +import * as regexp from '../constants/regexp' import store from '../app/store' import path from '../app/path' import scroll from './scroll' import request from '../app/request' import render from '../app/render' -import * as regexp from '../constants/regexp' +import history from './history' /** * Link (href) handler @@ -83,13 +84,13 @@ export default (function (connected) { if (state) { - if (state.cache === 'reset') store.clear(url) - if (state.cache === 'clear') store.clear() - - if (store.has(url, { snapshot: true })) return render.update(state) + if (typeof state.cache === 'string') { + state.cache === 'clear' + ? store.clear() + : store.clear(url) + } const page = await request.get(state) - if (page) return render.update(page) } else { @@ -101,7 +102,7 @@ export default (function (connected) { } } - return window.location.replace(path.absolute(url)) + return location.assign(url) } @@ -157,15 +158,14 @@ export default (function (connected) { event.preventDefault() target.removeEventListener('click', click, false) - target.removeAttribute('style') - - if (!dispatchEvent('pjax:click', {}, true)) return undefined + render.capture(history.updateState) // PRESERVE CURRENT PAGE + prefetch.stop() return typeof state === 'object' ? render.update(state) : typeof state === 'string' ? navigate(state) - : window.location.replace(path.absolute(path.url)) + : location.assign(path.url) } @@ -177,7 +177,7 @@ export default (function (connected) { */ const handleTrigger = (event) => { - // window.performance.mark('started') + window.performance.mark('start') if (!linkEvent(event)) return undefined @@ -187,9 +187,6 @@ export default (function (connected) { if (!dispatchEvent('pjax:trigger', { target }, true)) return undefined const { url, location } = path.get(target, { update: true }) - - store.history() // PRESERVE CURRENT PAGE - const click = handleClick(target) if (request.transit.has(url)) { @@ -198,15 +195,12 @@ export default (function (connected) { } else { - const state = attrparse(target, { url, location, position: scroll.set(path.url) }) - - if (state.capture) render.captureDOM(state.location.lastpath) + const state = attrparse(target, { url, location, position: scroll.y0x0 }) if (store.has(url, { snapshot: true })) { target.addEventListener('click', click(store.update(state)), false) } else { - // TRIGGERS FETCH - request.get(state) + request.get(state) // TRIGGERS FETCH target.addEventListener('click', click(url), false) } } @@ -217,8 +211,8 @@ export default (function (connected) { /* EXPORTS ------------------------------------ */ - attrparse, - navigate, + attrparse + , navigate /* CONTROLS ----------------------------------- */ @@ -227,7 +221,7 @@ export default (function (connected) { * * @returns {void} */ - start: () => { + , start: () => { if (!connected) { @@ -241,14 +235,14 @@ export default (function (connected) { connected = true } - }, + } /** * Removed `click` event listener. * * @returns {void} */ - stop: () => { + , stop: () => { if (connected) { From cc00370b5e9d698ab81ae238843bbe077aa7c0f1 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:19:16 +0200 Subject: [PATCH 51/60] Update intersect.js --- src/observers/intersect.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/observers/intersect.js b/src/observers/intersect.js index f33c382..49d79a0 100644 --- a/src/observers/intersect.js +++ b/src/observers/intersect.js @@ -55,6 +55,8 @@ export default (function (connect) { return { + /* CONTROLS ----------------------------------- */ + /** * Starts prefetch, will initialize `IntersectionObserver` and * add event listeners and other logics. From 3bcd1d20e246a4a90d8f0db203406202ff12a69b Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:19:43 +0200 Subject: [PATCH 52/60] remove set fn, instead use new logics --- src/observers/scroll.js | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/observers/scroll.js b/src/observers/scroll.js index dea5000..e40883d 100644 --- a/src/observers/scroll.js +++ b/src/observers/scroll.js @@ -1,5 +1,3 @@ -import store from '../app/store' - /** * Scroll position handler * @@ -41,8 +39,12 @@ export default (function (connected) { return { + /* EXPORTS ------------------------------------ */ + reset, + /* GETTERS ------------------------------------ */ + /** * Returns to current scroll position, the `reset()` * function **MUST** be called after referencing this @@ -54,28 +56,15 @@ export default (function (connected) { get position () { return position }, /** - * Sets scroll position to the cache reference and - * returns a reset position. - * - * This function is called before a new page visit - * navigation begins, as it will assert the current - * position to the current page and return the reset - * position, ie: `{x: 0, y: 0 }`) to new page visit. - * + * Returns a faux scroll position. This prevents the + * tracked scroll position from being overwritten and is + * used within functions like `href.attrparse` * - * @exports - * @param {string} url * @returns {Store.IPosition} */ - set (url) { - - // We assert current position here - - if (store.has(url)) store.get(url).page.position = this.position + get y0x0 () { return { x: 0, y: 0 } }, - return reset() - - }, + /* CONTROLS ----------------------------------- */ /** * Attached `scroll` event listener. From 4344586cf7bc399c4e6c7d5c8c5cadb5b9897605 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:19:53 +0200 Subject: [PATCH 53/60] remove capture and captured --- types/store.d.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/types/store.d.ts b/types/store.d.ts index 81d4070..f0b673c 100644 --- a/types/store.d.ts +++ b/types/store.d.ts @@ -369,13 +369,6 @@ export interface IPage { */ snapshot?: string; - /** - * UUID to a captured snapshot HTML string. Captures are temporary - * snapshots used to preserve the document when navigating between history - * popstate. Captured snapshots are removed upon subsequent visits to location. - */ - captured?: string; - /** * Location URL */ @@ -391,15 +384,6 @@ export interface IPage { */ history?: boolean; - /** - * Whether page should execute capture. When set to `true` - * the snapshot reference is created and its UUID value assigned - * to the `captured` property value. - * - * @default false - */ - capture?: boolean; - /** * List of fragment element selectors. Accepts any valid * `querySelector()` string. From 60c420abcd5d51e80be4ce2c87c4f13c6601e121 Mon Sep 17 00:00:00 2001 From: Nikolas Savvidis Date: Sun, 28 Mar 2021 20:56:53 +0200 Subject: [PATCH 54/60] Update readme.md --- readme.md | 169 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 55 deletions(-) diff --git a/readme.md b/readme.md index 4e9f8c4..b93092e 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,12 @@ ## @brixtol/pjax -A blazing fast, lightweight (5kb gzipped), feature full drop-in new generation pjax solution for SSR web applications. Supports multiple fragment replacements, appends and prepends, pre-fetching capabilities via mouse, pointer, touch and intersection event and snapshot caching which prevent subsequent requests for occurring resulting in instantaneous navigation. +A blazing fast, lightweight (5kb gzipped), feature full drop-in next generation pjax solution for SSR web applications. Supports multiple fragment replacements, appends and prepends. Pre-fetching capabilities via mouse, pointer, touch and intersection events and snapshot caching which prevent subsequent requests for occurring that results in instantaneous navigation. + +**Note:** _This is still in beta stage, use it with care, expect some changes to be shipped before official release._ + +### Example + +We are using this module live on our [webshop](https://brixtoltextiles.com). ## Install @@ -8,11 +14,11 @@ A blazing fast, lightweight (5kb gzipped), feature full drop-in new generation p pnpm i @brixtol/pjax ``` -> Because [pnpm](https://pnpm.js.org/en/cli/install) is dope and does dope shit. +> _Because [pnpm](https://pnpm.js.org/en/cli/install) is dope and does dope shit._ ## Usage -To initialize, call `Pjax.connect()` in your bundle and optionally pass preset configuration. By default Pjax will replace the entire `` fragments upon each navigation, so it is recommended that you define a set of `targets[]` whose inner contents will change on per-page basis. +To initialize, call `Pjax.connect()` in your bundle and optionally pass preset configuration. By default Pjax will replace the entire `` fragment upon each navigation. You should define a set of `targets[]` whose inner contents change on a per-page basis. ```js @@ -25,8 +31,7 @@ Pjax.connect({ limit: 25, }, requests: { - timeout: 1500, - poll: 150, + timeout: 30000, async: true, }, prefetch: { @@ -61,7 +66,7 @@ Pjax.connect({ ## Lifecycle Events -Lifecycle events are dispatched to the document upon each navigation. You access context information from within `event.detail` and also cancel events with `preventDefault()` if you wish to prevent execution. +Lifecycle events are dispatched to the document upon each navigation. You can access context information from within `event.detail` or cancel events with `preventDefault()` and prevent execution. ```javascript @@ -87,7 +92,7 @@ document.addEventListener("pjax:load"); ## Methods -In addition to Lifecycle events, a list of methods are available. Methods will allow you some basic programmatic control. +In addition to Lifecycle events, a list of methods are available. Methods will allow you some basic programmatic control of the Pjax session. ```javascript @@ -119,7 +124,11 @@ Pjax.disconnect(): void ## Attributes -#### `data-pjax-eval="false"` +Link elements can be annotated with `data-pjax` attributes. You can control how pages are rendered by passing the below attributes on `` nodes. + +
+`data-pjax-eval="false"` +
Used on resources contained within `` fragment like styles and scripts. Use this attribute if you want pjax the evaluate scripts and/or stylesheets. This option accepts a `false` value so you can define which scripts to execute on each navigation. By default, pjax will run and evaluate all `` tags. @@ -143,7 +152,7 @@ Example
-#### `data-pjax-disable` +##### data-pjax-disable Place on `href` elements you don't want pjax navigation to be executed. When a link element is annotated with `data-pjax-disable` a normal page navigation will be executed and cache will be cleared. @@ -160,7 +169,7 @@ Clicking this link will clear cache and a normal page navigation will be execute -#### `data-pjax-track` +##### data-pjax-track Place on elements to track on a per-page basis that might otherwise not be contained within target elements. @@ -171,7 +180,7 @@ Example Lets assume you are navigating from `Page 1` to `Page 2` and `#main` is your defined target. When you navigate from `Page 1` only the `#main` target will be replaced and any other dom elements will be skipped which are not contained within `#main`. Element located outside of target/s that do no exist on previous or future pages will be added. -###### Page 1 +**Page 1** ```html