@webqit/webflo 0.11.61 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +1 -1
  2. package/src/{Context.js → AbstractContext.js} +1 -9
  3. package/src/deployment-pi/origins/index.js +1 -1
  4. package/src/index.js +1 -9
  5. package/src/runtime-pi/HttpEvent.js +101 -81
  6. package/src/runtime-pi/HttpUser.js +126 -0
  7. package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
  8. package/src/runtime-pi/MessagingOverChannel.js +85 -0
  9. package/src/runtime-pi/MessagingOverSocket.js +106 -0
  10. package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
  11. package/src/runtime-pi/WebfloCookieStorage.js +27 -0
  12. package/src/runtime-pi/WebfloEventTarget.js +39 -0
  13. package/src/runtime-pi/WebfloMessageEvent.js +58 -0
  14. package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
  15. package/src/runtime-pi/{Router.js → WebfloRouter.js} +3 -34
  16. package/src/runtime-pi/WebfloRuntime.js +52 -0
  17. package/src/runtime-pi/WebfloStorage.js +109 -0
  18. package/src/runtime-pi/client/ClientMessaging.js +5 -0
  19. package/src/runtime-pi/client/Context.js +2 -6
  20. package/src/runtime-pi/client/CookieStorage.js +17 -0
  21. package/src/runtime-pi/client/Router.js +3 -13
  22. package/src/runtime-pi/client/SessionStorage.js +33 -0
  23. package/src/runtime-pi/client/Url.js +24 -72
  24. package/src/runtime-pi/client/WebfloClient.js +544 -0
  25. package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
  26. package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
  27. package/src/runtime-pi/client/WebfloSubClient.js +165 -0
  28. package/src/runtime-pi/client/Workport.js +89 -161
  29. package/src/runtime-pi/client/generate.js +1 -1
  30. package/src/runtime-pi/client/index.js +13 -18
  31. package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
  32. package/src/runtime-pi/client/worker/Context.js +2 -6
  33. package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
  34. package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
  35. package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
  36. package/src/runtime-pi/client/worker/Workport.js +13 -73
  37. package/src/runtime-pi/client/worker/index.js +7 -18
  38. package/src/runtime-pi/index.js +1 -8
  39. package/src/runtime-pi/server/ClientMessaging.js +18 -0
  40. package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
  41. package/src/runtime-pi/server/Context.js +2 -6
  42. package/src/runtime-pi/server/CookieStorage.js +17 -0
  43. package/src/runtime-pi/server/Router.js +2 -68
  44. package/src/runtime-pi/server/SessionStorage.js +53 -0
  45. package/src/runtime-pi/server/WebfloServer.js +755 -0
  46. package/src/runtime-pi/server/index.js +7 -18
  47. package/src/runtime-pi/util-http.js +268 -32
  48. package/src/runtime-pi/xURL.js +25 -22
  49. package/src/runtime-pi/xfetch.js +2 -2
  50. package/src/runtime-pi/Application.js +0 -29
  51. package/src/runtime-pi/Cookies.js +0 -82
  52. package/src/runtime-pi/Runtime.js +0 -21
  53. package/src/runtime-pi/client/Application.js +0 -76
  54. package/src/runtime-pi/client/Runtime.js +0 -525
  55. package/src/runtime-pi/client/createStorage.js +0 -58
  56. package/src/runtime-pi/client/worker/Application.js +0 -44
  57. package/src/runtime-pi/client/worker/Runtime.js +0 -275
  58. package/src/runtime-pi/server/Application.js +0 -101
  59. package/src/runtime-pi/server/Runtime.js +0 -558
  60. package/src/runtime-pi/xFormData.js +0 -24
  61. package/src/runtime-pi/xHeaders.js +0 -146
  62. package/src/runtime-pi/xRequest.js +0 -46
  63. package/src/runtime-pi/xRequestHeaders.js +0 -109
  64. package/src/runtime-pi/xResponse.js +0 -33
  65. package/src/runtime-pi/xResponseHeaders.js +0 -117
  66. package/src/runtime-pi/xxHttpMessage.js +0 -102
