@akc42/app-utils 3.1.5
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/LICENSE +674 -0
- package/README.md +172 -0
- package/app-keys.js +294 -0
- package/config-promise.js +72 -0
- package/csv.js +37 -0
- package/debug.js +109 -0
- package/dom-host.js +31 -0
- package/master-tab-promise.js +99 -0
- package/package.json +24 -0
- package/pdf.js +23 -0
- package/post-api.js +54 -0
- package/submit-function.js +91 -0
- package/switch-path.js +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# app-utils
|
|
2
|
+
General utilities that I use for building SPAs
|
|
3
|
+
|
|
4
|
+
New from release 3.0
|
|
5
|
+
|
|
6
|
+
config-promise is a function which returns both a promise for the config AND adds the config items to sessionStorage.
|
|
7
|
+
|
|
8
|
+
debug has been replaced with a version that works differently and is incompatible with our previous usage (hence the bump to version 3)
|
|
9
|
+
|
|
10
|
+
# app-keys
|
|
11
|
+
|
|
12
|
+
`keys` is a module that provides keyboard support by normalising key
|
|
13
|
+
handling amongst browsers and providing a simple interface that can be used
|
|
14
|
+
by the application. Each keyboard usage request the user create a new
|
|
15
|
+
instance of the AppKeys class passing the key target in to the constructor.
|
|
16
|
+
This module will add and event handler for itself to that key target for the
|
|
17
|
+
keydown event. On that event it will parse the key combinations pressed and
|
|
18
|
+
fire a "key-pressed" event on the target.
|
|
19
|
+
|
|
20
|
+
The key grammer is of the form `[<modifier>+]<key>[:<event>]` and each of these is
|
|
21
|
+
space separated. optional `<modifiers>` (default none) are shift ctrl alt meta, and the optional
|
|
22
|
+
`<events>` are keydown and keyup (keydown is used as default)
|
|
23
|
+
|
|
24
|
+
To allow a using module the freedom to connect and disconnect to the dom, we
|
|
25
|
+
provide two methods to be called during the disconnectCallback and
|
|
26
|
+
connectCallback to disconnect and reconnect to this event
|
|
27
|
+
|
|
28
|
+
There are two models of using this, the first, shown below allows an element to add
|
|
29
|
+
the keys event to document body. This is good for pages that are swapped in and
|
|
30
|
+
out by a paging mechanism so that only one page is in the dom at a given time This is
|
|
31
|
+
good if only one handler is needed for the entire page.
|
|
32
|
+
|
|
33
|
+
NOTE: If you take this approach limit the keys to non function keys as the main app, with its main
|
|
34
|
+
menu may add a handler for the function keys and both menus, and any actions you choose would run at the same time.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
...
|
|
40
|
+
this._keyPressed = this._keyPressed.bind(this);
|
|
41
|
+
}
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
super.connectedCallback();
|
|
44
|
+
document.body.addEventListener('key-pressed', this._keyPressed);
|
|
45
|
+
if (this.keys === undefined) {
|
|
46
|
+
this.keys = new AppKeys(document.body, 'Enter Esc'); //list the keys you want to identify (see comment above for grammer)
|
|
47
|
+
} else {
|
|
48
|
+
this.keys.connect();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
disconnectedCallback() {
|
|
52
|
+
super.disconnectedCallback();
|
|
53
|
+
this.keys.disconnect();
|
|
54
|
+
document.body.removeEventListener('key-pressed', this._keyPressed);
|
|
55
|
+
}
|
|
56
|
+
_keyPressed(e) {
|
|
57
|
+
e.detail is an string containing the [<modifier>+]<key> combination from one you requested
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The second way of using this, better for when you don't want to react unless a particular area of the page has focus, or
|
|
62
|
+
if there are different areas of the page needing the respond depending which area
|
|
63
|
+
has focus. This is to create the AppKeys object in the firstUpdated function. Here is a possible example (just one area)
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
super.connectedCallback();
|
|
68
|
+
if (this.keys !== undefined) this.keys.connect();
|
|
69
|
+
}
|
|
70
|
+
disconnectedCallback() {
|
|
71
|
+
super.disconnectedCallback();
|
|
72
|
+
this.keys.disconnect();
|
|
73
|
+
}
|
|
74
|
+
firstUpdated() {
|
|
75
|
+
this.target = this.shadowRoot.querySelector('#keyreceive');
|
|
76
|
+
this.keys = new AppKeys(this.target, 'Enter'); //replace 'Enter' with space separated string of keys
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
Then on the dom element in the Render function
|
|
80
|
+
|
|
81
|
+
`<div id="keyreceive" @keys-pressed=${this._keyPressed}>Contents</div>`
|
|
82
|
+
|
|
83
|
+
NOTE: There is no need to bind this._keyPressed to this (as shown in the first example). Lit manages it.
|
|
84
|
+
|
|
85
|
+
If for any reason you want more info about the keyPress, access
|
|
86
|
+
`this.keys.lastPressed`, and it will return the complete binding object
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# config-promise
|
|
90
|
+
|
|
91
|
+
This module provides two functions:-
|
|
92
|
+
|
|
93
|
+
1. This first is the default function which when called returns a promise.
|
|
94
|
+
2. a `mockConfig` function which can be used by a test harness to provide the promise returned by the default function
|
|
95
|
+
|
|
96
|
+
If a request is made for the promise and it hasn't yet been created a new promise is made with a fetch get request to `/api/config`
|
|
97
|
+
to retrieve the configuration data. If the server is down, this request will continue attempting to get the value once a minute ad
|
|
98
|
+
infinitum.
|
|
99
|
+
|
|
100
|
+
The configuration data is returned to resolve the promise, but local session
|
|
101
|
+
storage also has one item added to it for each of the first level properties
|
|
102
|
+
of this data. If the property is a String, this is stored in the session
|
|
103
|
+
storage as is, otherwise the value is passed through `JSON.stringify` and that is stored.
|
|
104
|
+
|
|
105
|
+
# csv
|
|
106
|
+
|
|
107
|
+
the modules default export is a function which can be called with a name and optionally a parameters object. The name is added to `/api/csv/` to make a download request and if they exist the parameters object is turned into a query string. The response from that uri is downloaded (expected to be a csv file).
|
|
108
|
+
|
|
109
|
+
# debug
|
|
110
|
+
|
|
111
|
+
The purpose of this module is to provide a debugable capability which can be
|
|
112
|
+
dynamically switched on and off browser by setting a key in the config
|
|
113
|
+
returned by the server. It will post request to `/api/log` url with an
|
|
114
|
+
`application/json` body part containing message, topic and gap, where message is
|
|
115
|
+
the concatenation of the debug parameters separated by space, topic is the
|
|
116
|
+
topic of this debug message and gap is the number of milliseconds since the
|
|
117
|
+
last debug message with the same topic.
|
|
118
|
+
|
|
119
|
+
Topic data is held in a map, so this module can be used in multiple modules in
|
|
120
|
+
the client and if its the same topic then the gap will be since the last call
|
|
121
|
+
from **any** module.
|
|
122
|
+
|
|
123
|
+
To use the debug function in your code, import this module then set the topic
|
|
124
|
+
as shown.
|
|
125
|
+
|
|
126
|
+
import Debug from 'debug.js';
|
|
127
|
+
|
|
128
|
+
const debug = Debug('topic') topic should only contain the characters a-z or
|
|
129
|
+
A-Z as is converted to lowercase.
|
|
130
|
+
|
|
131
|
+
debug(..messages) //messages will be concatenated by space
|
|
132
|
+
|
|
133
|
+
the debug function will only log the message if config.debug (see
|
|
134
|
+
config-promise) is set to a string which is a comma separated list of topics
|
|
135
|
+
and that list has the topic for this debug call in it.
|
|
136
|
+
|
|
137
|
+
**Note**: the debug function checks with the server (via a debugconf api call)
|
|
138
|
+
to see if the topic is enabled. This is then cached for a minute, so any
|
|
139
|
+
calls around the same time will use the reply. This allows the server to
|
|
140
|
+
change what topics are available and for the client side to quickly find if it
|
|
141
|
+
should now start sending message
|
|
142
|
+
|
|
143
|
+
# dom-host
|
|
144
|
+
|
|
145
|
+
the default function of this module is called with a single parameter (normally `this` inside a custom element)
|
|
146
|
+
and it finds the parent. Its useful for custom elements to sit at the top level of the custom element above and listen for
|
|
147
|
+
events bubbling up from below. It uses this function to find the element to add an event listener to.
|
|
148
|
+
|
|
149
|
+
# master-tab-promise
|
|
150
|
+
|
|
151
|
+
the default function returns a promise, which when resolves will tell you if you are the first (and therefore master) tab in a particular application.
|
|
152
|
+
|
|
153
|
+
if the master tab closes an custom Event 'master-close' is dispatched on the window. You can use that to recheck the promise to see if you (or perhaps some other tab also still open is now master);
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# pdf
|
|
157
|
+
|
|
158
|
+
the modules default export is a function which can be called with a name and optionally a parameters object. The name is added to `/api/pdf/` to post a message to the server, expecting it to stream a precreated, or created on the fly pdf document. This is opened in a new window.
|
|
159
|
+
|
|
160
|
+
# post-api
|
|
161
|
+
|
|
162
|
+
Provides a wrapper found the `fetch` api with error handling and built it post message support to a url prefixed with `/api/`.
|
|
163
|
+
|
|
164
|
+
# submit-function
|
|
165
|
+
|
|
166
|
+
Acts as the on-submit handler for a form. But instead of allowing the form to send itself it creates an ajax request with all the correct parameters.
|
|
167
|
+
In doing this it can traverse inside custom elements looking for input elements. It will also check for custom elements and if they have both a name and value property if can pretend to be in input. Also slots are also traversed for all assigned nodes.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# switch-path
|
|
171
|
+
|
|
172
|
+
Changed window location based on parameters provided
|
package/app-keys.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2020 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
`keys` is a module that provides keyboard support by normalising key
|
|
23
|
+
handling amongst browsers and providing a simple interface that can be used
|
|
24
|
+
by the application. Each keyboard usage request the user create a new
|
|
25
|
+
instance of the AppKeys class passing the key target in to the constructor.
|
|
26
|
+
This module will add and event handler for itself to that key target for the
|
|
27
|
+
keydown event. On that event it will parse the key combinations pressed and
|
|
28
|
+
fire a "key-pressed" event on the target.
|
|
29
|
+
|
|
30
|
+
The key grammer is of the form [<modifier>+]<key>[:<event>] and each of these is
|
|
31
|
+
space separated. optional <modifiers> (default none) are shift ctrl alt meta, and the optional
|
|
32
|
+
<events> are keydown and keyup (keydown is used as default)
|
|
33
|
+
|
|
34
|
+
To allow a using module the freedom to connect and disconnect to the dom, we
|
|
35
|
+
provide two methods to be called during the disconnectCallback and
|
|
36
|
+
connectCallback to disconnect and reconnect to this event
|
|
37
|
+
|
|
38
|
+
There are two models of using this, the first, shown below allows an element to add
|
|
39
|
+
the keys event to document body. This is good for pages that are swapped in and
|
|
40
|
+
out by a paging mechanism so that only one page is in the dom at a given time This is
|
|
41
|
+
good if only one handler is needed for the entire page.
|
|
42
|
+
|
|
43
|
+
NOTE: If you take this approach limit the keys to non function keys as the main app, with its main
|
|
44
|
+
menu may add a handler for the function keys and both menus, and any actions you choose would run at the same time.
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
...
|
|
49
|
+
this._keyPressed = this._keyPressed.bind(this);
|
|
50
|
+
}
|
|
51
|
+
connectedCallback() {
|
|
52
|
+
super.connectedCallback();
|
|
53
|
+
document.body.addEventListener('key-pressed', this._keyPressed);
|
|
54
|
+
if (this.keys === undefined) {
|
|
55
|
+
this.keys = new AppKeys(document.body, 'Enter Esc'); //list the keys you want to identify (see comment above for grammer)
|
|
56
|
+
} else {
|
|
57
|
+
this.keys.connect();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
this.keys.disconnect();
|
|
63
|
+
document.body.removeEventListener('key-pressed', this._keyPressed);
|
|
64
|
+
}
|
|
65
|
+
_keyPressed(e) {
|
|
66
|
+
e.detail is an string containing the [<modifier>+]<key> combination from one you requested
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
The second way of using this, better for when you don't want to react unless a particular area of the page has focus, or
|
|
70
|
+
if there are different areas of the page needing the respond depending which area
|
|
71
|
+
has focus. This is to create the AppKeys object in the firstUpdated function. Here is a possible example (just one area)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
connectedCallback() {
|
|
75
|
+
super.connectedCallback();
|
|
76
|
+
if (this.keys !== undefined) this.keys.connect();
|
|
77
|
+
}
|
|
78
|
+
disconnectedCallback() {
|
|
79
|
+
super.disconnectedCallback();
|
|
80
|
+
this.keys.disconnect();
|
|
81
|
+
}
|
|
82
|
+
firstUpdated() {
|
|
83
|
+
this.target = this.shadowRoot.querySelector('#keyreceive');
|
|
84
|
+
this.keys = new AppKeys(this.target, 'Enter'); //replace 'Enter' with space separated string of keys
|
|
85
|
+
}
|
|
86
|
+
Then on the dom element in the Render function
|
|
87
|
+
<div id="keyreceive" @keys-pressed=${this._keyPressed}>Contents</div>
|
|
88
|
+
|
|
89
|
+
NOTE: There is no need to bind this._keyPressed to this (as in the first example). Lit manages it.
|
|
90
|
+
|
|
91
|
+
If for any reason you want more info about the keyPress, access
|
|
92
|
+
this.keys.lastPressed, and it will return the complete binding object
|
|
93
|
+
|
|
94
|
+
NOTE: I considered making this a mixin to avoid some of this verbosity, but decided in
|
|
95
|
+
the end the extra was worth it for the flexibility of the different use cases.
|
|
96
|
+
*/
|
|
97
|
+
/*
|
|
98
|
+
Initial constants are taken from iron-a11y-keys behavior with the following licence. This same licences applies to those portions.
|
|
99
|
+
|
|
100
|
+
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
101
|
+
This code may only be used under the BSD style license found at
|
|
102
|
+
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
|
103
|
+
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
|
104
|
+
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
|
105
|
+
part of the polymer project is also subject to an additional IP rights grant
|
|
106
|
+
found at http://polymer.github.io/PATENTS.txt
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Special table for KeyboardEvent.keyCode. only used when KeyBoardEvent.key can't be
|
|
111
|
+
*
|
|
112
|
+
* Values from:
|
|
113
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
const KEY_CODE = {
|
|
117
|
+
8: 'backspace',
|
|
118
|
+
9: 'tab',
|
|
119
|
+
13: 'enter',
|
|
120
|
+
27: 'esc',
|
|
121
|
+
33: 'pageup',
|
|
122
|
+
34: 'pagedown',
|
|
123
|
+
35: 'end',
|
|
124
|
+
36: 'home',
|
|
125
|
+
32: 'space',
|
|
126
|
+
37: 'left',
|
|
127
|
+
38: 'up',
|
|
128
|
+
39: 'right',
|
|
129
|
+
40: 'down',
|
|
130
|
+
46: 'del',
|
|
131
|
+
106: '*'
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* MODIFIER_KEYS maps the short name for modifier keys used in a key
|
|
135
|
+
* combo string to the property name that references those same keys
|
|
136
|
+
* in a KeyboardEvent instance.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
const MODIFIER_KEYS = {
|
|
140
|
+
'shift': 'shift',
|
|
141
|
+
'ctrl': 'control',
|
|
142
|
+
'alt': 'alt',
|
|
143
|
+
'meta': 'meta'
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* KeyboardEvent.key is mostly represented by printable character made by
|
|
147
|
+
* the keyboard, with unprintable keys labeled nicely.
|
|
148
|
+
*
|
|
149
|
+
* However, on OS X, Alt+char can make a Unicode character that follows an
|
|
150
|
+
* Apple-specific mapping. In this case, we fall back to .keyCode.
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
const KEY_CHAR = /[a-z0-9*]/;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Matches arrow keys in Gecko 27.0+
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
const ARROW_KEY = /^arrow/;
|
|
160
|
+
/**
|
|
161
|
+
* Matches space keys everywhere (notably including IE10's exceptional name
|
|
162
|
+
* `spacebar`).
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
const SPACE_KEY = /^space(bar)?/;
|
|
166
|
+
/**
|
|
167
|
+
* Matches ESC key.
|
|
168
|
+
*
|
|
169
|
+
* Value from: http://w3c.github.io/uievents-key/#key-Escape
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
const ESC_KEY = /^escape$/;
|
|
173
|
+
|
|
174
|
+
class AppKeys {
|
|
175
|
+
constructor(target, keys, stop) {
|
|
176
|
+
this.stop = stop; //marker to say stop propagation;
|
|
177
|
+
if (!(target instanceof HTMLElement)) throw new Error('AppKeys required an HTML Element as target');
|
|
178
|
+
this.target = target;
|
|
179
|
+
this.lastPressed = null;
|
|
180
|
+
const keyArray = keys.split(' ');
|
|
181
|
+
this.eventBindings = {};
|
|
182
|
+
for (let keyString of keyArray) {
|
|
183
|
+
const [keyCombo, event] = keyString.split(':');
|
|
184
|
+
const keyBinding = { combo: keyCombo, modifiers: [], hasModifiers: false };
|
|
185
|
+
const keyComboArray = keyCombo.split('+');
|
|
186
|
+
keyBinding.key = keyComboArray.pop();
|
|
187
|
+
if (keyBinding.key.toLowerCase() in MODIFIER_KEYS) {
|
|
188
|
+
//we asked for a modifier so push it back
|
|
189
|
+
keyComboArray.push(keyBinding.key);
|
|
190
|
+
}
|
|
191
|
+
if (keyComboArray.length === 0) {
|
|
192
|
+
//There were no modifiers, but what if we want a single upperCase char
|
|
193
|
+
if (keyBinding.key.length === 1 && keyBinding.key.toLowerCase() !== keyBinding.key) {
|
|
194
|
+
keyBinding.hasModifiers = true;
|
|
195
|
+
keyBinding.modifiers.push('shift');
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
for (let modifier of keyComboArray) {
|
|
199
|
+
modifier = modifier.toLowerCase();
|
|
200
|
+
if (modifier in MODIFIER_KEYS) {
|
|
201
|
+
keyBinding.hasModifiers = true;
|
|
202
|
+
keyBinding.modifiers.push(modifier);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
keyBinding.key = keyBinding.key.toLowerCase();
|
|
207
|
+
if (this.eventBindings[event ? event : 'keydown'] === undefined) {
|
|
208
|
+
this.eventBindings[event ? event : 'keydown'] = { keyBindings: [] };
|
|
209
|
+
}
|
|
210
|
+
this.eventBindings[event ? event : 'keydown'].keyBindings.push(keyBinding);
|
|
211
|
+
}
|
|
212
|
+
this._bindHandlers();
|
|
213
|
+
}
|
|
214
|
+
connect() {
|
|
215
|
+
if (!this.handlersBound) this._bindHandlers();
|
|
216
|
+
}
|
|
217
|
+
disconnect() {
|
|
218
|
+
if (this.handlersBound) {
|
|
219
|
+
for (let event in this.eventBindings) {
|
|
220
|
+
this.target.removeEventListener(event, this.eventBindings[event].boundHandler);
|
|
221
|
+
}
|
|
222
|
+
this.handlersBound = false;
|
|
223
|
+
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
_bindHandlers() {
|
|
227
|
+
for (let event in this.eventBindings) {
|
|
228
|
+
this.eventBindings[event].boundHandler = this._keyHandler.bind(this, this.eventBindings[event].keyBindings);
|
|
229
|
+
this.target.addEventListener(event, this.eventBindings[event].boundHandler);
|
|
230
|
+
}
|
|
231
|
+
this.handlersBound = true;
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
_keyHandler(keyBindings, e) {
|
|
235
|
+
if (e.defaultPrevented) return;
|
|
236
|
+
const key = e.key.toLowerCase();
|
|
237
|
+
let validKey = '';
|
|
238
|
+
|
|
239
|
+
if (key === ' ' || SPACE_KEY.test(key)) {
|
|
240
|
+
validKey = 'space';
|
|
241
|
+
} else if (ESC_KEY.test(key)) {
|
|
242
|
+
validKey = 'esc';
|
|
243
|
+
} else if (key.length == 1) {
|
|
244
|
+
if (KEY_CHAR.test(key)) {
|
|
245
|
+
validKey = key;
|
|
246
|
+
} else {
|
|
247
|
+
if (e.keyCode) {
|
|
248
|
+
if (e.keyCode in KEY_CODE) {
|
|
249
|
+
validKey = KEY_CODE[e.keyCode];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (ARROW_KEY.test(key)) {
|
|
254
|
+
validKey = key.replace('arrow', '');
|
|
255
|
+
} else if (key == 'multiply') {
|
|
256
|
+
// numpad '*' can map to Multiply on IE/Windows
|
|
257
|
+
validKey = '*';
|
|
258
|
+
} else {
|
|
259
|
+
validKey = key;
|
|
260
|
+
}
|
|
261
|
+
for (let binding of keyBindings) {
|
|
262
|
+
if (binding.hasModifiers) {
|
|
263
|
+
let modifierNotFound = false;
|
|
264
|
+
for (let modifier of binding.modifiers) {
|
|
265
|
+
if (!e[modifier + 'Key']) {
|
|
266
|
+
modifierNotFound = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (modifierNotFound) return; //didn't have the right combination of modifiers
|
|
271
|
+
if (binding.key in MODIFIER_KEYS) {
|
|
272
|
+
//we only wanted the modifiers so if our key IS the modifier we wanted
|
|
273
|
+
if (MODIFIER_KEYS[binding.key] === validKey) {
|
|
274
|
+
this.lastPressed = binding;
|
|
275
|
+
if (this.stop) e.stopPropagation();
|
|
276
|
+
if (!this.target.dispatchEvent(new CustomEvent('key-pressed', {bubbles: true, composed: true, cancelable: true, detail: binding.combo}))) {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (binding.key === validKey) {
|
|
284
|
+
this.lastPressed = binding
|
|
285
|
+
if (this.stop) e.stopPropagation();
|
|
286
|
+
if (!this.target.dispatchEvent(new CustomEvent('key-pressed', { bubbles: true, composed: true, cancelable: true, detail: binding.combo }))) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export default AppKeys;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2020 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
let configPromise;
|
|
22
|
+
|
|
23
|
+
export function mockConfig(promise) {
|
|
24
|
+
configPromise = promise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function config() {
|
|
28
|
+
if (configPromise === undefined) {
|
|
29
|
+
let resolved = false;
|
|
30
|
+
let resolver;
|
|
31
|
+
configPromise = new Promise(accept => resolver = accept);
|
|
32
|
+
let text;
|
|
33
|
+
let config;
|
|
34
|
+
while (!resolved) {
|
|
35
|
+
try {
|
|
36
|
+
const response = await window.fetch('/api/config', { method: 'get' })
|
|
37
|
+
if (!response.ok) throw new CustomEvent('api-error', { composed: true, bubbles: true, detail: response.status });
|
|
38
|
+
text = await response.text();
|
|
39
|
+
config = JSON.parse(text);
|
|
40
|
+
/*
|
|
41
|
+
some uses of this put the config in sessionStorage, but that only works if every config value is a string
|
|
42
|
+
so we check if they are a string, otherwise we stringify them
|
|
43
|
+
*/
|
|
44
|
+
for (const p in config) {
|
|
45
|
+
const v = config[p];
|
|
46
|
+
if (typeof v === 'string') {
|
|
47
|
+
sessionStorage.setItem(p, v);
|
|
48
|
+
} else {
|
|
49
|
+
sessionStorage.setItem(p,JSON.stringify(v));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
resolver(config);
|
|
53
|
+
resolved = true;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.detail === 502) {
|
|
56
|
+
//server down so wait a minute and try again;
|
|
57
|
+
await new Promise(accept => {
|
|
58
|
+
setTimeout(accept, 60000);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
if (err.type === 'api-error') throw err; //just throw whatever error we had
|
|
62
|
+
//we failed to parse the json - the actual code should be in the text near the end;
|
|
63
|
+
throw new CustomEvent('api-error', { composed: true, bubbles: true, detail: parseInt(text.substr(-6, 3), 10) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return configPromise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export default config;
|
package/csv.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2021 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {generateUri} from './switch-path.js';
|
|
22
|
+
const link = document.createElement('a');
|
|
23
|
+
link.setAttribute('download', null);
|
|
24
|
+
|
|
25
|
+
const csv = (url, params) => {
|
|
26
|
+
let href;
|
|
27
|
+
if (typeof url == 'string') {
|
|
28
|
+
href = generateUri(`/api/csv/${url}`, params ?? {});
|
|
29
|
+
} else {
|
|
30
|
+
//we assume it is a URL object
|
|
31
|
+
href = url.href;
|
|
32
|
+
}
|
|
33
|
+
link.setAttribute('href', href);
|
|
34
|
+
link.click();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default csv;
|
package/debug.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@licence
|
|
3
|
+
Copyright (c) 2021 Alan Chandler, all rights reserved
|
|
4
|
+
|
|
5
|
+
This file is part of @akc42/app-utils.
|
|
6
|
+
|
|
7
|
+
@akc42/app-utils is free software: you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
@akc42/app-utils is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
The purpose of this module is to provide a debugable capability which can be
|
|
23
|
+
dynamically switched on and off browser by setting a key in the config
|
|
24
|
+
returned by the server. It will post request to '/api/log' url with
|
|
25
|
+
application/json body part containing message, topic and gap, where message is
|
|
26
|
+
the concatenation of the debug parameters separated by space, topic is the
|
|
27
|
+
topic of this debug message and gap is the number of milliseconds since the
|
|
28
|
+
last debug message with the same topic.
|
|
29
|
+
|
|
30
|
+
Topic data is held in a map, so this module can be used in multiple modules in
|
|
31
|
+
the client and if its the same topic then the gap will be since the last call
|
|
32
|
+
from ANY module.
|
|
33
|
+
|
|
34
|
+
To use the debug function in your code, import this module then set the topic
|
|
35
|
+
as shown.
|
|
36
|
+
|
|
37
|
+
import Debug from 'debug.js';
|
|
38
|
+
|
|
39
|
+
const debug = Debug('topic') topic should only contain the characters a-z or
|
|
40
|
+
A-Z as is converted to lowercase.
|
|
41
|
+
|
|
42
|
+
debug(..messages) //messages will be concatenated by space
|
|
43
|
+
|
|
44
|
+
the debug function will only log the message if config.debug (see
|
|
45
|
+
config-promise) is set to a string which is a comma separated list of topics
|
|
46
|
+
and that list has the topic for this debug call in it.
|
|
47
|
+
|
|
48
|
+
NOTE: It is normally expected for the server to provide a mechanism to update
|
|
49
|
+
the config before it is returned and for the client to be restarted to enable
|
|
50
|
+
the appropriate debug topics, an alternative could be for client side function
|
|
51
|
+
to use the `mockConfig` call to replace the promise with one which contained a
|
|
52
|
+
different list of topics. debug checks the list of topics on every call so
|
|
53
|
+
would dynamically pick up the changes
|
|
54
|
+
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const topicMap = new Map();
|
|
58
|
+
|
|
59
|
+
import api from './post-api.js';
|
|
60
|
+
|
|
61
|
+
function Debug (t) {
|
|
62
|
+
if (typeof t !== 'string' || t.length === 0 || !/^[a-zA-Z]+$/.test(t)) {
|
|
63
|
+
console.error('Debug requires topic which is a non zero length string of only letters', t, 'Received');
|
|
64
|
+
throw new Error('Invalid Debug Topic');
|
|
65
|
+
}
|
|
66
|
+
const tl = t.toLowerCase();
|
|
67
|
+
|
|
68
|
+
if (topicMap.has(tl) ) {
|
|
69
|
+
const topic = topicMap.get(tl);
|
|
70
|
+
return topic.debug;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const topicHandler = {
|
|
74
|
+
topic: tl,
|
|
75
|
+
timestamp: new Date().getTime(),
|
|
76
|
+
expires: 0, //when our knowledge of enabled expires
|
|
77
|
+
enabled: false, //is this topic enabled
|
|
78
|
+
debug: async function (...args) {
|
|
79
|
+
//do time calc before potential delay to see if we are enabled
|
|
80
|
+
const now = new Date().getTime();
|
|
81
|
+
const gap = now - this.timestamp;
|
|
82
|
+
this.timestamp = now;
|
|
83
|
+
if (now > this.expires) {
|
|
84
|
+
//do this first so following requests don't try to find out again whilst I am checking, wasting calls
|
|
85
|
+
this.expires = now + 60000;
|
|
86
|
+
//expired so find out if topic is enabled
|
|
87
|
+
this.enabled = await api(`debugconf/${this.topic}`);
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
if (this.enabled) {
|
|
91
|
+
const message = args.reduce((cum, arg) => {
|
|
92
|
+
return `${cum} ${arg}`.trim();
|
|
93
|
+
}, '');
|
|
94
|
+
console.log(`+${gap}ms`, this.topic, message);
|
|
95
|
+
const blob = new Blob([JSON.stringify({
|
|
96
|
+
message: message,
|
|
97
|
+
gap: gap
|
|
98
|
+
})], { type: 'application/json' })
|
|
99
|
+
|
|
100
|
+
navigator.sendBeacon(`/api/debuglog/${this.topic}`, blob);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
topicHandler.debug = topicHandler.debug.bind(topicHandler);
|
|
105
|
+
topicMap.set(topicHandler.topic, topicHandler);
|
|
106
|
+
return topicHandler.debug
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default Debug;
|