@thepassle/app-tools 0.0.11 → 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 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 { beforeFetch } of plugins) {
102
- const overrides = await beforeFetch?.({ responseType, headers, fetchFn, baseURL, url, method, opts, data });
103
- if(overrides) {
104
- ({ responseType, headers, fetchFn, baseURL, url, method, opts, data } = {...overrides});
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 { afterFetch } of plugins) {
125
- res = await afterFetch?.(res) ?? res;
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 { transform } of plugins) {
136
- data = await transform?.(data) ?? data;
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
  });
@@ -6,6 +6,7 @@ export function abortPlugin() {
6
6
  const requests = new Map();
7
7
 
8
8
  return {
9
+ name: 'abort',
9
10
  beforeFetch: (meta) => {
10
11
  const { method, url } = meta;
11
12
  requestId = `${method}:${url}`;
@@ -1,21 +1,22 @@
1
1
  const TEN_MINUTES = 1000 * 60 * 10;
2
2
 
3
3
  /**
4
- * @param {{maxAge?: number}} [options]
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 || TEN_MINUTES)) {
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();
@@ -2,6 +2,9 @@
2
2
  * @param {number} ms
3
3
  * @returns {import('../index').Plugin}
4
4
  */
5
- export const delayPlugin = (ms) => ({ afterFetch: () => new Promise(r => setTimeout(r,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);
@@ -5,6 +5,7 @@
5
5
  export function jsonPrefixPlugin(jsonPrefix) {
6
6
  let responseType;
7
7
  return {
8
+ name: 'jsonPrefix',
8
9
  beforeFetch: ({responseType: type}) => {
9
10
  responseType = type;
10
11
  },
@@ -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]);
@@ -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
+ });
@@ -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
+ }
@@ -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
+ }
@@ -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.11",
3
+ "version": "0.7.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "test": "wtr api/api.test.js --node-resolve",
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
- "./api/plugins/abort.js": "./api/plugins/abort.js",
16
- "./api/plugins/cache.js": "./api/plugins/cache.js",
17
- "./api/plugins/delay.js": "./api/plugins/delay.js",
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/abort.js",
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/index.js",
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",
@@ -0,0 +1,7 @@
1
+ export const capabilities = {
2
+ WAKELOCK: 'wakeLock' in navigator,
3
+ BADGING: 'setAppBadge' in navigator,
4
+ SHARE: 'share' in navigator,
5
+ SERVICEWORKER: 'serviceWorker' in navigator,
6
+ NOTIFICATION: 'Notification' in window
7
+ };
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
+ }