@thepassle/app-tools 0.0.10 → 0.7.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.
- package/README.md +3 -1
- package/api/index.js +31 -9
- package/api/plugins/abort.js +1 -0
- package/api/plugins/cache.js +4 -3
- package/api/plugins/debounce.js +37 -0
- package/api/plugins/delay.js +4 -1
- package/api/plugins/jsonPrefix.js +1 -0
- package/api/plugins/logger.js +1 -0
- package/api/plugins/mock.js +2 -14
- package/api/plugins/xsrf.js +29 -0
- package/dialog/dialog.test.js +66 -0
- package/dialog/events.js +14 -0
- package/dialog/index.js +184 -0
- package/dialog/utils.js +40 -0
- package/dialog.js +1 -0
- package/package.json +19 -16
- package/pwa/capabilities.js +7 -0
- package/pwa/events.js +33 -0
- package/pwa/index.js +174 -0
- package/pwa.js +2 -0
- package/router/index.js +221 -0
- package/router/plugins/appName.js +12 -0
- package/router/plugins/checkServiceWorkerUpdate.js +15 -0
- package/router/plugins/data.js +13 -0
- package/router/plugins/lazy.js +13 -0
- package/router/plugins/offline.js +16 -0
- package/router/plugins/redirect.js +13 -0
- package/router/plugins/resetFocus.js +31 -0
- package/router/plugins/scrollToTop.js +9 -0
- package/router.js +1 -0
- package/state/index.js +6 -1
- package/utils/CONSTANTS.js +1 -0
- package/utils/Service.js +83 -0
- package/utils/async.js +81 -0
- package/utils/log.js +44 -0
- package/utils/media.js +47 -0
- package/utils.js +3 -0
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# `@thepassle/app-tools`
|
|
2
2
|
|
|
3
|
-
Collection of tools I regularly use to build apps. Maybe they're useful to somebody else. Maybe not.
|
|
3
|
+
Collection of tools I regularly use to build apps. Maybe they're useful to somebody else. Maybe not. Most of these are thin wrappers around native API's, like the native `<dialog>` element, `fetch` API, and `URLPattern`.
|
|
4
4
|
|
|
5
5
|
## Packages
|
|
6
6
|
|
|
7
7
|
- [`state`](/state/README.md)
|
|
8
|
+
- [`router`](/router/README.md)
|
|
8
9
|
- [`api`](/api/README.md)
|
|
9
10
|
- [`pwa`](/pwa/README.md)
|
|
11
|
+
- [`dialog`](/dialog/README.md)
|
|
10
12
|
- [`env`](/env/README.md)
|
|
11
13
|
- [`utils`](/utils/README.md)
|
package/api/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { createLogger } from '../utils/log.js';
|
|
2
|
+
const log = createLogger('api');
|
|
3
|
+
|
|
1
4
|
function handleStatus(response) {
|
|
2
5
|
if (!response.ok) {
|
|
6
|
+
log('Response not ok', response.statusText);
|
|
3
7
|
throw new Error(response.statusText);
|
|
4
8
|
}
|
|
5
9
|
return response;
|
|
@@ -20,6 +24,7 @@ function handleStatus(response) {
|
|
|
20
24
|
* beforeFetch?: (meta: MetaParams) => MetaParams | Promise<MetaParams> | void,
|
|
21
25
|
* afterFetch?: (res: Response) => Response | Promise<Response>,
|
|
22
26
|
* transform?: (data: any) => any,
|
|
27
|
+
* name: string,
|
|
23
28
|
* handleError?: (e: Error) => boolean
|
|
24
29
|
* }} Plugin
|
|
25
30
|
*
|
|
@@ -98,13 +103,19 @@ export class Api {
|
|
|
98
103
|
url += `${(~url.indexOf('?') ? '&' : '?')}${new URLSearchParams(opts.params)}`;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
for(const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
(
|
|
106
|
+
for(const plugin of plugins) {
|
|
107
|
+
try {
|
|
108
|
+
const overrides = await plugin?.beforeFetch?.({ responseType, headers, fetchFn, baseURL, url, method, opts, data });
|
|
109
|
+
if(overrides) {
|
|
110
|
+
({ responseType, headers, fetchFn, baseURL, url, method, opts, data } = {...overrides});
|
|
111
|
+
}
|
|
112
|
+
} catch(e) {
|
|
113
|
+
log(`Plugin "${plugin.name}" error on afterFetch hook`);
|
|
114
|
+
throw e;
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
|
|
118
|
+
log(`Fetching ${method} ${url}`, { responseType, headers: Object.fromEntries(headers), fetchFn, baseURL, url, method, opts, data });
|
|
108
119
|
return fetchFn(url, {
|
|
109
120
|
method,
|
|
110
121
|
headers,
|
|
@@ -121,8 +132,13 @@ export class Api {
|
|
|
121
132
|
})
|
|
122
133
|
/** [PLUGINS - AFTERFETCH] */
|
|
123
134
|
.then(async res => {
|
|
124
|
-
for(const
|
|
125
|
-
|
|
135
|
+
for(const plugin of plugins) {
|
|
136
|
+
try {
|
|
137
|
+
res = await plugin?.afterFetch?.(res) ?? res;
|
|
138
|
+
} catch(e) {
|
|
139
|
+
log(`Plugin "${plugin.name}" error on afterFetch hook`)
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
126
142
|
}
|
|
127
143
|
|
|
128
144
|
return res;
|
|
@@ -132,14 +148,20 @@ export class Api {
|
|
|
132
148
|
/** [RESPONSETYPE] */
|
|
133
149
|
.then(res => res[responseType]())
|
|
134
150
|
.then(async data => {
|
|
135
|
-
for(const
|
|
136
|
-
|
|
151
|
+
for(const plugin of plugins) {
|
|
152
|
+
try {
|
|
153
|
+
data = await plugin?.transform?.(data) ?? data;
|
|
154
|
+
} catch(e) {
|
|
155
|
+
log(`Plugin "${plugin.name}" error on transform hook`)
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
137
158
|
}
|
|
138
|
-
|
|
159
|
+
log(`Fetch successful ${method} ${url}`, data);
|
|
139
160
|
return data;
|
|
140
161
|
})
|
|
141
162
|
/** [PLUGINS - HANDLEERROR] */
|
|
142
163
|
.catch(async e => {
|
|
164
|
+
log(`Fetch failed ${method} ${url}`, e);
|
|
143
165
|
const shouldThrow = (await Promise.all(plugins.map(({handleError}) => handleError?.(e) ?? false))).some(_ => !!_);
|
|
144
166
|
if(shouldThrow) throw e;
|
|
145
167
|
});
|
package/api/plugins/abort.js
CHANGED
package/api/plugins/cache.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
const TEN_MINUTES = 1000 * 60 * 10;
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @param {{maxAge?: number}}
|
|
4
|
+
* @param {{maxAge?: number}} options
|
|
5
5
|
* @returns {import('../index').Plugin}
|
|
6
6
|
*/
|
|
7
|
-
export function cachePlugin({maxAge} = {}) {
|
|
7
|
+
export function cachePlugin({maxAge = TEN_MINUTES} = {}) {
|
|
8
8
|
let requestId;
|
|
9
9
|
const cache = new Map();
|
|
10
10
|
|
|
11
11
|
return {
|
|
12
|
+
name: 'cache',
|
|
12
13
|
beforeFetch: (meta) => {
|
|
13
14
|
const { method, url } = meta;
|
|
14
15
|
requestId = `${method}:${url}`;
|
|
15
16
|
|
|
16
17
|
if(cache.has(requestId)) {
|
|
17
18
|
const cached = cache.get(requestId);
|
|
18
|
-
if(cached.updatedAt > Date.now() - (maxAge
|
|
19
|
+
if(cached.updatedAt > Date.now() - (maxAge)) {
|
|
19
20
|
meta.fetchFn = () => Promise.resolve(new Response(JSON.stringify(cached.data), {status: 200}));
|
|
20
21
|
return meta;
|
|
21
22
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function _debounce(promise, opts = { timeout: 1000 }) {
|
|
2
|
+
let timeoutId;
|
|
3
|
+
return (...args) => {
|
|
4
|
+
clearTimeout(timeoutId);
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
timeoutId = setTimeout(() => {
|
|
7
|
+
resolve(promise(...args))
|
|
8
|
+
}, opts.timeout);
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{
|
|
15
|
+
* timeout: number
|
|
16
|
+
* }} opts
|
|
17
|
+
* @returns {import('../index.js').Plugin}
|
|
18
|
+
*/
|
|
19
|
+
export function debouncePlugin(opts = {
|
|
20
|
+
timeout: 1000
|
|
21
|
+
}) {
|
|
22
|
+
let debounced;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: 'debounce',
|
|
26
|
+
beforeFetch: (meta) => {
|
|
27
|
+
if(!debounced) {
|
|
28
|
+
const originalFetch = meta.fetchFn;
|
|
29
|
+
debounced = _debounce(originalFetch, opts);
|
|
30
|
+
}
|
|
31
|
+
meta.fetchFn = debounced;
|
|
32
|
+
return meta;
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const debounce = debouncePlugin();
|
package/api/plugins/delay.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* @param {number} ms
|
|
3
3
|
* @returns {import('../index').Plugin}
|
|
4
4
|
*/
|
|
5
|
-
export const delayPlugin = (ms) => ({
|
|
5
|
+
export const delayPlugin = (ms) => ({
|
|
6
|
+
name: 'delay',
|
|
7
|
+
afterFetch: () => new Promise(r => setTimeout(r,ms))
|
|
8
|
+
});
|
|
6
9
|
|
|
7
10
|
export const delay = delayPlugin(1000);
|
package/api/plugins/logger.js
CHANGED
|
@@ -9,6 +9,7 @@ export function loggerPlugin({collapsed = true} = {}) {
|
|
|
9
9
|
let start;
|
|
10
10
|
const group = collapsed ? 'groupCollapsed' : 'group';
|
|
11
11
|
return {
|
|
12
|
+
name: 'logger',
|
|
12
13
|
beforeFetch: (meta) => {
|
|
13
14
|
console[group](`[START] [${new Date().toLocaleTimeString()}] [${meta.method}] "${meta.url}"`);
|
|
14
15
|
console.table([meta]);
|
package/api/plugins/mock.js
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @param {() => void} f
|
|
3
|
-
* @param {number} ms
|
|
4
|
-
* @param {{
|
|
5
|
-
* signal?: AbortSignal
|
|
6
|
-
* }} [options]
|
|
7
|
-
*/
|
|
8
|
-
function setAbortableTimeout(f, ms, {signal}) {
|
|
9
|
-
let t;
|
|
10
|
-
if(!signal?.aborted) {
|
|
11
|
-
t = setTimeout(f, ms);
|
|
12
|
-
}
|
|
13
|
-
signal?.addEventListener('abort', () => clearTimeout(t), {once: true});
|
|
14
|
-
};
|
|
1
|
+
import { setAbortableTimeout } from '../../utils/async.js';
|
|
15
2
|
|
|
16
3
|
/**
|
|
17
4
|
* @param {Response | (() => Response) | (() => Promise<Response>)} response
|
|
@@ -19,6 +6,7 @@ function setAbortableTimeout(f, ms, {signal}) {
|
|
|
19
6
|
*/
|
|
20
7
|
export function mock(response) {
|
|
21
8
|
return {
|
|
9
|
+
name: 'mock',
|
|
22
10
|
beforeFetch: (meta) => {
|
|
23
11
|
meta.fetchFn = function mock(_, opts) {
|
|
24
12
|
return new Promise(r => setAbortableTimeout(
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function getCookie(name, _document = document) {
|
|
2
|
+
const match = _document.cookie.match(new RegExp(`(^|;\\s*)(${name})=([^;]*)`));
|
|
3
|
+
return match ? decodeURIComponent(match[3]) : null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{
|
|
8
|
+
* xsrfCookieName?: string,
|
|
9
|
+
* xsrfHeaderName?: string,
|
|
10
|
+
* }} options
|
|
11
|
+
* @returns {import('../index').Plugin}
|
|
12
|
+
*/
|
|
13
|
+
export function xsrfPlugin({
|
|
14
|
+
xsrfCookieName = 'XSRF-TOKEN',
|
|
15
|
+
xsrfHeaderName = 'X-CSRF-TOKEN'
|
|
16
|
+
} = {}) {
|
|
17
|
+
const csrfToken = getCookie(xsrfCookieName);
|
|
18
|
+
return {
|
|
19
|
+
name: 'xsrf',
|
|
20
|
+
beforeFetch: (meta) => {
|
|
21
|
+
if(csrfToken) {
|
|
22
|
+
meta.headers.set(xsrfHeaderName, csrfToken);
|
|
23
|
+
}
|
|
24
|
+
return meta;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const xsrf = xsrfPlugin();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect, oneEvent } from '@open-wc/testing';
|
|
2
|
+
import { stub } from 'sinon';
|
|
3
|
+
import { Dialog } from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('Dialog', () => {
|
|
6
|
+
let dialog;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
dialog = new Dialog({
|
|
10
|
+
foo: { opening: ({dialog}) => dialog.form.innerHTML = 'hello world' }
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
if(dialog.open) await dialog.close();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('opens', async () => {
|
|
19
|
+
await dialog.open({id: 'foo'});
|
|
20
|
+
await dialog.opened;
|
|
21
|
+
expect(dialog.isOpen).to.be.true;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('closes', async () => {
|
|
25
|
+
await dialog.open({id: 'foo'});
|
|
26
|
+
await dialog.opened;
|
|
27
|
+
await dialog.close();
|
|
28
|
+
await dialog.closed;
|
|
29
|
+
expect(dialog.isOpen).to.be.false;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('modify', async () => {
|
|
33
|
+
await dialog.open({id: 'foo'});
|
|
34
|
+
|
|
35
|
+
const d = await dialog.opened;
|
|
36
|
+
dialog.modify(node => {node.classList.add('foo')});
|
|
37
|
+
|
|
38
|
+
expect(d.classList.contains('foo')).to.be.true;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('runs callbacks', async () => {
|
|
42
|
+
const cbs = {
|
|
43
|
+
opening: stub(),
|
|
44
|
+
opened: stub(),
|
|
45
|
+
closing: stub(),
|
|
46
|
+
closed: stub(),
|
|
47
|
+
};
|
|
48
|
+
const dialog = new Dialog({foo: cbs});
|
|
49
|
+
|
|
50
|
+
await dialog.open({id: 'foo'});
|
|
51
|
+
await dialog.opened;
|
|
52
|
+
|
|
53
|
+
expect(cbs.opening.called).to.be.true;
|
|
54
|
+
expect(cbs.opened.called).to.be.true;
|
|
55
|
+
expect(cbs.closing.called).to.be.false;
|
|
56
|
+
expect(cbs.closed.called).to.be.false;
|
|
57
|
+
|
|
58
|
+
dialog.close()
|
|
59
|
+
await dialog.closed;
|
|
60
|
+
|
|
61
|
+
expect(cbs.opening.called).to.be.true;
|
|
62
|
+
expect(cbs.opened.called).to.be.true;
|
|
63
|
+
expect(cbs.closing.called).to.be.true;
|
|
64
|
+
expect(cbs.closed.called).to.be.true;
|
|
65
|
+
});
|
|
66
|
+
});
|
package/dialog/events.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class DialogStateEvent extends Event {
|
|
2
|
+
/**
|
|
3
|
+
* @param {'opening' | 'opened' | 'closing' | 'closed'} kind
|
|
4
|
+
* @param {{
|
|
5
|
+
* id: string,
|
|
6
|
+
* dialog: import('./index.js').DialogNode,
|
|
7
|
+
* }} opts
|
|
8
|
+
*/
|
|
9
|
+
constructor(kind, {id, dialog}) {
|
|
10
|
+
super(kind);
|
|
11
|
+
this.dialog = dialog;
|
|
12
|
+
this.id = id;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dialog/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { APP_TOOLS } from '../utils/CONSTANTS.js';
|
|
2
|
+
import { setupGlobalDialogStyles } from './utils.js';
|
|
3
|
+
import { DialogStateEvent } from './events.js';
|
|
4
|
+
import { onePaint, animationsComplete } from '../utils/async.js';
|
|
5
|
+
import { createLogger } from '../utils/log.js';
|
|
6
|
+
const log = createLogger('dialog');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {HTMLDialogElement & { form: HTMLFormElement }} DialogNode
|
|
10
|
+
* @typedef {Record<string, {
|
|
11
|
+
* opening?: <Parameters>(opts: {dialog: DialogNode, parameters: Parameters}) => void,
|
|
12
|
+
* opened?: <Parameters>(opts: {dialog: DialogNode, parameters: Parameters}) => void,
|
|
13
|
+
* closing?: (opts: {dialog: DialogNode}) => void,
|
|
14
|
+
* closed?: (opts: {dialog: DialogNode}) => void,
|
|
15
|
+
* }>} Config
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
setupGlobalDialogStyles();
|
|
19
|
+
|
|
20
|
+
export class Dialog extends EventTarget {
|
|
21
|
+
#id = '';
|
|
22
|
+
/** @type {Config} */
|
|
23
|
+
#config = {};
|
|
24
|
+
isOpen = false;
|
|
25
|
+
opened = new Promise((resolve) => {this.__resolveOpened = resolve;});
|
|
26
|
+
closed = new Promise((resolve) => {this.__resolveClosed = resolve;});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @param {Config} config
|
|
31
|
+
*/
|
|
32
|
+
constructor(config) {
|
|
33
|
+
super();
|
|
34
|
+
this.#config = config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @returns {DialogNode}
|
|
39
|
+
*/
|
|
40
|
+
__initDialogNode() {
|
|
41
|
+
const dialogNode = /** @type {DialogNode} */ (document.createElement('dialog'));
|
|
42
|
+
dialogNode.setAttribute(APP_TOOLS, '');
|
|
43
|
+
dialogNode.addEventListener('close', this.__onDialogClose);
|
|
44
|
+
dialogNode.addEventListener('mousedown', this.__onLightDismiss);
|
|
45
|
+
|
|
46
|
+
const form = document.createElement('form');
|
|
47
|
+
form.setAttribute(APP_TOOLS, '');
|
|
48
|
+
form.setAttribute('method', 'dialog');
|
|
49
|
+
dialogNode.form = form;
|
|
50
|
+
|
|
51
|
+
dialogNode.appendChild(form);
|
|
52
|
+
|
|
53
|
+
return dialogNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
__onLightDismiss = ({target}) => {
|
|
57
|
+
if(target.nodeName === 'DIALOG') {
|
|
58
|
+
this.close('dismiss');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
close = (kind = 'programmatic') => {
|
|
63
|
+
this.__dialog?.close(kind);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
__onDialogClose = async () => {
|
|
67
|
+
const id = this.#id;
|
|
68
|
+
const d = /** @type {DialogNode} */ (this.__dialog);
|
|
69
|
+
|
|
70
|
+
log(`Closing dialog "${id}"`, {
|
|
71
|
+
id,
|
|
72
|
+
dialog: d,
|
|
73
|
+
returnValue: d?.returnValue
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
d.removeAttribute('opened');
|
|
77
|
+
d.setAttribute('closing', '');
|
|
78
|
+
this.dispatchEvent(new DialogStateEvent('closing', {
|
|
79
|
+
id,
|
|
80
|
+
dialog: d,
|
|
81
|
+
}));
|
|
82
|
+
try {
|
|
83
|
+
await this.#config[id]?.closing?.({dialog: d});
|
|
84
|
+
} catch(e) {
|
|
85
|
+
log(`Dialog "${id}" error on closing hook`);
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
await animationsComplete(d);
|
|
89
|
+
|
|
90
|
+
this.isOpen = false;
|
|
91
|
+
d.removeAttribute('closing');
|
|
92
|
+
d.setAttribute('closed', '');
|
|
93
|
+
|
|
94
|
+
// @ts-ignore
|
|
95
|
+
this.__resolveClosed(d);
|
|
96
|
+
|
|
97
|
+
this.dispatchEvent(new DialogStateEvent('closed', {
|
|
98
|
+
id,
|
|
99
|
+
dialog: d
|
|
100
|
+
}));
|
|
101
|
+
try {
|
|
102
|
+
await this.#config[id]?.closed?.({dialog: d});
|
|
103
|
+
} catch(e) {
|
|
104
|
+
log(`Dialog "${id}" error on closed hook`);
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
log(`Closed dialog "${id}"`, {
|
|
108
|
+
id,
|
|
109
|
+
dialog: d,
|
|
110
|
+
returnValue: d?.returnValue
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
d?.remove();
|
|
114
|
+
this.__dialog = undefined;
|
|
115
|
+
|
|
116
|
+
this.opened = new Promise((resolve) => {this.__resolveOpened = resolve;});
|
|
117
|
+
this.#id = '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
*
|
|
122
|
+
* @param {{
|
|
123
|
+
* id: string,
|
|
124
|
+
* parameters?: object
|
|
125
|
+
* }} options
|
|
126
|
+
* @returns
|
|
127
|
+
*/
|
|
128
|
+
async open({id, parameters}) {
|
|
129
|
+
if(!(id in this.#config)) {
|
|
130
|
+
throw new Error(`No dialog configured for id: ${id}`);
|
|
131
|
+
}
|
|
132
|
+
this.#id = id;
|
|
133
|
+
|
|
134
|
+
if(this.isOpen) {
|
|
135
|
+
log(`Tried to open dialog "${id}" while it was already open.`, { id, parameters, dialog: this.__dialog });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.__dialog = this.__initDialogNode();
|
|
140
|
+
document.body.appendChild(this.__dialog);
|
|
141
|
+
|
|
142
|
+
log(`Openening dialog "${id}"`, { id, parameters, dialog: this.__dialog });
|
|
143
|
+
this.__dialog.setAttribute('opening', '');
|
|
144
|
+
this.dispatchEvent(new DialogStateEvent('opening', { id, dialog: this.__dialog }));
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await this.#config?.[id]?.opening?.({dialog: this.__dialog, parameters});
|
|
148
|
+
} catch(e) {
|
|
149
|
+
log(`Dialog "${this.#id}" error on opening hook`);
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
await onePaint();
|
|
153
|
+
|
|
154
|
+
this.__dialog.showModal();
|
|
155
|
+
|
|
156
|
+
await animationsComplete(this.__dialog);
|
|
157
|
+
|
|
158
|
+
this.isOpen = true;
|
|
159
|
+
this.__dialog.removeAttribute('opening');
|
|
160
|
+
this.__dialog.setAttribute('opened', '');
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
this.__resolveOpened(this.__dialog);
|
|
163
|
+
this.dispatchEvent(new DialogStateEvent('opened', { id, dialog: this.__dialog }));
|
|
164
|
+
try {
|
|
165
|
+
await this.#config?.[id]?.opened?.({dialog: this.__dialog, parameters});
|
|
166
|
+
} catch(e) {
|
|
167
|
+
log(`Dialog "${this.#id}" error on opened hook`);
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
log(`Opened dialog "${id}"`, { id, parameters, dialog: this.__dialog });
|
|
171
|
+
this.closed = new Promise((resolve) => {this.__resolveClosed = resolve;});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Can be used to modify the dialog
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* dialog.modify(node => {node.classList.add('foo')});
|
|
179
|
+
* @param {(dialog: DialogNode | undefined) => void} cb
|
|
180
|
+
*/
|
|
181
|
+
modify(cb) {
|
|
182
|
+
cb(this.__dialog);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dialog/utils.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { APP_TOOLS } from '../utils/CONSTANTS.js';
|
|
2
|
+
|
|
3
|
+
const DIALOG_STYLES_ID = 'dialog-styles';
|
|
4
|
+
|
|
5
|
+
export function setupGlobalDialogStyles() {
|
|
6
|
+
let el = document.head.querySelector(`style[${APP_TOOLS}]#${DIALOG_STYLES_ID}`);
|
|
7
|
+
if (!el) {
|
|
8
|
+
el = document.createElement('style');
|
|
9
|
+
el.setAttribute(APP_TOOLS, '');
|
|
10
|
+
el.id = DIALOG_STYLES_ID;
|
|
11
|
+
el.innerHTML = `
|
|
12
|
+
html:has(dialog[${APP_TOOLS}][open]) {
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
dialog[${APP_TOOLS}] {
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
inset: 0;
|
|
19
|
+
position: fixed;
|
|
20
|
+
display: block;
|
|
21
|
+
|
|
22
|
+
padding: 0;
|
|
23
|
+
width: 200px;
|
|
24
|
+
height: 200px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dialog[${APP_TOOLS}] > form[${APP_TOOLS}] {
|
|
28
|
+
width: calc(100% - 10px);
|
|
29
|
+
height: calc(100% - 10px);
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding: 5px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
dialog[${APP_TOOLS}][open] {
|
|
35
|
+
pointer-events: auto;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
document.head.prepend(el);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dialog.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dialog } from './dialog/index.js';
|
package/package.json
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thepassle/app-tools",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "wtr
|
|
8
|
+
"test": "wtr **/*.test.js --node-resolve",
|
|
9
9
|
"test:watch": "npm run test -- --watch"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
"./state.js": "./state.js",
|
|
13
13
|
"./pwa.js": "./pwa.js",
|
|
14
|
+
"./dialog.js": "./dialog.js",
|
|
15
|
+
"./pwa/*.js": "./pwa/*.js",
|
|
14
16
|
"./api.js": "./api.js",
|
|
15
|
-
"./
|
|
16
|
-
"./
|
|
17
|
-
"./api/plugins
|
|
18
|
-
"./api/plugins/jsonPrefix.js": "./api/plugins/jsonPrefix.js",
|
|
19
|
-
"./api/plugins/mock.js": "./api/plugins/mock.js",
|
|
20
|
-
"./api/plugins/logger.js": "./api/plugins/logger.js",
|
|
21
|
-
"./api/plugins/xsrf.js": "./api/plugins/xsrf.js",
|
|
17
|
+
"./router.js": "./router.js",
|
|
18
|
+
"./router/plugins/*.js": "./router/plugins/*.js",
|
|
19
|
+
"./api/plugins/*.js": "./api/plugins/*.js",
|
|
22
20
|
"./utils.js": "./utils.js",
|
|
21
|
+
"./utils/*.js": "./utils/*.js",
|
|
23
22
|
"./env.js": {
|
|
24
23
|
"development": "./env/env-dev.js",
|
|
25
24
|
"default": "./env/env-prod.js"
|
|
@@ -27,23 +26,27 @@
|
|
|
27
26
|
"./package.json": "./package.json"
|
|
28
27
|
},
|
|
29
28
|
"files": [
|
|
29
|
+
"./pwa.js",
|
|
30
|
+
"./dialog.js",
|
|
31
|
+
"./dialog/*.js",
|
|
32
|
+
"./pwa/*.js",
|
|
33
|
+
"./router.js",
|
|
34
|
+
"./router/index.js",
|
|
35
|
+
"./router/plugins/*.js",
|
|
30
36
|
"./api.js",
|
|
31
37
|
"./api/index.js",
|
|
32
|
-
"./api/plugins
|
|
33
|
-
"./api/plugins/cache.js",
|
|
34
|
-
"./api/plugins/delay.js",
|
|
35
|
-
"./api/plugins/jsonPrefix.js",
|
|
36
|
-
"./api/plugins/mock.js",
|
|
37
|
-
"./api/plugins/logger.js",
|
|
38
|
+
"./api/plugins/*.js",
|
|
38
39
|
"./state.js",
|
|
39
40
|
"./state/index.js",
|
|
40
41
|
"./utils.js",
|
|
41
|
-
"./utils
|
|
42
|
+
"./utils/*.js",
|
|
42
43
|
"./env/env-dev.js",
|
|
43
44
|
"./env/env-prod.js"
|
|
44
45
|
],
|
|
45
46
|
"keywords": [
|
|
47
|
+
"router",
|
|
46
48
|
"state",
|
|
49
|
+
"pwa",
|
|
47
50
|
"api",
|
|
48
51
|
"fetch",
|
|
49
52
|
"client",
|
package/pwa/events.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `'installable'` event
|
|
3
|
+
* @example pwa.dispatchEvent(new InstallableEvent());
|
|
4
|
+
*/
|
|
5
|
+
export class InstallableEvent extends Event {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('installable');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `'installed'` event
|
|
13
|
+
* @example pwa.dispatchEvent(new InstalledEvent(installed));
|
|
14
|
+
*/
|
|
15
|
+
export class InstalledEvent extends Event {
|
|
16
|
+
/**
|
|
17
|
+
* @param {boolean} installed
|
|
18
|
+
*/
|
|
19
|
+
constructor(installed) {
|
|
20
|
+
super('installed');
|
|
21
|
+
this.installed = installed;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `'update-available'` event
|
|
27
|
+
* @example pwa.dispatchEvent(new UpdateAvailableEvent());
|
|
28
|
+
*/
|
|
29
|
+
export class UpdateAvailableEvent extends Event {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('update-available');
|
|
32
|
+
}
|
|
33
|
+
}
|