diff --git a/.babelrc b/.babelrc
index 75cc0be..d816c8d 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,17 @@
{
- "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",
+ "@babel/plugin-proposal-class-properties"
+ ]
}
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
diff --git a/.gitignore b/.gitignore
index 4b8cc44..bf2cdba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ public
package
!src/public
node_modules
+versions
# FILES -----------------------------
.env
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..1a7ea63
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,20 @@
+# DIRECTORIES
+# --------------------
+.vscode/
+node_modules/
+src/
+demo/
+tests/
+versions/
+
+# FILES
+# -------------------
+.babelrc
+.browserslistrc
+.eslintignore
+.gitignore
+.prettierignore
+rollup.config.js
+tsconfig.json
+changelog.md
+readme.md
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
+ }
+ }
+ ]
+}
diff --git a/package.json b/package.json
index 9123dc8..fc956a0 100644
--- a/package.json
+++ b/package.json
@@ -1,26 +1,33 @@
{
"name": "@brixtol/pjax",
- "version": "0.1.0",
+ "version": "0.1.0-beta.1",
"private": false,
"description": "A modern next generation pjax solution for SSR web applications",
"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": [
"@brixtol/eslint-config-javascript"
- ]
+ ],
+ "rules": {
+ "multiline-ternary": "off"
+ }
},
"ava": {
"files": [
@@ -34,28 +41,49 @@
"cjs": true
},
"dependencies": {
- "mergerino": "^0.4.0"
+ "@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",
+ "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.12.3",
- "@babel/preset-env": "^7.12.1",
- "@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",
+ "@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",
+ "@babel/preset-env": "^7.13.10",
+ "@babel/runtime-corejs3": "^7.13.10",
+ "@brixtol/browserslist-config": "^1.0.3",
+ "@brixtol/eslint-config-javascript": "^2.0.1",
+ "@brixtol/prettier-config": "^1.0.3",
+ "@brixtol/rollup-utils": "^0.1.0",
+ "@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"
},
diff --git a/readme.md b/readme.md
index c013ac5..21274f9 100644
--- a/readme.md
+++ b/readme.md
@@ -1,25 +1,12 @@
-## @brixtol/pjax
-
-A modern next generation drop-in pjax solution for SSR web applications.
-
-### Key Features
+> _This is still in beta stages, use it with care and expect some changes to be shipped before official release. Tests are still being worked on and will be pushed at v1, sit tight._
-✓ Drop-in solution
-✓ Supports multiple fragments
-✓ Per-page configuration
-✓ Lifecycle event hooks
-✓ Intersection caching engine
-✓ Pre-fetching capabilities
-✓ Tiny! Only 4.2kb minified and gzipped
-✓ Integrates seamlessly with Stimulus
-
-### Why?
+## @brixtol/pjax
-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.
+A blazing fast, lightweight (8kb 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.
-### Differences
+##### Example
-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.
+We are using this module live on our [webshop](https://brixtoltextiles.com).
## Install
@@ -27,98 +14,123 @@ This pjax solution will cache each request using an immutable state management 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._
-## 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 `
` fragment upon each navigation. You should define a set of `targets[]` whose inner contents change on a per-page basis.
+
```js
import * as Pjax from "@brixtol/pjax";
-/* CONNECT
-/* -------------------------------------------- */
-
Pjax.connect({
- target: ["main", "#navbar"],
- action: "replace",
- prefetch: true,
- cache: true,
- throttle: 0,
- progress: false,
- threshold: {
- intersect: 250,
- hover: 100,
+ targets: ["body"],
+ cache: {
+ enable: true,
+ limit: 25,
+ },
+ requests: {
+ timeout: 30000,
+ 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 }) => {});
+## Lifecycle Events
-document.addEventListener("pjax:click", (event) => {});
+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.
-document.addEventListener("pjax:request", (event) => {});
+
+```javascript
-document.addEventListener("pjax:cache", (event) => {});
+// called when a prefetch is triggered
+document.addEventListener("pjax:prefetch");
-document.addEventListener("pjax:render", ({ detail }) => {});
-```
+// called when a mousedown event occurs on a link
+document.addEventListener("pjax:trigger");
-You can also cherry-pick the export methods:
+// called before a page is fetched over XHR
+document.addEventListener("pjax:request");
-```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,
- },
-});
+// called before a page is cached
+document.addEventListener("pjax:cache");
+
+// called before a page is rendered
+document.addEventListener("pjax:render");
+
+// called after a page has rendered
+document.addEventListener("pjax:load");
```
-## Define Presets
+## Methods
-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.
+In addition to Lifecycle events, a list of methods are available. Methods will allow you some basic programmatic control of the Pjax session.
-| Option | Type | Default |
-| --------- | ---------- | -------------------------------- |
-| target | `string[]` | `['body']` |
-| method | `string` | `replace` |
-| throttle | `number` | `0` |
-| cache | `boolean` | `true` |
-| progress | `boolean` | `false` |
-| threshold | `object{}` | `{ intersect: 250, hover: 100 }` |
+```javascript
-## Methods
+// Check to see if Pjax is supported by the browser
+Pjax.supported: boolean
-#### `Pjax.connect(options?)`
+// Connects Pjax, called upon initialization
+Pjax.connect(options?): void
-This is the initializer method. Call this to activate pjax. Pass in preset configuration options.
+// Execute a programmatic visit
+Pjax.visit(url?, options?): Promise
-#### `Pjax.visit(url, options?)`
+// Access the cache, pass in href for specific record
+Pjax.cache(url?): Page{}
-Programmatic navigation visit to a URL. You can optionally pass in options for the visit.
+// Clears the cache, pass in href to clear specific record
+Pjax.clear(url?): void
-#### `Pjax.cache(url?)`
+// Returns a UUID string via nanoid
+Pjax.uuid(size = 16): string
-Returns cache `Map` session. All methods available to `Map` can be accessed via this method.
+// Reloads the current page
+Pjax.reload(): Page{}
-## Navigation Options
+// Disconnects Pjax
+Pjax.disconnect(): void
-#### `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.
+## Attributes
-> 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.
+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
+
+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 upon the first visit but never again after that.
@@ -138,22 +150,16 @@ Example
-#### `data-pjax-disable`
+#### data-pjax-disable
-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
@@ -161,7 +167,7 @@ Clicking this link will clear cache and normal page navigation will be executed.
-#### `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.
@@ -170,106 +176,143 @@ 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
+**Page 1**
```html
- I will be replaced, I am active on every page.
-
+
I will be replaced, I am active on every page.
```
-###### Page 2
+**Page 2**
```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-target="*"`
+#### data-pjax-replace
-Target selectors for navigation. Use space separation when defining multiple targets to reload.
+Executes a replacement of defined targets, where each target defined in the array is replaced.
+
+- `(['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, where `[0]` will prepend itself to `[1]` defined in that value. Multiple prepend actions can be defined. Each prepend action is recorded are marked.
-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.
+- `(['target' , 'target'])`
+- `(['target' , 'target'], ['target' , 'target'])`
Example
+**PAGE 1**
+
+
```html
-
-```
+ href="*"
+ data-pjax-prepend="(['target-1', 'target-2'])">
+ Page 2
+
-
+
+ I will prepend to target-2 on next navigation
+
-#### `data-pjax-method="*"`
+
+
target-1 will prepended to me on next navigation
+
-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
-
+**PAGE 2**
+
```html
-
+
+
+ Page 2
+
+
+
+
+
+
+ I am target-1 and have been prepended to target-2
+
+
+
target-1 is now prepended to me
+
+
+
```
-#### `data-pjax-prefetch="*"`
+#### 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
@@ -277,14 +320,18 @@ Example
```html
+
+
+
+
```
-#### `data-pjax-threshold="*"`
+#### 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.
@@ -292,17 +339,17 @@ Example
```html
-
-
-
+
+
+
-
+
```
-#### `data-pjax-position="*"`
+#### data-pjax-position
Scroll position of the next navigation. Space separated expression with colon separated prop and value.
@@ -319,11 +366,9 @@ Example
-#### `data-pjax-cache="*"`
+#### 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.
@@ -336,9 +381,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.
@@ -346,36 +391,216 @@ Example
```html
-
-
+
+
```
-## Events
+#### data-pjax-progress
+
+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.
-Each events can accessed via `document`and allows you to hook into each lifecycle.
+
+
+Example
+
-#### `pjax:click`
+```html
+
+
+```
+
+
-Fires when when a link has been clicked. You can cancel the pjax navigation with `preventDefault()`.
+## State
+
+Each page has an object state value. Page state is immutable and created for every unique url `/path` or `/pathname?query=param` value encountered throughout a pjax navigation session. The state value of each page is added to its pertaining History stack record.
+
+> Navigation sessions begin once a Pjax connection 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, so when you access page state via the `Pjax.cache()` method you are given a bridge to the Map object of all active sessions cache data.
+
+#### Write
+
+State modifications are carried out via link attributes or when executing a programmatic visit using the `Pjax.visit()` method. The visit method provides an `options` parameter for adjustments to be merged, please note that this method will only allow you to modify the next navigation. Generally speaking, you should avoid modifying state outside of the available methods, 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;
+
+ /**
+ * The Document title
+ */
+ title?: string;
+
+ /**
+ * Should this fetch be pushed to history
+ */
+ history?: 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";
+
+ /**
+ * 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;
+
+ /**
+ * Scroll position of the next navigation.
+ *
+ * ---
+ * `x` - Equivalent to `scrollLeft` in pixels
+ *
+ * `y` - Equivalent to `scrollTop` in pixels
+ */
+ position?: {
+ y: number;
+ x: number;
+ };
+
+ /**
+ * Location URL
+ */
+ location?: {
+ /**
+ * The URL origin name
+ *
+ * @example
+ * 'https://website.com'
+ */
+ origin?: string;
+ /**
+ * The URL Hostname
+ *
+ * @example
+ * 'website.com'
+ */
+ hostname?: string;
+
+ /**
+ * 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 page path URL, this is also the cache identifier
+ *
+ * @example
+ * '/pathname' OR '/pathname?foo=bar'
+ */
+ lastpath?: string;
+ };
+}
+```
-#### `pjax:request`
+## Contributing
-Fires after a request has completed. You can access the parsed response document via `target`and make adjustments where necessary.
+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 type checking, so all features you enjoy with TypeScript are available.
-#### `pjax:cache`
+This module is consumed by us for a couple of our projects, we will update it according to what we need. Feel free to suggest features or report bugs, PR's are welcome too!
-Fires on pre-fetches after caching a response. If you are leveraging `intersect` it will fire for each request encountered.
+## Acknowledgements
-#### `pjax:render`
+This module combines concepts originally introduced by other awesome Open Source projects and owes its creation and overall approach to those originally creators:
-Fires before rendering document targets in the dom. When you are replacing multiple targets, it will fire for each replacement.
+- [Defunkt Pjax](https://github.com/defunkt/jquery-pjax)
+- [Pjax.js](https://github.com/brcontainer/pjax.js)
+- [MoOx Pjax](https://github.com/MoOx/pjax)
+- [InstantClick](https://github.com/dieulot/instantclick)
+- [Turbo](https://github.com/hotwired/turbo)
+- [Turbolinks](https://github.com/turbolinks/turbolinks)
-#### `pjax:load`
+## Licence
-Fires on initialization and on each page navigation. Treat this event as you would the `DOMContentLoaded` event.
+Licensed under [MIT](#LICENCE)
-### Licence
+---
-[MIT](#)
+We [♡](https://www.brixtoltextiles.com/discount/4D3V3L0P3RS]) open source!
diff --git a/rollup.config.js b/rollup.config.js
index 81fb09a..90db91f 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -2,48 +2,130 @@ 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' ],
+ [ '@babel/plugin-proposal-class-properties' ]
+ ]
+ }),
+ 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' ],
+ [ '@babel/plugin-proposal-class-properties' ]
+ ]
+ }),
+ filesize()
+ ]
+ }
+]
diff --git a/src/app/controller.js b/src/app/controller.js
index ee6f95c..2873e37 100644
--- a/src/app/controller.js
+++ b/src/app/controller.js
@@ -1,153 +1,92 @@
-import { store } from './store'
-import { dispatchEvent } from './utils'
-import { expandURL } from './location'
-import { xhrSuccess } from '../constants/enums'
-import * as hrefs from '../observers/hrefs'
-import * as prefetch from '../observers/prefetch'
-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
- */
-function setInitialCache ({ location: { href } }) {
-
- const location = expandURL(href)
- const state = store.update.page({
- location,
- url: location.pathname + location.search,
- snapshot: document.documentElement.outerHTML
- })
-
- store.cache.set(store.page.url, state)
-
-}
-
-/**
- * Initialize
- */
-export const initialize = () => {
-
- if (!store.started) {
-
- setInitialCache(window)
-
- hrefs.start()
- prefetch.start()
-
- console.info('Pjax: Connection Established ⚡')
-
- addEventListener('popstate', popstate)
- dispatchEvent('pjax:load', store.page)
-
- store.started = true
+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 _history from 'history/browser'
+import path from './path'
+import store from './store'
+
+export default (function (connected) {
+
+ /**
+ * Sets initial page state executing on intial load.
+ * Caches page so a return navigation does not perform
+ * an extrenous request.
+ *
+ * @returns {void}
+ */
+ const onload = () => {
+
+ const page = store.create({
+ url: path.url,
+ location: path.parse(path.url),
+ position: scroll.position
+ }, document.documentElement.outerHTML)
+
+ _history.replace(history.location, page)
+
+ removeEventListener('load', onload)
}
-}
-
-/**
- * Destory Pjax instances
- *
- * @exports
- */
-export function destroy () {
+ /**
+ * Initialize
+ *
+ * @exports
+ * @returns {void}
+ */
+ const initialize = () => {
- if (store.started) {
+ if (!connected) {
- removeEventListener('popstate', popstate)
+ history.start()
+ hrefs.start()
+ scroll.start()
+ hover.start()
+ intersect.stop()
- hrefs.stop()
- prefetch.stop()
+ addEventListener('load', onload)
- store.cache.clear()
- store.started = false
+ connected = true
- console.info('Pjax: Instance has been disconnected! 😔')
-
- } else {
- console.info('Pjax: No connection made, disconnection is void 🙃')
+ console.info('Pjax: Connection Established ⚡')
+ }
}
-}
-
-/**
- * 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)
+ /**
+ * Destory Pjax instances
+ *
+ * @exports
+ * @returns {void}
+ */
+ const destroy = () => {
- // console.log('cache', url)
+ if (connected) {
- if (store.config.prefetch) prefetch.stop()
+ history.stop()
+ hrefs.stop()
+ scroll.stop()
+ hover.stop()
+ intersect.stop()
+ store.clear()
- // ensure we have state before updating
- if (state) render.update(state)
+ connected = false
- if (store.config.prefetch) prefetch.start()
+ console.warn('Pjax: Instance has been disconnected! 😔')
+ } else {
+ console.warn('Pjax: No connection made, disconnection is void 🙃')
+ }
-}
-
-/**
- * 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)
+ return {
- if (response === xhrSuccess) render.update(store.page)
+ /* EXPORTS ------------------------------------ */
- if (store.config.prefetch) prefetch.start()
+ initialize
+ , destroy
-}
-
-/**
- * Executes a pjax navigation.
- *
- * @param {IPjax.IState} state
- */
-export function navigate (state) {
+ }
- return store.cache.has(state.url) ? cacheVisit(state.url) : pjaxVisit(state)
-}
+}(false))
diff --git a/src/app/location.js b/src/app/location.js
deleted file mode 100644
index 0613ba3..0000000
--- a/src/app/location.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Expands URL href location.
- *
- * @param {string} url
- */
-export function expandURL (url) {
-
- const anchor = document.createElement('a')
-
- anchor.href = url.toString()
-
- const {
- protocol
- , origin
- , hostname
- , href
- , pathname
- , search
- } = new URL(anchor.href)
-
- return {
- protocol
- , origin
- , hostname
- , href
- , pathname
- , search
- }
-
-}
-
-/**
- * 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
- *
- * @param {Element} target
- */
-export const getURL = target => expandURL(target.getAttribute('href'))
-
-/**
- * Returns the pathname from `href` target used for cache key.
- *
- * @param {IPjax.ILocation} location
- */
-export const getCacheKey = ({ pathname, search }) => (pathname + search)
-
-/**
- * Returns the pathname from `href` target used for cache key.
- *
- * @param {Element} target
- */
-export const getCacheKeyFromTarget = target => getCacheKey(getURL(target))
-
-/**
- * Returns the protocol and host
- *
- * @param {URL} location
- */
-export const getProtocol = ({ protocol, host }) => protocol.replace(/:/g, `://${host}`)
diff --git a/src/app/path.js b/src/app/path.js
new file mode 100644
index 0000000..4470744
--- /dev/null
+++ b/src/app/path.js
@@ -0,0 +1,139 @@
+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
+
+ /**
+ * 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
+ * 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 }
+
+ }
+
+})(window)
diff --git a/src/app/prefetch.js b/src/app/prefetch.js
new file mode 100644
index 0000000..f84d883
--- /dev/null
+++ b/src/app/prefetch.js
@@ -0,0 +1,30 @@
+import store from './store'
+import mouseover from '../observers/hover'
+import intersect from '../observers/intersect'
+
+/**
+ * Starts prefetch, will initialize `IntersectionObserver` and
+ * add event listeners and other logics.
+ *
+ * @exports
+ * @returns {void}
+ */
+export function start () {
+
+ if (store.config.prefetch.mouseover.enable) mouseover.start()
+ if (store.config.prefetch.intersect.enable) intersect.start()
+
+}
+
+/**
+ * Stops prefetch, will disconnect `IntersectionObserver` and
+ * remove any event listeners or transits.
+ *
+ * @exports
+ * @returns {void}
+ */
+export function 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
new file mode 100644
index 0000000..e07ff5d
--- /dev/null
+++ b/src/app/progress.js
@@ -0,0 +1,26 @@
+import nprogress from 'nprogress'
+
+/* -------------------------------------------- */
+/* LETTINGS */
+/* -------------------------------------------- */
+
+/**
+ * @type {nprogress.NProgress}
+ */
+export let progress = null
+
+/* -------------------------------------------- */
+/* FUNCTIONS */
+/* -------------------------------------------- */
+
+/**
+ * Setup nprogress
+ *
+ * @export
+ * @param {Store.IProgress} options
+ */
+export const config = options => {
+
+ progress = nprogress.configure(options)
+
+}
diff --git a/src/app/render.js b/src/app/render.js
index 50f02d0..35ade32 100644
--- a/src/app/render.js
+++ b/src/app/render.js
@@ -1,243 +1,212 @@
-import { DOMParseFallback, isReplace } from '../constants/regexp'
-import { Implementation, ArraySlice, DomParser } from '../constants/common'
-import { eachSelector, dispatchEvent, forEach } from './utils'
-import { store } from './store'
+import { dispatchEvent, forEach } from './utils'
+import { progress } from './progress'
+import history from 'history/browser'
+import * as prefetch from './prefetch'
+import store from './store'
/**
- * DOM Head Nodes
+ * Renderer
*
- * @param {string[]} nodes
- * @param {HTMLHeadElement} head
- * @return {string}
+ * @param {boolean} connected
*/
-function DOMHeadNodes (nodes, { children }) {
+export default (function () {
- 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('')
-
-}
-
-/**
- * DOM Head
- *
- * @param {HTMLHeadElement} head
- */
-function 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)
- }
- })
+ /**
+ * @type{DOMParser} data
+ */
+ const DOMParse = new DOMParser()
-}
+ /**
+ * Tracked Elements
+ *
+ * @type {Set}
+ */
+ const tracked = new Set()
-/**
- * Append Tracked Node
- *
- * @param {Element} node
- */
-function appendTrackedNode (node) {
+ /**
+ * Parse HTML document string from request response
+ * using `parser()` method. Cached pages will pass
+ * the saved response here.
+ *
+ * @param {string} HTMLString
+ * @return {Document}
+ */
+ const parse = HTMLString => DOMParse.parseFromString(HTMLString, 'text/html')
- // tracked element must contain id
- if (!node.hasAttribute('id')) return
+ /**
+ * 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)
+ }
+ })
+
+ return nodes.join('')
- if (!store.dom.tracked.has(node.id)) {
- document.body.appendChild(node)
- store.dom.tracked.add(node.id)
}
-}
-
-/**
- * Apply actions to the documents target fragments
- * with the request response.
- *
- * @param {Element} target
- * @param {IPjax.IState} state
- * @returns {(DOM: Element) => void}}
- * @memberof Render
- */
-const replaceTarget = (target, element, { method }) => DOM => {
-
- if (!isReplace.test(method)) {
-
- dispatchEvent('pjax:render', { method, element, fragment: target }, true)
-
- DOM.innerHTML = target.innerHTML
-
- } else {
-
- let fragment = document.createElement('div')
- const nodes = ArraySlice.call(target.childNodes)
-
- forEach(nodes, node => fragment.appendChild(node))
-
- if (method === 'append') {
- dispatchEvent('pjax:render', { method, element, fragment }, true)
- DOM.appendChild(fragment)
+ /**
+ * DOM Head
+ *
+ * @param {HTMLHeadElement} head
+ */
+ const DOMHead = async ({ 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)
+ }
+ })
- console.log(fragment)
- console.log('in append')
+ }
- } else {
- dispatchEvent('pjax:render', { method, element, fragment }, true)
- DOM.insertBefore(fragment, DOM.firstChild)
+ /**
+ * Append Tracked Node
+ *
+ * @param {Element} node
+ */
+ const 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)
}
- fragment = null
-
}
-}
-
-/**
- * Updates cached DOM
- *
- * @export
- * @param {string} url
- */
-export function getActiveDOM (url) {
- if (store.config.cache && store.cache.has(url)) {
+ /**
+ * 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 => {
- // store.cache.get(url).snapshot = document.documentElement.outerHTML
+ if (dispatchEvent('pjax:render', { target }, true)) {
- // console.log(store.cache.get(url).snapshot)
+ DOM.innerHTML = target.innerHTML
- }
+ if (state?.append || state?.prepend) {
-}
+ const fragment = document.createElement('div')
-/**
- * Parse HTML document string from request response
- * using `DomParser()` method. Cached pages will pass
- * the saved response here.
- *
- * @param {string} data
- * @return {Document}
- */
-export function DOMParse (data) {
+ forEach([ ...target.childNodes ], node => fragment.appendChild(node))
- if (DomParser) return DomParser.parseFromString(data, 'text/html')
+ state.append
+ ? DOM.appendChild(fragment)
+ : DOM.insertBefore(fragment, DOM.firstChild)
- /**
- * 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
-
-}
-
-/**
- * Update the DOM and execute page adjustments
- * to new navigation point
- *
- * @param {IPjax.IState} state
- * @param {boolean} [popstate=false]
- * @memberof Render
- */
-export function update (state, popstate = false) {
-
- console.log(state)
-
- const target = DOMParse(state.snapshot)
- const title = document.title = target.title || ''
-
- if (!popstate) {
- history.pushState(state, title, state.location.href)
- }
+ /**
+ * Captures current document element and sets a
+ * record to snapshot state
+ *
+ * @param {Store.IPage} state
+ */
+ const capture = async ({ url, snapshot }) => {
+
+ 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)
+ }
- if (target.head) {
- DOMHead(target.head)
}
- // APPEND TRACKED NODES
- //
- eachSelector(target, '[data-pjax-track]', appendTrackedNode)
-
- eachSelector(document, '[data-pjax-replace]', element => {
-
- element.replaceWith(target.getElementById(element.id))
-
- })
+ /**
+ * 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')
+ // console.log(window.performance.measure('time', 'start'))
+
+ const target = parse(store.snapshot(state.snapshot))
+
+ state.title = document.title = target?.title || ''
+
+ if (!popstate && state.history) {
+ if (state.url === state.location.lastpath) {
+ history.replace(state.location, state)
+ } else {
+ history.push(state.location, state)
+ }
+ }
- Object.keys(state.action).forEach(prop => {
+ if (target?.head) DOMHead(target.head)
- if (state.action[prop]) {
+ let fallback = 1
- forEach(state.action[prop], ([ from, to ]) => {
+ forEach(state?.replace
+ ? [ ...state.targets, ...state.replace ]
+ : state.targets, element => {
- const nodes = target.body.querySelectorAll(from)
- const frag = document.body.querySelector(to)
+ const node = target.body.querySelector(element)
- console.log(nodes)
- nodes.forEach(node => {
- frag.appendChild(node)
- dispatchEvent('pjax:render', { node }, true)
- })
+ return node ? document.body
+ .querySelectorAll(element)
+ .forEach(replaceTarget(node, state)) : fallback++
+ })
- })
+ if (fallback === state.targets.length) {
+ replaceTarget(target.body, state)(document.body)
}
- })
+ // APPEND TRACKED NODES
+ target.body
+ .querySelectorAll('[data-pjax-track]')
+ .forEach(appendTrackedNode)
- const fallback = 1
+ window.scrollTo(state.position.x, state.position.y)
- // REPLACE TARGETS
- //
- /* forEach(state.target, element => {
+ progress.done()
+ prefetch.start()
- const node = target.body.querySelector(element)
+ dispatchEvent('pjax:load', state)
- // if (node && node.hasAttribute('data-pjax-class')) setTargetClass(node)
+ // console.log(window.performance.measure('Render Time', 'render'))
+ // console.log(window.performance.measure('Total', 'started'))
- return node ? eachSelector(
- document,
- element,
- replaceTarget(node, element, 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)
}
- // SET SCROLL POSITION
- //
- // window.scrollTo(state.position.x, state.position.y)
+ return {
+
+ /* EXPORTS ------------------------------------ */
- console.log(store.dom)
+ update
+ , parse
+ , capture
- dispatchEvent('pjax:load', { state, target })
+ }
-}
+})()
diff --git a/src/app/request.js b/src/app/request.js
index abc76ba..b81d352 100644
--- a/src/app/request.js
+++ b/src/app/request.js
@@ -1,177 +1,225 @@
-import { store } from './store'
-import { asyncTimeout, byteConvert, byteSize, dispatchEvent } from './utils'
-import { xhrFailed, xhrPrevented, xhrSuccess, xhrEmpty, xhrExists } from '../constants/enums'
+import store from './store'
+import { byteConvert, byteSize, dispatchEvent } from './utils'
+import { progress } from './progress'
/**
- * 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`
- *
- * @param {string} DOMString
- * DOM string, equivelent to`document.documentElement.outerHTML`
+ * Requests Handler
*
- * @returns {void}
- * Executes asynchronously, to prevent any delayed between requests
+ * @param {boolean} connected
*/
-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)
- }
- })
+export default (function () {
+
+ /**
+ * @type {number}
+ */
+ let ratelimit = 0
+
+ /**
+ * @type {number}
+ */
+ let storage = 0
+
+ /**
+ * @type {boolean}
+ */
+ let showprogress = false
+
+ /**
+ * XHR Requests
+ *
+ * @type {Map}
+ */
+ 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))
+ }
- // console.log('cache size: ', store.request.cache.weight)
+ /**
+ * Executes on request end. Removes the XHR recrod and update
+ * the response DOMString cache size record.
+ *
+ * @param {string} url
+ * @param {string} DOMString
+ */
+ const HttpRequestEnd = (url, DOMString) => {
-}
+ transit.delete(url)
+ storage = storage + byteSize(DOMString)
-/**
- * Fetch XHR Request wrapper function
- *
- * @param {IPjax.IState} state
- * The `location.href`request address
- *
- * @return {Promise}
- * DOM string, equivelent to `document.documentElement.outerHTML`
- */
-function HttpRequest ({
- url,
- prefetch,
- target,
- location: {
- href
}
-}) {
- const xhr = new XMLHttpRequest()
+ /**
+ * Fetch XHR Request wrapper function
+ *
+ * @param {string} url
+ * @returns {Promise}
+ */
+ const HttpRequest = async (url) => {
- return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest()
- // OPEN
- //
- xhr.open('GET', href, true)
+ 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, store.config.request.async)
- // EVENTS
- //
- xhr.onloadstart = e => store.request.xhr.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
+ xhr.timeout = store.config.request.timeout
+ xhr.responseType = 'text'
- })
+ // SEND
+ //
+ xhr.send(null)
-}
+ })
-/**
- * Cancels the request in transit
- *
- * @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) {
+ }
- if (store.request.xhr.has(url)) {
+ /**
+ * Cancels the request in transit
+ *
+ * @exports
+ * @param {string} url
+ * @returns {void}
+ */
+ const cancel = (url) => {
- // ABORT AND REMOVE
- //
- store.request.xhr.get(url).abort()
- store.request.xhr.delete(url)
+ if (transit.has(url)) {
- console.info(`XHR Request was cancelled for url: ${url}`)
+ transit.get(url).abort()
+ transit.delete(url)
- } else {
- console.warn(`No in-flight request in transit 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.
- *
- * @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`
- */
-export async function inFlight (url, limit = 0) {
+ /**
+ * 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.timeout) {
+
+ if (!showprogress && (ratelimit * 10) === store.config.progress.threshold) {
+ progress.start()
+ showprogress = true
+ }
- if (store.request.xhr.has(url) && limit <= 85) {
- console.log('waiting', url, limit)
- return asyncTimeout(() => inFlight(url, (limit + 1)), 25)
- }
+ return asyncTimeout(() => {
+ ratelimit++
+ return inFlight(url)
+ }, 1)
- return !store.request.xhr.has(url)
+ }
-}
+ ratelimit = 0
+ showprogress = false
-/**
- * Fetches documents and guards from duplicated requests
- * from being dispatched if an indentical fetch is in flight.
- *
- * @param {IPjax.IState} state
- * The page state object acquired from link
- *
- * @return {Promise}
- * A boolean response representing a successful or failed fetch
- */
-export async function get (state) {
+ return !transit.has(url)
- if (store.request.xhr.has(state.url)) return xhrExists
- if (!dispatchEvent('pjax:request', state.location, true)) return xhrPrevented
+ }
+
+ /**
+ * 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
+ }
- try {
+ if (!dispatchEvent('pjax:request', state.url, true)) {
+ console.warn(`Pjax: Request cancelled via dispatched event for: ${state.url}`)
+ return false
+ }
- const snapshot = await HttpRequest(state)
+ try {
- if (typeof snapshot === 'string' && snapshot.length > 0) {
+ const response = await HttpRequest(state.url)
- state.snapshot = snapshot
+ if (typeof response === 'string') return store.create(state, response)
- if (store.config.cache && !store.cache.has(state.url)) {
- if (dispatchEvent('pjax:cache', state, true)) {
- store.cache.set(state.url, state)
- }
- }
+ console.warn(`Pjax: Failed to retrive response at: ${state.url}`)
+
+ } catch (error) {
- return xhrSuccess
+ transit.delete(state.url)
+ console.error(error)
- } else {
- console.info(`Pjax: Failed to receive response at: ${state.url}`)
- return xhrEmpty
}
- } catch (error) {
- store.request.xhr.delete(state.url)
- console.error(error)
+ return false
+
}
- return xhrFailed
+ return {
+
+ /* EXPORTS ------------------------------------ */
+
+ get
+ , cancel
+ , transit
+ , inFlight
+
+ /* GETTERS ------------------------------------ */
+
+ /**
+ * Returns request cache metrics
+ *
+ * @exports
+ * @returns {Store.ICacheSize}
+ */
+ , get cacheSize () {
+
+ return {
+ total: storage,
+ weight: byteConvert(storage)
+ }
+
+ }
+
+ }
-}
+})()
diff --git a/src/app/store.js b/src/app/store.js
index 68b870a..4ced087 100644
--- a/src/app/store.js
+++ b/src/app/store.js
@@ -1,245 +1,318 @@
import merge from 'mergerino'
+import history from 'history/browser'
+import { nanoid } from 'nanoid'
+import { dispatchEvent } from './utils'
+import scroll from '../observers/scroll'
+import * as nprogress from './progress'
+import render from './render'
+
+/**
+ * store
+ */
+export default ((config) => {
-export const store = (
+ /**
+ * Cache
+ *
+ * @exports
+ * @type {Map}
+ */
+ const cache = new Map()
/**
- * @param {IPjax.IStoreState} state
+ * Cache
+ *
+ * @exports
+ * @type {Map}
*/
- state => ({
+ const snapshots = new Map()
+
+ /**
+ * Preset Configuration
+ *
+ * @type {object}
+ */
+ let presets
+
+ return ({
/**
- * Connect Store
- *
- * MUST BE CALLED TO UPON INITIALIZATION
+ * 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.
*
- * @param {IPjax.IConfigPresets} options
+ * @param {Store.IPresets} [options]
*/
- connect (options) {
+ connect (options = {}) {
- this.update.config(options)
- this.update.page(this.config)
- this.update.dom()
- this.update.request()
+ presets = merge(config, {
+ // PRESETS PATCH COPY
+ ...options
+ , request: { ...options?.request, dispatch: undefined }
+ , cache: { ...options?.cache, save: undefined }
+ })
- }
+ nprogress.config(this.config.progress.options)
- ,
+ },
- /* -------------------------------------------- */
- /* STARTED */
- /* -------------------------------------------- */
+ /**
+ * 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 presets },
- get started () {
+ /**
+ * 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) {
- return state.started
+ const page = {
- }
+ // EDITABLE
+ //
+ history: true,
+ 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,
- ,
+ // USER OPTIONS
+ //
+ ...state,
- set started (status) {
+ // READ ONLY
+ //
+ targets: this.config.targets
- state.started = status
+ }
- }
+ if (page.cache && dispatchEvent('pjax:cache', page, true)) {
+ cache.set(page.url, page)
+ snapshots.set(page.snapshot, snapshot)
+ }
- ,
+ return page
- /* -------------------------------------------- */
- /* CACHE */
- /* -------------------------------------------- */
+ },
/**
- * @return {Map}
+ * Removes cached records. Optionally pass in URL
+ * to remove specific record.
+ *
+ * @param {string} id
*/
- get cache () {
+ snapshot: id => snapshots.get(id),
- return state.cache
+ /**
+ * 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()
+ }
+ },
- }
+ /**
+ * Check if cache record exists with snapshot
+ *
+ * @param {string} url
+ * @param {{snapshot?: boolean}} has
+ */
+ get: url => ({
+
+ /**
+ * @returns {Store.IPage}
+ */
+ get page () {
+ return cache.get(url)
+ },
+
+ /**
+ * @returns {string}
+ */
+ get snapshot () {
+ if (this.page?.snapshot) {
+ return snapshots.get(this.page.snapshot)
+ }
+ },
- ,
+ /**
+ * @returns {Document}
+ */
+ get target () {
+ return render.parse(this.snapshot)
+ }
- /* -------------------------------------------- */
- /* STORE GETTERS */
- /* -------------------------------------------- */
+ }),
/**
- * @return {IPjax.IConfigPresets}
+ * Map setters
*/
- get config () {
-
- return state.config
+ get set () {
+
+ return ({
+
+ /**
+ * @param {string} key
+ * @param {Store.IPage} value
+ */
+ cache: (key, value) => {
+ cache.set(key, value)
+ return value
+ },
+
+ /**
+ * @param {string} key
+ * @param {string} value
+ */
+ snapshots: (key, value) => {
+ snapshots.set(key, value)
+ return key
+ }
- }
+ })
- ,
+ },
/**
- * @return {IPjax.IState}
+ * Map Deletions
*/
- get page () {
+ get delete () {
- return state.page
+ return ({
- }
+ /**
+ * @param {string} url
+ */
+ cache: url => cache.delete(url),
- ,
+ /**
+ * @param {string} id
+ */
+ snapshots: id => snapshots.delete(id)
- /**
- * @return {IPjax.IDom}
- */
- get dom () {
+ })
- return state.dom
+ },
- }
+ /**
+ * Check if cache record exists with snapshot
+ *
+ * @param {string} url
+ * @param {{snapshot?: boolean}} has
+ * @return {boolean}
+ */
+ has: (url, has = { snapshot: false }) => (
- ,
+ !has.snapshot ? cache.has(url) : (
+ cache.has(url) || snapshots.has(cache.get(url)?.snapshot)
+ )
- /* -------------------------------------------- */
- /* REQUEST GETTER */
- /* -------------------------------------------- */
+ ),
/**
- * @return {IPjax.IRequest}
+ * Update current pushState History
+ *
+ * @param {string} url
+ * @returns {string}
*/
- get request () {
+ history: () => {
- return state.request
+ history.replace(history.location, {
+ ...history.location.state,
+ position: scroll.position
+ })
- }
+ // @ts-ignore
+ return history.location.state.url
- ,
-
- /* -------------------------------------------- */
- /* UPDATES */
- /* -------------------------------------------- */
-
- update: {
-
- /* CONFIG ------------------------------------- */
-
- config: (
- initial => patch => (
- state.config = merge(
- initial,
- patch
- )
- )
- )(
- {
- target: [
- 'main',
- '#navbar',
- '[script]'
- ],
- method: 'replace',
- prefetch: true,
- cache: true,
- throttle: 0,
- progress: false,
- threshold: {
- intersect: 250,
- hover: 100
- }
- }
- )
+ },
- ,
-
- /* PAGE --------------------------------------- */
-
- page: (
- initial => patch => (
- state.page = merge(
- state.page || initial,
- {
- ...patch,
- action: {
- append: null,
- prepend: null
- }
- }
- )
- )
- )(
- {
- url: '',
- snapshot: '',
- target: [],
- chunks: Object.create(null),
- method: 'replace',
- prefetch: 'intersect',
- action: {
- prepend: null,
- append: null
- },
- cache: null,
- progress: false,
- reload: false,
- throttle: 0,
- location: {
- protocol: '',
- origin: '',
- hostname: '',
- href: '',
- pathname: '',
- search: ''
- },
- position: {
- x: 0,
- y: 0
- }
- }
- )
+ /**
+ * 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 => (
- ,
-
- /* DOM ---------------------------------------- */
-
- dom: (
- initial => patch => (
- state.dom = merge(
- state.dom || initial,
- { ...patch, tracked: initial.tracked }
- )
- )
- )(
- {
- tracked: new Set(),
- head: Object.create(null)
- }
- )
+ cache
+ .set(state.url, merge(cache.get(state.url), state))
+ .get(state.url)
- ,
-
- 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()
+})({
+ targets: [ 'body' ],
+ request: {
+ timeout: 30000,
+ poll: 250,
+ 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: 850,
+ options: {
+ minimum: 0.10,
+ easing: 'ease',
+ speed: 225,
+ trickle: true,
+ trickleSpeed: 225,
+ showSpinner: false
}
- )
-)
+ }
+})
diff --git a/src/app/utils.js b/src/app/utils.js
index 2bc6955..de02464 100644
--- a/src/app/utils.js
+++ b/src/app/utils.js
@@ -1,10 +1,32 @@
-import { isNumber, ActionAttr, ActionParams } from '../constants/regexp'
+import { isNumber } from '../constants/regexp'
import { Units } from './../constants/common'
+import path from './path'
+import store from './store'
+
+/**
+ * Locted the closest link when click bubbles.
+ *
+ * @exports
+ * @param {EventTarget|MouseEvent} target
+ * @param {string} selector
+ * @return {Element|false}
+ */
+export function getLink (target, selector) {
+
+ if (target instanceof Element) {
+ const element = target.closest(selector)
+ if (element && element.tagName === 'A') return element
+ }
+
+ return false
+
+}
/**
* 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
@@ -24,7 +46,7 @@ import { Units } from './../constants/common'
* { string: 'foo', number: 200 }
*
*/
-export function jsonAttrs (accumulator, current, index, source) {
+export function jsonattrs (accumulator, current, index, source) {
return (index % 2 ? ({
...accumulator
@@ -38,11 +60,11 @@ export function jsonAttrs (accumulator, current, index, source) {
/**
* Array Chunk function
*
- * @export
- * @param {number} size
+ * @exports
+ * @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 ])
@@ -53,79 +75,29 @@ export function chunk (size) {
}
/**
- * Constructs a JSON object from HTML `data-pjax-*` attributes.
- * Attributes are passed in as array items
+ * Dispatches lifecycle events on the document.
*
- * @param {string} string
- * @return {object}
+ * @exports
+ * @param {Store.IEvents} eventName
+ * @param {Element} target
+ * @return {boolean}
*/
-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), [])
- }
- }
+export function targetedEvent (eventName, target) {
- return string
- .match(ActionAttr)
- .reduce(actions, {})
+ // create and dispatch the event
+ const newEvent = new CustomEvent(eventName, { cancelable: true })
-}
+ return target.dispatchEvent(newEvent)
-/**
- * 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.
*
- * @export
- *
- * @param {IPjax.IEvents} eventName
- * The event name to be created
- *
+ * @exports
+ * @param {Store.IEvents} eventName
* @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) {
@@ -140,18 +112,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 !store.has(path.get(target).url, { snapshot: true })
+}
+
+/**
+ * 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) {
@@ -166,39 +167,17 @@ export function byteConvert (bytes) {
)
}
-/**
- * Async Timeout
- *
- * @param {function} callback
- * @param {number} ms
- */
-export function asyncTimeout (callback, ms = 0) {
-
- return new Promise(
- resolve => setTimeout(() => {
- const response = callback()
- return resolve(response)
- }, ms)
- )
-}
-
/**
* 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 } = {}) {
-
let i = list.length - 1
for (; i >= 0; i--) index ? fn(list[i], i) : fn(list[i])
}
@@ -206,12 +185,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 = []) {
@@ -228,21 +205,3 @@ export function getElementAttrs ({ attributes }, exclude = []) {
]
) : accumulator, [])
}
-
-/**
- * Each Selector
- *
- * @param {Document} document
- * The document Element
- *
- * @param {string} query
- * The element selector
- *
- * @param {(element: Element) => void} callback
- * The callback function
- */
-export function eachSelector ({ body }, query, callback) {
-
- return [].slice.call(body.querySelectorAll(query)).forEach(callback)
-
-}
diff --git a/src/constants/common.js b/src/constants/common.js
index 1555a82..e9f977b 100644
--- a/src/constants/common.js
+++ b/src/constants/common.js
@@ -1,39 +1,40 @@
-/**
- * Array Slice Prototype
- */
-export const ArraySlice = Array.prototype.slice
-
-/**
- * Document Implentation
- */
-export const Implementation = document.implementation
/**
* 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])'
/**
- * DOM Parse
- */
-export const DomParser = new DOMParser()
-
-/**
- * Units
+ * Units Array used for cache size
+ *
+ * @exports
+ * @type {string[]}
*/
export const Units = [ 'B', 'KB', 'MB', 'GB', 'TB' ]
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/constants/regexp.js b/src/constants/regexp.js
index 8a412b3..c117739 100644
--- a/src/constants/regexp.js
+++ b/src/constants/regexp.js
@@ -1,7 +1,40 @@
+/**
+ * Attribute Configuration
+ *
+ * Used to match Pjax data attribute names
+ *
+ * @exports
+ * @type {RegExp}
+ */
+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
+ *
+ * Used to match first pathname from a URL (group 1)
+ *
+ * @exports
+ * @type {RegExp}
+ */
+export const Pathname = /\/\/[^/]*(\/[^;]*)/
+
/**
* Form Inputs
*
* Used to match Form Input elements
+ *
+ * @exports
+ * @type {RegExp}
*/
export const FormInputs = /^(input|textarea|select|datalist|button|output)$/i
@@ -9,20 +42,29 @@ 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)$/i
/**
* Boolean Attribute value
*
* Used to Match 'true' or 'false' attribute
+ *
+ * @exports
+ * @type {RegExp}
*/
-export const isBoolean = /\b(true|false)\b/
+export const isBoolean = /^(true|false)$/i
/**
* Matches decimal number
*
* Used to Match number, respected negative numbers
+ *
+ * @exports
+ * @type {RegExp}
*/
export const isNumber = /^[+-]?\d*\.?\d+$/
@@ -30,55 +72,140 @@ 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
+ * Cache Attribute
*
- * Used to match the event caller for attribute actions
+ * Used to match and validate a cache attribute config
*
+ * @exports
+ * @type {RegExp}
*/
-export const ActionAttr = /\b(?:append|prepend|replace)/g
+export const isCache = /\b(?:false|true|reset|flush)\b/
+
+/**
+ * Threshold Attribute Value
+ *
+ * Used to match threshold JSON attributes
+ *
+ * @exports
+ * @type {RegExp}
+ */
+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
+/**
+ * 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
*
* Used to match `x:0` and `y:0` JSON space separated attributes
+ *
+ * @exports
+ * @type {RegExp}
*/
-export const inPosition = /[xy]|[0-9]+/g
+export const inPosition = /[xy]|\d*\.?\d+/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/index.js b/src/index.js
index 63c8787..b54bf68 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,10 @@
-import { Protocol, isReady } from './constants/regexp'
-import { store } from './app/store'
-import * as controller from './app/controller'
+import { Protocol } from './constants/regexp'
+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 controller from './app/controller'
/**
* @export
@@ -9,13 +13,14 @@ import * as controller from './app/controller'
export const supported = !!(
window.history.pushState &&
window.requestAnimationFrame &&
- window.addEventListener
+ window.addEventListener &&
+ window.DOMParser
)
/**
* Connect Pjax
*
- * @param {IPjax.IConfigPresets} options
+ * @param {Store.IPresets} options
*/
export const connect = options => {
@@ -23,7 +28,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 +43,41 @@ 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
+ *
+ * @param {string} [url]
+ */
+export const clear = (url) => store.clear(url)
+
+/**
+ * Capture DOM
+ *
+ * @param {string} url
+ * @param {object} action
+ */
+export const capture = (url, action) => render.captureDOM(path.key(url), action)
/**
* Visit
*
- * @param {IPjax.IState} state
+ * @param {string|Element} link
+ * @param {Store.IPage} state
+ * @returns {Promise}
*/
-export const visit = state => controller.navigate(state)
+export const visit = (link, state = {}) => {
+
+ const { url, location } = path.get(link, { update: true })
+
+ return hrefs.navigate(url, { ...state, url, location })
+}
/**
* Disconnect
diff --git a/src/observers/history.js b/src/observers/history.js
new file mode 100644
index 0000000..74ab9ea
--- /dev/null
+++ b/src/observers/history.js
@@ -0,0 +1,124 @@
+import history from 'history/browser'
+import { createPath } from 'history'
+import render from '../app/render'
+import request from '../app/request'
+import store from '../app/store'
+import scroll from './scroll'
+
+/* -------------------------------------------- */
+/* FUNCTIONS */
+/* -------------------------------------------- */
+/**
+ * Link (href) handler
+ *
+ * @typedef {Store.IPage|string|boolean} click
+ * @param {boolean} connected
+ */
+export default (function (connected) {
+
+ /**
+ * @type {function}
+ */
+ let unlisten = null
+
+ /**
+ * @type {string}
+ */
+ let inTransit = null
+
+ /**
+ * Popstate Navigation
+ *
+ * @param {string} url
+ * @param {Store.IPage} state
+ * @returns {Promise}
+ */
+ const popstate = async (url, state) => {
+
+ // console.log(state)
+
+ if (url !== inTransit) request.cancel(inTransit)
+
+ if (store.has(url, { snapshot: true })) {
+ return render.update(store.get(url).page, true)
+ }
+
+ inTransit = url
+
+ const page = await request.get(state)
+
+ return page
+ ? render.update(page, true)
+ : location.assign(url)
+
+ }
+
+ /**
+ * Event History dispatch controller, handles popstate,
+ * push and replace events via third party module
+ *
+ * @param {import('history').BrowserHistory} event
+ */
+ const listener = ({ action, location }) => {
+
+ // console.log(action, location)
+ if (action === 'POP') {
+ return popstate(createPath(location), location.state)
+ }
+
+ }
+
+ 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 ----------------------------------- */
+
+ /**
+ * 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
+ }
+ }
+
+ }
+
+})(false)
diff --git a/src/observers/hover.js b/src/observers/hover.js
new file mode 100644
index 0000000..0479d44
--- /dev/null
+++ b/src/observers/hover.js
@@ -0,0 +1,267 @@
+import { supportsPointerEvents } from 'detect-it'
+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'
+/**
+ * Link (href) handler
+ *
+ * @typedef {string|Store.IPage} clickState
+ * @param {boolean} connected
+ */
+export default (function (connected) {
+
+ /**
+ * @exports
+ * @type {Map}
+ */
+ const transit = new Map()
+
+ /**
+ * @type {Store.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
+ */
+ const onMouseleave = (event) => {
+
+ const target = getLink(event.target, LinkPrefetchHover)
+
+ if (target) {
+
+ cleanup(path.get(target).url)
+ handleLeave(target)
+ }
+
+ }
+
+ /**
+ * 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, location } = path.get(target)
+
+ if (!dispatchEvent('pjax:prefetch', {
+ target,
+ url,
+ location
+ }, true)) return disconnect(target)
+
+ if (store.has(url, { snapshot: true })) return disconnect(target)
+
+ handleLeave(target)
+
+ const state = hrefs.attrparse(target, {
+ url,
+ location,
+ position: scroll.y0x0
+ })
+
+ throttle(url, async () => {
+
+ if ((await prefetch(state))) handleLeave(target)
+
+ }, 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)
+
+ 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)
+ }
+ }
+
+ /**
+ * Adds and/or Removes click events.
+ *
+ * @param {EventTarget} target
+ * @returns {void}
+ */
+ function disconnect (target) {
+
+ if (supportsPointerEvents) {
+ target.removeEventListener('pointerover', onMouseleave, false)
+ target.removeEventListener('pointerout', onMouseleave, false)
+ } else {
+ target.removeEventListener('mouseleave', onMouseleave, false)
+ target.removeEventListener('mouseover', onMouseover, false)
+ }
+
+ }
+
+ return {
+
+ /* CONTROLS ----------------------------------- */
+
+ /**
+ * 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 9f1a410..5c08e33 100644
--- a/src/observers/hrefs.js
+++ b/src/observers/hrefs.js
@@ -1,233 +1,263 @@
-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 { supportsPointerEvents } from 'detect-it'
+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 history from './history'
/**
- * @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
- *
- * @export
+ * Link (href) handler
*
- * @param {Element} target
- * The link `href` element target
+ * @typedef {string|Store.IPage} clickState
+ * @param {boolean} connected
*/
-function getLocation (target) {
+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)
- const location = expandURL(target.getAttribute('href'))
-
- return {
- location,
- url: location.pathname + location.search
}
-}
-/**
- * Get State Page
- *
- *
- * @param {Element} target
- * The link `href` element target
- *
- * @return {IPjax.IState}
- * Returns an updated page state object
- */
-function getPageState (target) {
+ /**
+ * 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)
- 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)
+ /**
+ * Executes a pjax navigation.
+ *
+ * @param {string} url
+ * @param {Store.IPage|false} [state=false]
+ * @export
+ */
+ const navigate = async (url, state = false) => {
+
+ if (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)
-/**
- * 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
- *
- * @return {IPjax.IState}
- * Returns an updated page state object
- */
-export function visitState (target, isPrefetch = false) {
+ } else {
- if (isPrefetch === false) getActiveDOM(store.page.url)
+ if ((await request.inFlight(url))) {
+ return render.update(store.get(url).page)
+ } else {
+ request.cancel(url)
+ }
+ }
- const state = getPageState(target)
+ return location.assign(url)
- forEach(attrs, prop => {
+ }
- const value = target.getAttribute(`data-pjax-${prop}`)
+ /**
+ * 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) {
- if (value === null) {
+ event.preventDefault()
+ target.removeEventListener('click', click, false)
+ render.capture(history.updateState) // PRESERVE CURRENT PAGE
+ prefetch.stop()
- if (
- prop === 'prefetch' &&
- value !== 'hover' &&
- value !== 'intersect') state[prop] = false
+ return typeof state === 'object'
+ ? render.update(state)
+ : typeof state === 'string'
+ ? navigate(state)
+ : location.assign(path.url)
- } else {
+ }
- state[prop] = prop === 'target' ? (
+ /**
+ * Triggers a page fetch
+ *
+ * @param {MouseEvent} event
+ * @returns {void}
+ */
+ const handleTrigger = (event) => {
- value.split(regexp.isWhitespace)
+ window.performance.mark('start')
- ) : (prop === 'position' || prop === 'threshold') ? (
+ if (!linkEvent(event)) return undefined
- value.match(regexp.inPosition).reduce(jsonAttrs, {})
+ const target = getLink(event.target, Link)
- ) : regexp.isBoolean.test(value.trim()) ? (
+ if (!target) return undefined
+ if (!dispatchEvent('pjax:trigger', { target }, true)) return undefined
- value === 'true'
+ const { url, location } = path.get(target, { update: true })
+ const click = handleClick(target)
- ) : prop === 'action' ? (
+ if (request.transit.has(url)) {
- actionAttrs(value)
+ target.addEventListener('click', click(url), false)
- ) : regexp.isNumber.test(value.trim()) ? (
+ } else {
- Number(value)
+ const state = attrparse(target, { url, location, position: scroll.y0x0 })
- ) : (
+ if (store.has(url, { snapshot: true })) {
+ target.addEventListener('click', click(store.update(state)), false)
+ } else {
+ request.get(state) // TRIGGERS FETCH
+ target.addEventListener('click', click(url), false)
+ }
+ }
- value.trim()
+ }
- )
+ return {
- }
- })
+ /* EXPORTS ------------------------------------ */
- return state
+ attrparse
+ , navigate
-}
+ /* CONTROLS ----------------------------------- */
-/**
- * Attempts to visit href location, Handles click bubbles and
- * Dispatches a `pjax:click` event respecting the cancelable
- * `preventDefault()` from user event
- *
- * @param {MouseEvent} event
- */
-function visitOnClick (event) {
+ /**
+ * Attached `click` event listener.
+ *
+ * @returns {void}
+ */
+ , start: () => {
- if (linkEventValidate(event)) {
+ if (!connected) {
- event.preventDefault()
+ if (supportsPointerEvents) {
+ addEventListener('pointerdown', handleTrigger, false)
+ } else {
+ addEventListener('mousedown', handleTrigger, false)
+ addEventListener('touchstart', handleTrigger, false)
+ }
- const target = linkLocator(event.target, Link)
+ connected = true
- if (target) {
- if (target.tagName === 'A') {
- const state = visitState(target, false)
- if (dispatchEvent('pjax:click', state, true)) return navigate(state)
}
}
- }
-}
-
-/**
- * Adds and/or Removes click events.
- *
- * @private
- */
-function captureClick () {
- removeEventListener('click', visitOnClick, false)
- addEventListener('click', visitOnClick, false)
+ /**
+ * Removed `click` event listener.
+ *
+ * @returns {void}
+ */
+ , stop: () => {
-}
-
-/**
- * Attached `click` event listener.
- *
- * @export
- */
-export function start () {
+ if (connected) {
- if (!started) {
- addEventListener('click', captureClick, true)
- started = true
- }
+ if (supportsPointerEvents) {
+ removeEventListener('pointerdown', handleTrigger, false)
+ } else {
+ removeEventListener('mousedown', handleTrigger, false)
+ removeEventListener('touchstart', handleTrigger, false)
+ }
-}
+ connected = false
-/**
- * Removed `click` event listener.
- *
- * @export
- */
-export function stop () {
+ }
+ }
- if (started) {
- removeEventListener('click', captureClick, true)
- started = false
}
-}
+})(false)
diff --git a/src/observers/intersect.js b/src/observers/intersect.js
new file mode 100644
index 0000000..49d79a0
--- /dev/null
+++ b/src/observers/intersect.js
@@ -0,0 +1,95 @@
+import { LinkPrefetchIntersect } from '../constants/common'
+import { getTargets } from '../app/utils'
+import hrefs from './hrefs'
+import path from '../app/path'
+import request from '../app/request'
+
+/**
+ * @param {boolean} connect
+ */
+export default (function (connect) {
+
+ /**
+ * @type IntersectionObserver
+ */
+ let entries = null
+
+ /**
+ * Intersection callback when entries are in viewport.
+ *
+ * @param {IntersectionObserverEntry} params
+ * @returns {Promise}
+ */
+ const onIntersect = async ({ isIntersecting, target }) => {
+
+ if (isIntersecting) {
+
+ const state = hrefs.attrparse(target, path.get(target))
+ 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)
+ }
+
+ }
+ }
+
+ /**
+ * 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 {
+
+ /* CONTROLS ----------------------------------- */
+
+ /**
+ * 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
+ }
+
+ }
+
+ }
+
+})(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/scroll.js b/src/observers/scroll.js
new file mode 100644
index 0000000..e40883d
--- /dev/null
+++ b/src/observers/scroll.js
@@ -0,0 +1,98 @@
+/**
+ * Scroll position handler
+ *
+ * @param {boolean} connected
+ */
+export default (function (connected) {
+
+ /**
+ * @type {Store.IPosition}
+ */
+ let position = { x: 0, y: 0 }
+
+ /**
+ * Resets the scroll position`of the document, applying
+ * a `x`and `y` positions to `0`
+ *
+ * @exports
+ * @returns {Store.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 {
+
+ /* EXPORTS ------------------------------------ */
+
+ reset,
+
+ /* GETTERS ------------------------------------ */
+
+ /**
+ * Returns to current scroll position, the `reset()`
+ * function **MUST** be called after referencing this
+ * to reset position.
+ *
+ * @exports
+ * @returns {Store.IPosition}
+ */
+ get position () { return position },
+
+ /**
+ * Returns a faux scroll position. This prevents the
+ * tracked scroll position from being overwritten and is
+ * used within functions like `href.attrparse`
+ *
+ * @returns {Store.IPosition}
+ */
+ get y0x0 () { return { x: 0, y: 0 } },
+
+ /* CONTROLS ----------------------------------- */
+
+ /**
+ * 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/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": [
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000..f2b4d14
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,50 @@
+import { IPage, IPresets } from "./store";
+
+declare module "@brixtol/pjax" {
+ /**
+ * Pjax Support
+ */
+ export const supported: boolean;
+
+ /**
+ * Fetches state page by url. Pass `false` to clear cache
+ */
+ export function connect(options?: IPresets): IPage;
+
+ /**
+ * Fetches state page by url. Pass `false` to clear cache
+ */
+ 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;
+
+ /**
+ * 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): string;
+
+ /**
+ * Programmatic visit to location
+ */
+ export function visit(url: string, options?: IPage): Promise;
+
+ /**
+ * 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..f0b673c 100644
--- a/types/store.d.ts
+++ b/types/store.d.ts
@@ -1,166 +1,424 @@
-/**
- * Pjax Events
- */
-export type IEvents = (
- 'pjax:click' |
- 'pjax:request' |
- 'pjax:cache' |
- 'pjax:render' |
- 'pjax:load'
-)
+import { PartialPath } from "history";
/**
- * Action to be executed on navigation.
- */
-export type IConfigMethod= 'replace' | 'prepend' | 'append'
-
-/**
- * Prefetch operation on navigation
+ * Pjax Events
*/
-export type IConfigPrefetch = 'intersect' | 'hover'
+export type IEvents =
+ | "pjax:prefetch"
+ | "pjax:trigger"
+ | "pjax:click"
+ | "pjax:request"
+ | "pjax:cache"
+ | "pjax:render"
+ | "pjax:load";
/**
- * Cache operation on navigation
+ * Cache Size
*/
-export type IConfigCache = 'false' | 'reset' | 'save'
+export type ICacheSize = {
+ total: number;
+ weight: string;
+};
/**
* Scroll position records
*/
export type IPosition = {
- x: number,
- y: number
-}
+ x: number;
+ y: number;
+};
/**
* The URL location object
*/
-export interface ILocation {
-
- /**
- * The URL protocol
- *
- * @example
- * 'https:' OR 'http:'
- */
- protocol: string
+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;
-export type IConfigPresets = {
/**
- * List of target element selectors. Accepts any valid
- * `querySelector()` string.
+ * The previous page path URL, this is also the cache identifier
*
* @example
- * ['#main', '.header', '[data-attr]', 'header']
+ * '/pathname' OR '/pathname?foo=bar'
+ */
+ lastpath?: string;
+}
+
+/**
+ * NProgress Exposed Configuration Options
+ */
+export interface IProgress {
+ /**
+ * Changes the minimum percentage used upon starting.
+ *
+ * @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;
/**
- * Enable or Disable progres bar indicator
+ * Adjust how often to trickle/increment, in ms.
*
- * (_Requests are instantaneous, generally you wont need this_)
+ * @default 200
+ */
+ trickleSpeed?: number;
+ /**
+ * Turn on loading spinner by setting it to `true`
*
* @default false
*/
- progress?: boolean,
+ 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']
+ */
+ targets?: string[];
+
+ /**
+ * Request Configuration
+ */
+ 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;
+
+ /**
+ * Determin if page requests should be fetched asynchronously or synchronously.
+ * Setting this to `false` is not reccomended.
+ *
+ * ---
+ * @default true
+ */
+ async?: boolean;
+
+ /**
+ * **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
*/
- throttle?: number
+ 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?: {
+ /**
+ * 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;
+
+ /**
+ * 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
+ * List of fragment element selectors. Accepts any valid
* `querySelector()` string.
*
* @example
* ['#main', '.header', '[data-attr]', 'header']
*/
- target?: string[]
+ replace?: null | 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?: 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`
*
@@ -170,23 +428,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.
+ * Passing in __reset__ will remove the requested page from cache
+ * (if it exsists) and the next navigation result will be saved.
*
- * `save`
+ * `clear`
*
- * 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._
+ * 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.
@@ -196,151 +446,30 @@ export interface IConfig {
*
* `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?: string
+ 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
+ proximity?: number;
/**
- * Enable or Disable progres bar indicator
- *
- * (_Requests are instantaneous, generally you wont need this_)
+ * Progress bar threshold delay
*
- * @default false
- */
- progress?: boolean,
-
-}
-
-export interface IDom {
- readonly tracked?: Set,
- head?: object
-}
-
-
-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
-
- /**
- * The fetched HTML response string
- */
- chunks?: {
- [selector: string]: 'replace' | 'prepend' | 'append'
- }
-
- /**
- * The URL cache key
- */
- url?: string
-
- /**
- * Location URL
+ * @default 350
*/
- location?: ILocation
-
- /**
- * Action
- */
- action?: {
- append?: Array<[from: string, to: string]>,
- prepend?: Array<[from: string, to: string]>,
- }
-
- /**
- * Threshold
- */
- threshold?: {
- intersect?: 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;