@@ -0,0 +1,27 @@
1
+ import { _isObject } from '@webqit/util/js/index.js';
2
+ import { renderCookieObj } from './util-http.js';
3
+ import { WebfloStorage } from './WebfloStorage.js';
4
+
5
+ export class WebfloCookieStorage extends WebfloStorage {
6
+ constructor(request, iterable = []) {
7
+ iterable = [...iterable].map(([key, value]) => [key, !_isObject(value) ? { name: key, value } : value]);
8
+ super(request, null, iterable);
9
+ this.saveOriginals();
10
+ }
11
+
12
+ render() {
13
+ return this.getAdded().map((key) => renderCookieObj({ name: key, ...this.get(key, true) })).concat(
14
+ this.getDeleted().map((key) => renderCookieObj({ name: key, value: '', maxAge: 0 }))
15
+ );
16
+ }
17
+
18
+ set(key, value) {
19
+ if (!_isObject(value)) { value = { name: key, value }; }
20
+ return super.set(key, value);
21
+ }
22
+
23
+ get(key, withDetail = false) {
24
+ if (!withDetail) return super.get(key)?.value;
25
+ return super.get(key);
26
+ }
27
+ }
@@ -0,0 +1,39 @@
1
+ export class WebfloEventTarget extends EventTarget {
2
+
3
+ #parentNode;
4
+ #params;
5
+ #listenersRegistry = new Set;
6
+
7
+ get parentNode() { return this.#parentNode; }
8
+ get params() { return this.#params; }
9
+ get length() { return this.#listenersRegistry.size; }
10
+
11
+ constructor(parentNode, params = {}) {
12
+ super();
13
+ this.#parentNode = parentNode;
14
+ this.#params = params;
15
+ }
16
+
17
+ setParent(parentNode) {
18
+ this.#parentNode = parentNode;
19
+ }
20
+
21
+ dispatchEvent(event) {
22
+ const returnValue = super.dispatchEvent(event);
23
+ if (this.#parentNode instanceof EventTarget && !event.defaultPrevented && !event.propagationStopped) {
24
+ this.#parentNode.dispatchEvent(event);
25
+ }
26
+ return returnValue;
27
+ }
28
+
29
+ addEventListener(...args) {
30
+ this.#listenersRegistry.add(args);
31
+ return super.addEventListener(...args);
32
+ }
33
+
34
+ $destroy() {
35
+ for (const listenerArgs of this.#listenersRegistry) {
36
+ this.removeEventListener(...listenerArgs);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,58 @@
1
+
2
+ export class WebfloMessageEvent extends Event {
3
+
4
+ #originalTarget;
5
+ get originalTarget() { return this.#originalTarget; }
6
+
7
+ get runtime() {
8
+ let parentNode = this.#originalTarget;
9
+ do {
10
+ if (parentNode.runtime) return parentNode.runtime;
11
+ } while (parentNode = parentNode.parentNode)
12
+ }
13
+
14
+ #data;
15
+ get data() { return this.#data; }
16
+
17
+ #ports = [];
18
+ get ports() { return this.#ports; }
19
+
20
+ constructor(originalTarget, messageType, message, ports) {
21
+ super(messageType);
22
+ this.#originalTarget = originalTarget;
23
+ this.#data = message;
24
+ this.#ports = ports;
25
+ }
26
+
27
+ #immediatePropagationStopped = false;
28
+ get immediatePropagationStopped() { return this.#immediatePropagationStopped; }
29
+
30
+ stopImmediatePropagation() {
31
+ this.#immediatePropagationStopped = true;
32
+ this.#propagationStopped = true;
33
+ super.stopImmediatePropagation();
34
+ }
35
+
36
+ #propagationStopped = false;
37
+ get propagationStopped() { return this.#propagationStopped; }
38
+
39
+ stopPropagation() {
40
+ this.#propagationStopped = true;
41
+ super.stopPropagation();
42
+ }
43
+
44
+ #defaultPrevented = false;
45
+ get defaultPrevented() { return this.#defaultPrevented; }
46
+
47
+ preventDefault() {
48
+ this.#defaultPrevented = true;
49
+ super.preventDefault();
50
+ }
51
+
52
+ respondWith(data, transferOrOptions = []) {
53
+ for (const port of this.ports) {
54
+ port.postMessage(data, transferOrOptions);
55
+ }
56
+ return !!this.ports.length;
57
+ }
58
+ }
@@ -0,0 +1,69 @@
1
+ import { WebfloEventTarget } from './WebfloEventTarget.js';
2
+
3
+ export class WebfloMessagingAPI extends WebfloEventTarget {
4
+
5
+ #isConnected = false;
6
+ isConnected() { return this.#isConnected; }
7
+
8
+ #isSending = false;
9
+ isMessaging() { return this.#isSending || !!this.length; }
10
+
11
+ #hooks = new Set;
12
+ on(eventName, callback, { once = false } = {}) {
13
+ if (eventName === 'connected' && this.#isConnected) {
14
+ callback();
15
+ if (once) {
16
+ return;
17
+ }
18
+ }
19
+ const hook = { eventName, callback, once };
20
+ this.#hooks.add(hook);
21
+ return () => this.#hooks.delete(hook);
22
+ }
23
+
24
+ $emit(eventName, arg) {
25
+ if (eventName === 'connected') {
26
+ this.#isConnected = true;
27
+ }
28
+ for (const hook of this.#hooks) {
29
+ if (hook.eventName !== eventName) continue;
30
+ hook.callback(arg);
31
+ if (hook.once) {
32
+ this.#hooks.delete(hook);
33
+ }
34
+ }
35
+ }
36
+
37
+ /* ----------------- */
38
+
39
+ postMessage(data, transferOrOptions = []) {
40
+ this.#isSending = true;
41
+ }
42
+
43
+ postRequest(message, callback, options = {}) {
44
+ const { signal, once, ...$options } = options;
45
+ const messageChannel = new MessageChannel;
46
+ messageChannel.port1.addEventListener('message', (e) => callback(e), {
47
+ signal,
48
+ once
49
+ });
50
+ messageChannel.port1.start();
51
+ return this.postMessage(message, { ...$options, transfer: [ messageChannel.port2 ].concat($options.transfer || []) });
52
+ }
53
+
54
+ handleMessages(type, listener, options = {}) {
55
+ this.addEventListener(type, listener, options);
56
+ return () => {
57
+ this.removeEventListener(type, listener, options);
58
+ };
59
+ }
60
+
61
+ handleRequests(type, listener, options = {}) {
62
+ return this.handleMessages(type, async (e) => {
63
+ const response = await listener(e);
64
+ for (const p of e.ports) {
65
+ p.postMessage(response);
66
+ }
67
+ }, options);
68
+ }
69
+ }
@@ -1,43 +1,13 @@
1
-
2
- /**
3
- * @imports
4
- */
5
1
  import { _isString, _isFunction, _isArray } from '@webqit/util/js/index.js';
6
2
  import { _from as _arrFrom } from '@webqit/util/arr/index.js';
7
3
 
8
- /**
9
- * ---------------------------
10
- * The Router class
11
- * ---------------------------
12
- */
13
-
14
- export default class Router {
4
+ export class WebfloRouter {
15
5
 
16
- /**
17
- * Constructs a new Router instance
18
- * over route definitions.
19
- *
20
- * @param Context cx
21
- * @param String|Array path
22
- *
23
- * @return void
24
- */
25
6
  constructor(cx, path = []) {
26
7
  this.cx = cx;
27
8
  this.path = _isArray(path) ? path : (path + '').split('/').filter(a => a);
28
9
  }
29
10
 
30
- /**
31
- * Performs dynamic routing
32
- *
33
- * @param array|string method
34
- * @param Object event
35
- * @param any arg
36
- * @param function _default
37
- * @param function remoteFetch
38
- *
39
- * @return object
40
- */
41
11
  async route(method, event, arg, _default, remoteFetch = null) {
42
12
 
43
13
  const $this = this;
@@ -57,7 +27,7 @@ export default class Router {
57
27
  // -------------
58
28
  if (thisTick.exports) {
59
29
  // Broadcast any hints exported by handler
60
- if (thisTick.exports.hints) { await event.port.post({ ...thisTick.exports.hints, $type: 'handler:hints' }); }
30
+ //@obsolete if (thisTick.exports.hints) { await event.port.post({ ...thisTick.exports.hints, $type: 'handler:hints' }); }
61
31
  const methods = _arrFrom(thisTick.method).map(m => m === 'default' ? m : m.toUpperCase());
62
32
  const handler = _isFunction(thisTick.exports) && methods.includes('default') ? thisTick.exports : methods.reduce((_handler, name) => _handler || thisTick.exports[name], null);
63
33
  if (handler) {
@@ -68,7 +38,7 @@ export default class Router {
68
38
  const nextTick = { ...thisTick, arg: _args[0] };
69
39
  if (_args.length > 1) {
70
40
  let _url = _args[1], _request, requestInit = { ...(_args[2] || {}) };
71
- if (_args[1] instanceof nextTick.event.Request) {
41
+ if (_args[1] instanceof Request) {
72
42
  _request = _args[1];
73
43
  _url = _request.url;
74
44
  } else if (!_isString(_url)) {
@@ -126,5 +96,4 @@ export default class Router {
126
96
  });
127
97
 
128
98
  }
129
-
130
99
  }
@@ -0,0 +1,52 @@
1
+ import { _isObject } from '@webqit/util/js/index.js';
2
+
3
+ export class WebfloRuntime {
4
+
5
+ async dispatch(httpEvent, context, crossLayerFetch) {
6
+ // Exec routing
7
+ const router = new this.constructor.Router(this.cx, httpEvent.url.pathname);
8
+ const route = async () => {
9
+ return await router.route([httpEvent.request.method, 'default'], httpEvent, context, async (event) => {
10
+ return crossLayerFetch(event);
11
+ }, (...args) => this.remoteFetch(...args));
12
+ };
13
+ try {
14
+ // Route for response
15
+ return await (this.cx.middlewares || []).concat(route).reverse().reduce((next, fn) => {
16
+ return () => fn.call(this.cx, httpEvent, router, next);
17
+ }, null)();
18
+
19
+ } catch (e) {
20
+ console.error(e);
21
+ return new Response(null, { status: 500, statusText: e.message });
22
+ }
23
+ }
24
+
25
+ async normalizeResponse(httpEvent, response, forceCommit = false) {
26
+ // Normalize response
27
+ if (!(response instanceof Response)) {
28
+ response = typeof response === 'undefined'
29
+ ? new Response(null, { status: 404 })
30
+ : Response.create(response);
31
+ }
32
+ // Commit data
33
+ for (const storage of [httpEvent.cookies, httpEvent.session, httpEvent.storage]) {
34
+ await storage?.commit?.(response, forceCommit);
35
+ }
36
+ return response;
37
+ }
38
+
39
+ async execPush(clientPort, data) {
40
+ if (data instanceof Response) {
41
+ if ([301, 302, 303, 307, 308].includes(data.status) && data.headers.has('Location')) {
42
+ clientPort.postMessage(data.headers.get('Location'), { messageType: 'redirect' });
43
+ return;
44
+ }
45
+ data = await data.parse();
46
+ }
47
+ if (!_isObject(data)) {
48
+ throw new Error('Response not serializable');
49
+ }
50
+ clientPort.postMessage(data, { messageType: 'response' });
51
+ }
52
+ }
@@ -0,0 +1,109 @@
1
+ import { _isObject } from '@webqit/util/js/index.js';
2
+ import { _even } from '@webqit/util/obj/index.js';
3
+
4
+ export class WebfloStorage extends Map {
5
+
6
+ #request;
7
+ #session;
8
+
9
+ constructor(request, session, iterable = []) {
10
+ super(iterable);
11
+ this.#request = request;
12
+ this.#session = session === true ? this : session;
13
+ }
14
+
15
+ #originals;
16
+ saveOriginals() { this.#originals = new Map(this); }
17
+
18
+ getDeleted() {
19
+ if (!this.#originals) return [];
20
+ return [...this.#originals.keys()].filter((k) => {
21
+ return !this.has(k);
22
+ });
23
+ }
24
+
25
+ getAdded() {
26
+ if (!this.#originals) return [...this.keys()];
27
+ return [...new Set([...this.keys(), ...this.#originals.keys()])].filter((k) => {
28
+ return !this.#originals.has(k) || (this.has(k) && ((a, b) => _isObject(a) && _isObject(b) ? !_even(a, b) : a !== b)(this.get(k, true), this.#originals.get(k)));
29
+ });
30
+ }
31
+
32
+ commit() {
33
+ this.saveOriginals();
34
+ }
35
+
36
+ #handlers = new Map;
37
+ #reverseHandlers = new Map;
38
+ defineHandler(attr, ...handlers) {
39
+ let registry = this.#handlers;
40
+ if (handlers[0] === false) {
41
+ registry = this.#reverseHandlers;
42
+ handlers.shift();
43
+ }
44
+ const $handlers = [];
45
+ for (let handler of handlers) {
46
+ if (typeof handler === 'function') {
47
+ handler = { callback: handler };
48
+ } else if (typeof handler === 'string') {
49
+ handler = { url: handler };
50
+ } else if (typeof handler?.callback !== 'function' && typeof handler?.url !== 'string') {
51
+ throw new Error(`Handler must be either an URL or a function or an object specifying either an URL (handler.url) or a function (handler.callback)`);
52
+ }
53
+ $handlers.push(handler);
54
+ }
55
+ registry.set(attr, $handlers);
56
+ }
57
+
58
+ defineReverseHandler(attr, ...handlers) {
59
+ return this.defineHandler(attr, false, ...handlers);
60
+ }
61
+
62
+ getHandlers() { return this.#handlers; }
63
+
64
+ getReverseHandlers() { return this.#reverseHandlers; }
65
+
66
+ async require(attrs, callback = null, noNulls = false) {
67
+ const entries = [];
68
+ main: for await (const attr of [].concat(attrs)) {
69
+ if (!this.has(attr) || (noNulls && [undefined, null].includes(this.get(attr)))) {
70
+ const handlers = this.#handlers.get(attr);
71
+ if (!handlers) {
72
+ throw new Error(`No handler defined for the user attribute: ${attr}`);
73
+ }
74
+ for (let i = 0; i < handlers.length; i ++) {
75
+ const handler = handlers[i];
76
+ if (handler.callback) {
77
+ const returnValue = await handler.callback(this, attr);
78
+ if (returnValue instanceof Response) {
79
+ return returnValue;
80
+ }
81
+ if ((typeof returnValue === 'undefined' || (noNulls && returnValue === null)) && i < handlers.length - 1) {
82
+ continue;
83
+ }
84
+ entries.push(returnValue);
85
+ continue main;
86
+ }
87
+ const urlRewrite = new URL(handler.url, this.#request.url);
88
+ if (!urlRewrite.searchParams.has('success-redirect')) {
89
+ urlRewrite.searchParams.set('success-redirect', this.#request.url.replace(urlRewrite.origin, ''));
90
+ }
91
+ if (handler.message) {
92
+ if (!this.#session) {
93
+ throw new Error('Storage type does not support redirect messages');
94
+ }
95
+ const messageID = (0 | Math.random() * 9e6).toString(36);
96
+ urlRewrite.searchParams.set('redirect-message', messageID);
97
+ this.#session.set(`redirect-message:${messageID}`, { status: { type: handler.type || 'info', message: handler.message }});
98
+ }
99
+ return new Response(null, { status: 302, headers: {
100
+ Location: urlRewrite
101
+ }});
102
+ }
103
+ }
104
+ entries.push(this.get(attr));
105
+ }
106
+ if (callback) return await callback(...entries);
107
+ return entries;
108
+ }
109
+ }
@@ -0,0 +1,5 @@
1
+ import { MessagingOverChannel } from '../MessagingOverChannel.js';
2
+
3
+ export class ClientMessaging extends MessagingOverChannel {
4
+ get runtime() { return this.parentNode; }
5
+ }
@@ -1,7 +1,3 @@
1
+ import { AbstractContext } from '../../AbstractContext.js';
1
2
 
2
- /**
3
- * @imports
4
- */
5
- import _Context from '../../Context.js';
6
-
7
- export default class Context extends _Context {}
3
+ export class Context extends AbstractContext {}
@@ -0,0 +1,17 @@
1
+ import { WebfloCookieStorage } from '../WebfloCookieStorage.js';
2
+
3
+ export class CookieStorage extends WebfloCookieStorage {
4
+ static create(request) {
5
+ return new this(
6
+ request,
7
+ document.cookie.split(';').map((c) => c.split('=').map((s) => s.trim()))
8
+ );
9
+ }
10
+
11
+ commit(response) {
12
+ for (const cookieStr of this.render()) {
13
+ document.cookie = cookieStr;
14
+ }
15
+ super.commit();
16
+ }
17
+ }
@@ -1,17 +1,7 @@
1
-
2
- /**
3
- * @imports
4
- */
5
1
  import { path as Path } from '../util-url.js';
6
- import _Router from '../Router.js';
7
-
8
- /**
9
- * ---------------------------
10
- * The Router class
11
- * ---------------------------
12
- */
2
+ import { WebfloRouter } from '../WebfloRouter.js';
13
3
 
14
- export default class Router extends _Router {
4
+ export class Router extends WebfloRouter {
15
5
 
16
6
  async readTick(thisTick) {
17
7
  thisTick = { ...thisTick };
@@ -45,4 +35,4 @@ export default class Router extends _Router {
45
35
  pathJoin(...args) {
46
36
  return Path.join(...args);
47
37
  }
48
- };
38
+ }
@@ -0,0 +1,33 @@
1
+ import { WebfloStorage } from '../WebfloStorage.js';
2
+
3
+ export class SessionStorage extends WebfloStorage {
4
+ static get type() { return 'session'; }
5
+
6
+ static create(request) {
7
+ const keys = [];
8
+ const storeType = this.type === 'user' ? 'localStorage' : 'sessionStorage';
9
+ for(let i = 0; i < window[storeType].length; i ++){
10
+ keys.push(window[storeType].key(i));
11
+ };
12
+ const instance = new this(
13
+ request,
14
+ keys.map((key) => [key, window[storeType].getItem(key)])
15
+ );
16
+ return instance;
17
+ }
18
+
19
+ constructor(request, iterable) {
20
+ super(request, true, iterable);
21
+ }
22
+
23
+ commit() {
24
+ const storeType = this.constructor.type === 'user' ? 'localStorage' : 'sessionStorage';
25
+ for (const key of this.getAdded()) {
26
+ window[storeType].setItem(key, this.get(key));
27
+ }
28
+ for (const key of this.getDeleted()) {
29
+ window[storeType].removeItem(key);
30
+ }
31
+ super.commit();
32
+ }
33
+ }
@@ -1,28 +1,11 @@
1
-
2
- /**
3
- * @imports
4
- */
5
1
  import { _with } from '@webqit/util/obj/index.js';
6
2
  import { _isArray, _isObject, _isTypeObject, _isString, _isEmpty } from '@webqit/util/js/index.js';
7
- import { Observer } from './Runtime.js';
8
3
  import { params } from '../util-url.js';
9
4
 
10
- /**
11
- * ---------------------------
12
- * The Url class
13
- * ---------------------------
14
- */
5
+ const { Observer } = webqit;
15
6
 
16
- export default class Url {
7
+ export class Url {
17
8
 
18
- /**
19
- * Constructs a new Url instance.
20
- *
21
- * @param object input
22
- * @param object pathMappingScheme
23
- *
24
- * @return void
25
- */
26
9
  constructor(input) {
27
10
  const Self = this.constructor;
28
11
  // -----------------------
@@ -56,6 +39,9 @@ export default class Url {
56
39
  // ----------
57
40
  if (e.key === 'href' && e.related.length === 1) {
58
41
  var urlObj = Self.parseUrl(e.value);
42
+ if (urlObj.pathname) {
43
+ urlObj.pathname = '/' + urlObj.pathname.split('/').filter(s => s.trim()).join('/');
44
+ }
59
45
  delete urlObj.query;
60
46
  delete urlObj.href;
61
47
  onlyHrefChanged = true;
@@ -68,13 +54,27 @@ export default class Url {
68
54
  urlObj.search = search;
69
55
  }
70
56
  }
71
- if (e.key === 'search') {
57
+ if (e.key === 'search' && !e.related.includes('query')) {
72
58
  // "search" was updated. So we update "query"
73
59
  var query = Self.toQuery(urlObj.search || this.search); // Not e.value, as that might be a href value
74
60
  if (!_strictEven(query, this.query)) {
75
61
  urlObj.query = query;
76
62
  }
77
63
  }
64
+ if (e.key === 'pathname' && !e.related.includes('ancestorPathname')) {
65
+ // "pathname" was updated. So we update "ancestorPathname"
66
+ var ancestorPathname = (urlObj.pathname || this.pathname).replace(new RegExp('/[^/]+(?:/)?$'), '');
67
+ if (ancestorPathname !== this.ancestorPathname) {
68
+ urlObj.ancestorPathname = ancestorPathname;
69
+ }
70
+ }
71
+ if (e.key === 'ancestorPathname' && !e.related.includes('pathname')) {
72
+ // "ancestorPathname" was updated. So we update "pathname"
73
+ var pathname = '/' + (urlObj.ancestorPathname || this.ancestorPathname).split('/').filter(s => s).concat((urlObj.pathname || this.pathname).split('/').filter(s => s).pop()).join('/');
74
+ if (pathname !== this.pathname) {
75
+ urlObj.pathname = pathname;
76
+ }
77
+ }
78
78
  }
79
79
  if (!onlyHrefChanged) {
80
80
  var fullOrigin = this.origin,
@@ -110,85 +110,37 @@ export default class Url {
110
110
  Observer.set(this, _isString(input) ? Self.parseUrl(input) : Url.copy(input));
111
111
  }
112
112
 
113
- /**
114
- * Converts the instance to string.
115
- *
116
- * @return string
117
- */
118
113
  toString() {
119
114
  return this.href;
120
115
  }
121
116
 
122
- /**
123
- * Creates an instance from parsing an URL string
124
- * or from a regular object.
125
- *
126
- * @param string|object href
127
- *
128
- * @return Url
129
- */
130
117
  static from(href) {
131
118
  return new this(_isObject(href) ? href : this.parseUrl(href));
132
119
  }
133
120
 
134
- /**
135
- * Copies URL properties off
136
- * the given object.
137
- *
138
- * @param object urlObj
139
- *
140
- * @return object
141
- */
142
121
  static copy(urlObj) {
143
- var url = urlProperties.reduce((obj, prop) => _with(obj, prop, urlObj[prop]), {});
122
+ var url = urlProperties.reduce((obj, prop) => _with(obj, prop, urlObj[prop] || ''), {});
144
123
  if (!('query' in urlObj)) {
145
124
  delete url.query;
146
125
  }
147
126
  return url;
148
127
  }
149
128
 
150
- /**
151
- * Parses an URL and returns its properties
152
- *
153
- * @param string href
154
- *
155
- * @return object
156
- */
157
129
  static parseUrl(href) {
158
- var a = document.createElement('a');
159
- a.href = href;
130
+ var a = new URL(href);
160
131
  return this.copy(a);
161
132
  }
162
133
 
163
- /**
164
- * Parses the input search string into a named map
165
- *
166
- * @param string search
167
- *
168
- * @return object
169
- */
170
134
  static toQuery(search) {
171
135
  return params.parse(search);
172
136
  }
173
137
 
174
- /**
175
- * Stringifies the input query to search string.
176
- *
177
- * @param object query
178
- *
179
- * @return string
180
- */
181
138
  static toSearch(query) {
182
139
  var search = params.stringify(query);
183
140
  return search ? '?' + search : '';
184
- }}
141
+ }
142
+ }
185
143
 
186
- /**
187
- * These are standard
188
- * and shouldnt'/can't be modified
189
- *
190
- * @array
191
- */
192
144
  const urlProperties = [
193
145
  'protocol',
194
146
  'username',