diff --git a/CHANGELOG.md b/CHANGELOG.md
index e830754d..cf4b6d94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 3.40.0
+
+- Add FakeXMLHttpRequest.
+- Change `fetch.rewrite` option.
+- Change `update.rewrite` option.
+
## 3.39.1
- Fix noscript parsing.
diff --git a/README.md b/README.md
index 97816765..926f24a4 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,8 @@ Most SPA frameworks and pjax libraries lack many essential functions to keep the
|Execution sequence keeping| | |✓| |
|Non-blocking script load|✓|✓|✓| |
|**Subresource integrity verification**| | |✓\*1| |
-|Lightweight source rewrite| |✓|✓| |
+|**Rewrite request and response (XHR)**| | |✓| |
+|Rewrite source document| |✓|✓| |
|ETag support| | |✓| |
|Cache|✓|✓|✓|✓|
|URL scope| | |✓|✓|
@@ -43,7 +44,7 @@ Most SPA frameworks and pjax libraries lack many essential functions to keep the
|**Browser history fix**| | |✓| |
|**Scroll position restoration**| | |✓| |
|**Unexpected scroll prevention**| | |✓| |
-|NOSCRIPT tag restoration| | |✓| |
+|NOSCRIPT tag fix| | |✓| |
|History API support\*2| | |✓| |
|No jQuery dependency| | |✓|✓|
diff --git a/gh-pages/_includes/nav.html b/gh-pages/_includes/nav.html
index c4731478..76791540 100644
--- a/gh-pages/_includes/nav.html
+++ b/gh-pages/_includes/nav.html
@@ -6,8 +6,13 @@
APIs
diff --git a/gh-pages/docs/apis/index.md b/gh-pages/docs/apis/index.md
index f5013468..09893bfd 100644
--- a/gh-pages/docs/apis/index.md
+++ b/gh-pages/docs/apis/index.md
@@ -15,3 +15,7 @@ Pjax APIs.
## [Events]({{ site.basepath }}docs/apis/events/)
Global events.
+
+## [Util]({{ site.basepath }}docs/apis/util/)
+
+Utilities.
diff --git a/gh-pages/docs/apis/pjax/config/index.md b/gh-pages/docs/apis/pjax/config/index.md
index fb6f2fb5..bccdd63d 100644
--- a/gh-pages/docs/apis/pjax/config/index.md
+++ b/gh-pages/docs/apis/pjax/config/index.md
@@ -65,7 +65,7 @@ Set a dictionary object having has/get/set/delete methods of Map to pass the doc
## fetch: {...} = ...
-### rewrite: (path: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest
+### rewrite: (url: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest | undefined
Rewrite the XHR object, or replace it with another or fake.
@@ -79,7 +79,7 @@ Wait for the specified milliseconds after sending a request.
## update: {...} = ...
-### rewrite: (doc: Document, area: string, memory?: Document) => void = `() => undefined`
+### rewrite: (url: string, document: Document, area: string, cache?: Document) => void = `() => undefined`
Rewrite the source document object.
If you use the sequence option, you should use only it instead of this.
diff --git a/gh-pages/docs/apis/util/index.md b/gh-pages/docs/apis/util/index.md
new file mode 100644
index 00000000..7dd333a0
--- /dev/null
+++ b/gh-pages/docs/apis/util/index.md
@@ -0,0 +1,31 @@
+---
+layout: layout
+title: Util
+type: page
+nav: nav
+class: style-api style-api-detail
+---
+
+# Util
+
+## FakeXMLHttpRequest
+
+Make a fake XHR object that doesn't send a request.
+
+```ts
+import Pjax, { FakeXMLHttpRequest } from 'pjax-api';
+
+new Pjax({
+ fetch: {
+ rewrite: url =>
+ FakeXMLHttpRequest.create(
+ url,
+ fetch(url, { headers: { 'Content-Type': 'application/json' } })
+ .then(res => res.json())
+ .then(data =>
+ new DOMParser().parseFromString(
+ `${data.title}${data.body}`,
+ 'text/html'))),
+ },
+});
+```
diff --git a/gh-pages/index.md b/gh-pages/index.md
index fc937b42..d59f0f03 100644
--- a/gh-pages/index.md
+++ b/gh-pages/index.md
@@ -29,7 +29,9 @@ This site is also powered by PJAX as a demo. Try page transitions.
- [APIs]({{ site.basepath }}docs/apis/)
- [Pjax]({{ site.basepath }}docs/apis/pjax/)
+ - [Config]({{ site.basepath }}docs/apis/pjax/config/)
- [Events]({{ site.basepath }}docs/apis/events/)
+ - [Util]({{ site.basepath }}docs/apis/util/)
diff --git a/index.d.ts b/index.d.ts
index 4fc39b64..1fd6855b 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -23,13 +23,13 @@ export interface Config {
readonly cache?: Dict
;
readonly memory?: Dict;
readonly fetch?: {
- readonly rewrite?: (path: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest;
+ readonly rewrite?: (url: string, method: string, headers: Headers, timeout: number, body: FormData | null) => XMLHttpRequest | undefined;
readonly headers?: Headers;
readonly timeout?: number;
readonly wait?: number;
};
readonly update?: {
- readonly rewrite?: (path: string, doc: Document, area: string, memory: Document | undefined) => void;
+ readonly rewrite?: (url: string, document: Document, area: string, cache: Document | undefined) => void;
readonly head?: string;
readonly css?: boolean;
readonly script?: boolean;
@@ -63,3 +63,7 @@ declare global {
}
}
+
+export class FakeXMLHttpRequest extends XMLHttpRequest {
+ public static create(url: string, response: Document | PromiseLike): FakeXMLHttpRequest;
+}
diff --git a/src/export.ts b/src/export.ts
index 512130e0..1c793b6a 100644
--- a/src/export.ts
+++ b/src/export.ts
@@ -1 +1,2 @@
export { GUI as Pjax, GUI as default } from './layer/interface/service/gui';
+export { FakeXMLHttpRequest } from './lib/xhr';
diff --git a/src/layer/domain/data/config.ts b/src/layer/domain/data/config.ts
index bcddf1c2..7cd23e2c 100644
--- a/src/layer/domain/data/config.ts
+++ b/src/layer/domain/data/config.ts
@@ -54,7 +54,7 @@ export class Config implements Option {
wait: 0,
};
public readonly update = {
- rewrite: (_path: string, _doc: Document, _area: string, _memory: Document | undefined): void => undefined,
+ rewrite: (_url: string, _document: Document, _area: string, _cache: Document | undefined): void => undefined,
head: 'base, meta, link',
css: true,
script: true,
diff --git a/src/layer/domain/router/module/fetch/xhr.ts b/src/layer/domain/router/module/fetch/xhr.ts
index 391e5919..a52f91b0 100644
--- a/src/layer/domain/router/module/fetch/xhr.ts
+++ b/src/layer/domain/router/module/fetch/xhr.ts
@@ -25,7 +25,8 @@ export function xhr(
headers.set('If-None-Match', cache.get(displayURL.path)!.etag);
}
return new AtomicPromise>(resolve => {
- const xhr = rewrite(displayURL.path, method, headers, timeout, body);
+ const xhr = rewrite(displayURL.href, method, headers, timeout, body) ??
+ request(displayURL.href, method, headers, timeout, body);
if (xhr.responseType !== 'document') throw new Error(`Response type must be 'document'`);
@@ -77,14 +78,14 @@ export function xhr(
}
function request(
- path: URL.Path,
+ url: URL.Href,
method: RouterEventMethod,
headers: Headers,
timeout: number,
body: FormData | null,
): XMLHttpRequest {
const xhr = new XMLHttpRequest();
- xhr.open(method, path, true);
+ xhr.open(method, url, true);
for (const [name, value] of headers) {
xhr.setRequestHeader(name, value);
}
diff --git a/src/layer/domain/router/module/update.ts b/src/layer/domain/router/module/update.ts
index bd68f11d..22967fd4 100644
--- a/src/layer/domain/router/module/update.ts
+++ b/src/layer/domain/router/module/update.ts
@@ -54,7 +54,7 @@ export function update(
? config.memory?.get(event.location.dest.path)
: undefined;
config.update.rewrite(
- event.location.dest.path,
+ event.location.dest.href,
documents.src,
area,
memory && separate({ src: memory, dst: documents.dst }, [area]).extract(() => false)
diff --git a/src/lib/xhr.ts b/src/lib/xhr.ts
new file mode 100644
index 00000000..4824da2c
--- /dev/null
+++ b/src/lib/xhr.ts
@@ -0,0 +1,58 @@
+import { AtomicPromise } from 'spica/promise';
+
+export class FakeXMLHttpRequest extends XMLHttpRequest {
+ public static create(url: string, response: Document | PromiseLike): FakeXMLHttpRequest {
+ const xhr = new FakeXMLHttpRequest();
+ AtomicPromise.resolve(response)
+ .then(response => {
+ Object.defineProperties(xhr, {
+ responseURL: {
+ value: url,
+ },
+ responseXML: {
+ value: response,
+ },
+ });
+ xhr.send();
+ });
+ return xhr;
+ }
+ constructor() {
+ super();
+ this.responseType = 'document';
+ }
+ public override send(_?: Document | XMLHttpRequestBodyInit | null | undefined): void {
+ let state = 3;
+ Object.defineProperties(this, {
+ readyState: {
+ get: () => state,
+ },
+ status: {
+ value: 200,
+ },
+ statusText: {
+ value: 'OK',
+ },
+ response: {
+ get: () =>
+ this.responseType === 'document'
+ ? this.responseXML
+ : this.responseText,
+ },
+ })
+ setTimeout(() => {
+ this.dispatchEvent(new ProgressEvent('loadstart'));
+ state = 4;
+ this.dispatchEvent(new ProgressEvent('loadend'));
+ this.dispatchEvent(new ProgressEvent('load'));
+ });
+ }
+ public override getResponseHeader(name: string): string | null {
+ switch (name.toLowerCase()) {
+ case 'content-type':
+ return 'text/html';
+ default:
+ return null;
+ }
+ }
+}
diff --git a/test/integration/config/fetch.rewrite.test.ts b/test/integration/config/fetch.rewrite.test.ts
index 3900682c..ebdc7cc5 100644
--- a/test/integration/config/fetch.rewrite.test.ts
+++ b/test/integration/config/fetch.rewrite.test.ts
@@ -1,6 +1,7 @@
-import { Pjax } from '../../../index';
+import { Pjax, FakeXMLHttpRequest } from '../../../index';
import { route as router } from '../../../src/layer/interface/service/router';
import { parse } from '../../../src/lib/html';
+import { wait } from 'spica/timer';
import { once } from 'typed-dom';
describe('Integration: Config', function () {
@@ -10,33 +11,18 @@ describe('Integration: Config', function () {
describe('fetch.rewrite', function () {
it('', function (done) {
- const FakeXMLHttpRequest = XMLHttpRequest;
const url = '/base/test/integration/fixture/basic/1.html';
const document = parse('').extract();
new Pjax({
fetch: {
- rewrite: (path, method, headers, timeout, body) => {
- const xhr = new FakeXMLHttpRequest();
- xhr.open(method, path, true);
- for (const [name, value] of headers) {
- xhr.setRequestHeader(name, value);
- }
-
- xhr.responseType = 'document';
- xhr.timeout = timeout;
- xhr.send(body);
-
- Object.defineProperties(xhr, {
- responseURL: {
- value: url,
- },
- responseXML: {
- value: parse('Title 2Primary 2
').extract(),
- },
- });
- return xhr;
- },
- }
+ rewrite: url =>
+ FakeXMLHttpRequest.create(
+ url,
+ wait(100).then(() =>
+ new DOMParser().parseFromString(
+ 'Title 2Primary 2
',
+ 'text/html'))),
+ },
}, { document, router })
.assign(url);
once(document, 'pjax:ready', () => {
diff --git a/test/integration/config/update.rewrite.test.ts b/test/integration/config/update.rewrite.test.ts
index bb576ad7..c44f2e34 100644
--- a/test/integration/config/update.rewrite.test.ts
+++ b/test/integration/config/update.rewrite.test.ts
@@ -16,13 +16,13 @@ describe('Integration: Config', function () {
new Pjax({
memory: new Cache(100),
update: {
- rewrite(path, doc, area, memory) {
- switch (path) {
- case url:
- memory && doc.querySelector(area)?.replaceWith(memory.querySelector(area)!.cloneNode(true));
+ rewrite(url, doc, area, cache) {
+ switch (url.split('/').at(-1)) {
+ case '2.html':
+ cache && doc.querySelector(area)?.replaceWith(cache.querySelector(area)!.cloneNode(true));
}
},
- }
+ },
}, { document, router })
.assign(url);
once(document, 'pjax:ready', () => {
@@ -32,7 +32,8 @@ describe('Integration: Config', function () {
document.querySelector('#primary')!.textContent = 'PRIMARY 2';
once(document, 'pjax:ready', () => {
assert(window.location.pathname !== url);
- assert(document.title !== 'Title 2');
+ assert(document.title === 'Title 1');
+ assert(document.querySelector('#primary')!.textContent === 'Primary 1');
once(document, 'pjax:ready', () => {
assert(window.location.pathname === url);
assert(document.title === 'Title 2');
diff --git a/test/interface/index.test.ts b/test/interface/index.test.ts
index 9b944d7b..f3feaec5 100644
--- a/test/interface/index.test.ts
+++ b/test/interface/index.test.ts
@@ -1,9 +1,9 @@
-import _Pjax, { Pjax } from '../../index';
+import Pjax$, { Pjax, FakeXMLHttpRequest } from '../../index';
describe('Interface: Package', function () {
describe('default', function () {
it('default', function () {
- assert(_Pjax === Pjax);
+ assert(Pjax$ === Pjax);
});
});
@@ -19,4 +19,8 @@ describe('Interface: Package', function () {
});
+ describe('FakeXMLHttpRequest', function () {
+ assert(typeof FakeXMLHttpRequest === 'function');
+ });
+
});