@akc42/app-utils 4.2.1 → 5.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.
package/README.md CHANGED
@@ -1,15 +1,24 @@
1
1
  # app-utils
2
2
  General utilities that I use for building SPAs
3
3
 
4
- New from release 3.0
4
+ Release 5.0 adds a lot of new functions and some major changes to existing ones. It also has been packaged into an export from a single file.
5
5
 
6
- config-promise is a function which returns both a promise for the config AND adds the config items to sessionStorage.
6
+ ## api and ApiError
7
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)
8
+ The key function is **api** which handles a post request to a `/api/<url>` server, managing the response. ApiError are
9
+ how problems with this are reported. The api call takes two essential parameters and one optional.
9
10
 
10
- # app-keys
11
+ 1. url - the url appended to `/api/` to the endpoint of the server.
12
+ 2. params - an object containing the parameters to be passed to the endpoint
13
+ 3. optional flag to indicate the response is a blob (normally a pdf) to be opened in a new window rather than as a direct response.
11
14
 
12
- `keys` is a module that provides keyboard support by normalising key
15
+ The response is aysnchronousally returned as an object. Errors are throw using the **ApiError** object.
16
+
17
+ ## ApiKeys
18
+
19
+ **ApiKeys** is a class where the new
20
+
21
+ ApiKeys is a class that provides keyboard support by normalising key
13
22
  handling amongst browsers and providing a simple interface that can be used
14
23
  by the application. Each keyboard usage request the user create a new
15
24
  instance of the AppKeys class passing the key target in to the constructor.
@@ -33,7 +42,7 @@ debug has been replaced with a version that works differently and is incompatibl
33
42
  NOTE: If you take this approach limit the keys to non function keys as the main app, with its main
34
43
  menu may add a handler for the function keys and both menus, and any actions you choose would run at the same time.
35
44
 
36
- ```
45
+ ```javascript
37
46
  constructor() {
38
47
  super();
39
48
  ...
@@ -62,7 +71,7 @@ debug has been replaced with a version that works differently and is incompatibl
62
71
  if there are different areas of the page needing the respond depending which area
63
72
  has focus. This is to create the AppKeys object in the firstUpdated function. Here is a possible example (just one area)
64
73
 
65
- ```
74
+ ```javascript
66
75
  connectedCallback() {
67
76
  super.connectedCallback();
68
77
  if (this.keys !== undefined) this.keys.connect();
@@ -85,106 +94,104 @@ debug has been replaced with a version that works differently and is incompatibl
85
94
  If for any reason you want more info about the keyPress, access
86
95
  `this.keys.lastPressed`, and it will return the complete binding object
87
96
 
97
+ ## calcTextColour
88
98
 
89
- # csv
90
-
91
- 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).
92
-
93
- # debug
94
-
95
- The purpose of this module is to provide a debugable capability which can be
96
- dynamically switched on and off browser by setting a key in the config
97
- returned by the server. It will post request to `/api/log` url with an
98
- `application/json` body part containing message, topic and gap, where message is
99
- the concatenation of the debug parameters separated by space, topic is the
100
- topic of this debug message and gap is the number of milliseconds since the
101
- last debug message with the same topic.
102
-
103
- Topic data is held in a map, so this module can be used in multiple modules in
104
- the client and if its the same topic then the gap will be since the last call
105
- from **any** module.
106
-
107
- To use the debug function in your code, import this module then set the topic
108
- as shown.
109
-
110
- import Debug from 'debug.js';
111
-
112
- const debug = Debug('topic') topic should only contain the characters a-z or
113
- A-Z as is converted to lowercase.
114
-
115
- debug(..messages) //messages will be concatenated by space
116
-
117
- the debug function will only log the message if config.debug (see
118
- config-promise) is set to a string which is a comma separated list of topics
119
- and that list has the topic for this debug call in it.
120
-
121
- **Note**: the debug function checks with the sessionStorage.getItem('debug) (via an await for the Config Promise)
122
- to see if the topic is enabled (assumes the result is a ':' separated string of topics). This allows the server to
123
- change what topics are available via the config api call.
124
-
125
- # dom-host
126
-
127
- the default function of this module is called with a single parameter (normally `this` inside a custom element)
128
- and it finds the parent. Its useful for custom elements to sit at the top level of the custom element above and listen for
129
- events bubbling up from below. It uses this function to find the element to add an event listener to.
130
-
131
- # master-tab-promise
132
-
133
- 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.
134
-
135
- 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);
136
-
137
-
138
- # pdf
99
+ This is a utility function to determine the correct foreground colour (black or white) to use with a background colour
100
+ passed as the parameter (as a hex strings with an optional proceeding `#` symbol.). The colour is returned as either `#000000` or `#ffffff`
139
101
 
140
- 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.
102
+ ## config and related
141
103
 
142
- # post-api
104
+ By importing the **config** constant you are infact resolving from promise (this `import` statement does the resolving) to make an api call to the server `/api/config` to return config data as an object. What is returned is obviously server dependant. It has two independant functions. From an application point of view `import {config} from '@akc42/app-utils` produces a config constant with the config data set.
143
105
 
144
- Provides a wrapper found the `fetch` api with error handling and built it post message support to a url prefixed with `/api/`.
106
+ Additional support is provided with
107
+
108
+ 1. **setConfig** which can optionally be given a promise which resolves to a new config (useful in testing scenarios), or causes a re-read of the config from the server, although it does not return this reread..
109
+ 2. **reReadConfig** actualy causes a re-read from the server and returns the promise for its eventual resolution (i.e
110
+ `const config = await reReadConfig()`).
145
111
 
146
- # submit-function
112
+ ## csv
147
113
 
148
- 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.
149
- 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.
114
+ the modules default export is a function which can be called with a name and optionally a parameters object. The name is formed into url to `/api/csv/<name>` 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).
150
115
 
116
+ ## debug
151
117
 
152
- # switch-path
153
-
154
- Changed window location based on parameters provided
155
-
156
- # distributed-router
157
- Distributed Client Side Router for use with a hierarchy of custom components in an Single Page Application
158
-
159
- This is a series of javascript modules that you can link together to form a distributed client router system. It links
160
- the browsers URL bar into a chained list of `Routee` objects that process a part of the segmented url. It takes in a `route` object
161
- and passes out a `subRoute` object. These are chained together, the subRoute at one level being fed into route of the next level down
162
- the hierarchy.
163
-
164
- At the top level is a pair of functions `connectUrl` and `disconnectUrl`. Which ever element will be your master controller (I generally
165
- have a `<main-app>` element which has a `<session-manager>` and `<page-manager>`, both of which are controlling pages. A `<session-manager>`
166
- page doesn't reflect in the url bar at all as the user navigates the sign in process. But once authorised, the `<page-manager>` takes over
167
- and controls which page is displayed based on the url. So it is the `<page-manager>` custom element that calls `connectUrl` in its `connectedCallback`
168
- function, and `disconnectUrl` in its `disconnectedCallback` function. `connectUrl` uses a callback function as its only parameter and this callback
169
- function gets called on url change, passing the top level `route` object.
170
-
171
- The next piece in this arrangement is a router. This is a class called `Route` and is instanciated with one required parameter and one
172
- optional parameter. The required parameter is a string containing "/" separated segments, which must either literally match the part of
173
- the url, or can start with a ":" character followed by a name, in which case we assume that that part of the url should be interpreted
174
- as a parameter. We process a new `route` (however we receive it - either via the `connectUrl` callback, or being passed into a custom
175
- element via a property/attribute) by calling the `routeChange` method, this returns a `route` object which the part of the url segment
176
- checked against the specification provdied in the `new Route()` call. `route` has an `active` property to determine if it matched and
177
- a `params` property the value of any of the ":" segments. Any queryString is also decoded and placed in the `query` property of objects.
178
-
179
- If the `active` propery of a route is false, the subRoute will also have an `active` value of false. A `query` property is always passed
180
- straight through and it is up to the application to decided how and when to use it.
181
-
182
- The optional second parameter to the `new Route()` call is a matching string for the previous route up the chain. It consists of a string
183
- which contains a single ":" character. The left of the ":" character is a parameter name, and to the right a parameter value. The incoming
184
- route's `params` property must contain the "name" and it must have the value "value" for the subRoute to be active (as well as matching the url).
118
+ The purpose of this module is to provide a debugable capability which can be
119
+ dynamically switched on and off browser by setting a key in the config
120
+ returned by the server. It will post request to `/api/debuglog/:immediate` url with an
121
+ `application/json` body part containing debug data. Its purpose is to emulate what it's
122
+ server counterpart does locally.
123
+
124
+ It consists of two functions **Debug** and **Logger**.
125
+
126
+ **Debug** creates an instance of a debug function and its called with parameters:
127
+
128
+ - **topic** - a value that can be searched for. Useful for dividing into different sections
129
+ - **colourspec** - One of name of standard colors [app,db,api,client,log,mail,auth,error],
130
+ a hex color string, an rgb, comma seperated, string of three values 0-255
131
+ - **shortdate** - if true, then dates will be output as YYYY-MM-DD hh:mm else
132
+ YYYY-MM-DD hh:mm:ss.ms
133
+ - **immediate** - if true, the message is output (formatted) to the console, both in the
134
+ browser and in the server.
135
+
136
+ Calling this function returns a function that will send a row to the server (for writing into the log there), using the
137
+ parameters above and some optional extra values these extra parameters (just skip if not provided, the function
138
+ dynamically checks them) are:-
139
+
140
+ - **crash** - the literal word "crash". if set, then this will be highlighted in the output. Don't provide this as
141
+ the first parameter if a normal call
142
+ - **logtime**- a unix millisecond timestamp. If provided if must be for today, otherwise it will be as
143
+ though it were not provided. If provided it will be the `logtime`, otherwise `Date.now()` will be used.
144
+ - **ipaddress** - an optional parameter container a string representation of an ip address. Ignored if not
145
+ a valid adddress. If provided its value will be highlighted and surrounded in "[]"
146
+ - **...messages** - As many parameters containing parts of the message. The message will be joined together
147
+ with a space separation and displayed with the colourspec parameter.
148
+
149
+
150
+
151
+ **Logger** is like Debug (indeed its a wrapper for it) except it doesn't need short date, or immediate parameters as
152
+ that is what is assumed.
153
+
154
+ ## dom-host
155
+
156
+ The **domHost** function is called with a single parameter (normally `this` inside a custom element)
157
+ and it finds the parent. Its useful for custom elements to sit at the top level of the custom element above and listen for
158
+ events bubbling up from below. It uses this function to find the element to add an event listener to.
159
+
160
+ ## Client Side Routing.
161
+
162
+ A Distributed Client Side Router for use with a hierarchy of custom components in an Single Page Application. The
163
+ functions of this subsystem links the browsers URL bar into a chained list of `Route` objects that process a part of the
164
+ segmented url. It takes in a `route` object and passes out a `subRoute` object. These are chained together, the
165
+ subRoute at one level being fed into route of the next level down the hierarchy.
166
+
167
+ At the top level is a pair of functions **connectUrl** and **disconnectUrl**. Which ever element will be your master
168
+ controller (generally a `<main-app>` element which has a `<session-manager>` and `<page-manager>`, both of which are
169
+ controlling pages). A `<session-manager>` page doesn't reflect in the url bar at all as the user navigates the log in
170
+ process. But once authorised, the `<page-manager>` takes over and controls which page is displayed based on the url. So
171
+ it is the `<page-manager>` custom element that calls **connectUrl** in its `connectedCallback` function, and
172
+ **disconnectUrl** in its `disconnectedCallback` function. **connectUrl** uses a callback function as its only parameter and
173
+ this callback function gets called on url change, passing the top level `route` object.
174
+
175
+ The next piece in this arrangement is a router. This is a class called **Route** and is instanciated with one required
176
+ parameter and one optional parameter. The required parameter is a string containing "/" separated segments, which must
177
+ either literally match the part of the url, or can start with a ":" character followed by a name, in which case we
178
+ assume that that part of the url should be interpreted as a parameter. We process a new `route` (however we receive it
179
+ - either via the `connectUrl` callback, or being passed into a custom element via a property/attribute) by calling the
180
+ `routeChange` method, this returns a `route` object which the part of the url segment checked against the specification
181
+ provdied in the `new Route()` call. `route` has an `active` property to determine if it matched and a `params` property
182
+ the value of any of the ":" segments. Any queryString is also decoded and placed in the `query` property of objects.
183
+
184
+ If the `active` propery of a route is false, the subRoute will also have an `active` value of false. A `query` property
185
+ is always passed straight through and it is up to the application to decided how and when to use it.
186
+
187
+ The optional second parameter to the `new Route()` call is a matching string for the previous route up the chain. It
188
+ consists of a string which contains a single ":" character. The left of the ":" character is a parameter name, and to
189
+ the right a parameter value. The incoming route's `params` property must contain the "name" and it must have the value
190
+ "value" for the subRoute to be active (as well as matching the url).
185
191
 
186
192
  This is usually used with something like this
187
- ```
193
+
194
+ ```javascript
188
195
  const topLevel = new Route('/:page');
189
196
  const firstLevel = new Route('/:id', 'page:appointments');
190
197
  connectUrl(route => {
@@ -201,13 +208,15 @@ This is usually used with something like this
201
208
  });
202
209
 
203
210
  ```
204
- (I have simplified what happens - subRoute would probably be passed in as the `route` property to a custom element which might at some point want
205
- read a database record based on id).
211
+ (I have simplified what happens - subRoute would probably be passed in as the `route` property to a custom element which
212
+ might at some point want read a database record based on id).
206
213
 
207
- In this example we only want to read the (lets say) the appointment record from the database if the `<appointment-manager>` element had been activated
208
- with a url of the form "/appointements/53" and not (say) when the url was "/user/53", when the `<user-manager>` element is in the dom and the `<appointment-manager>` is still in the dom, but not doing anything. The other obvious question is why not do this:-
214
+ In this example we only want to read the (lets say) the appointment record from the database if the
215
+ `<appointment-manager>` element had been activated with a url of the form "/appointements/53" and not (say) when the url
216
+ was "/user/53", when the `<user-manager>` element is in the dom and the `<appointment-manager>` is still in the dom, but
217
+ not doing anything. The other obvious question is why not do this:-
209
218
 
210
- ```
219
+ ```javascript
211
220
  const firstLevel = new Route('/appointments/:id');
212
221
  connectUrl(route => {
213
222
  const subRoute = topLevel.routeChange(route);
@@ -225,41 +234,57 @@ with a url of the form "/appointements/53" and not (say) when the url was "/user
225
234
  ```
226
235
  and the answer to that is that I have an element `<route-manager>` which in fact something like `<page-manager>` extends
227
236
  which then allows me to do (in `lit-element`s `render` function)
228
- ```
229
- ${ {
230
- home: html`<app-home></app-home>`,
231
- user: html`<app-user managed-page .route=${this.subRoute}></app-user>
232
- appointments: html`<app-appointments managed-page .route=${this.subRoute}></app-appointments`
233
- }[this.page]
234
- }
237
+
238
+ ```javascript
239
+ ${
240
+ {
241
+ home: html`<app-home></app-home>`,
242
+ user: html`<app-user managed-page .route=${this.subRoute}></app-user>`,
243
+ appointments: html`<app-appointments managed-page .route=${this.subRoute}></app-appointments`
244
+ }[this.page]
245
+ }
235
246
  ```
236
247
 
237
248
  The route manager users `new Route('/:page')` to translate the incoming `route` to the `page` property.
238
249
 
239
- Internally the `Route` class uses a `route-changed` event which this overall module listens to on the window and this can be used to change the url.
240
- the `Route` class has three properties that can be set and which can change the url.
250
+ Internally the `Route` class uses a `route-changed` event which this overall module listens to on the window and this
251
+ can be used to change the url. the `Route` class has three properties that can be set and which can change the url.
241
252
 
242
- - `connection` which if set `true` join the input and output of the route managed by this instance provided only that the route doesn't have any ":" segment,
243
- and change the url accordingly. If set to `false` it will always make the output disconnected.
244
- - `params` which when set with an object which maps the properties of an active `params` in the `subRoute` will change the url - so for instance in the example
245
- above calling `firstlevel.params = {id: 20}` will change the url to `/appointments/20`.
253
+ - `connection` which if set `true` join the input and output of the route managed by this instance provided only that
254
+ the route doesn't have any ":" segment, and change the url accordingly. If set to `false` it will always make the
255
+ output disconnected.
256
+ - `params` which when set with an object which maps the properties of an active `params` in the `subRoute` will change
257
+ the url - so for instance in the example above calling `firstlevel.params = {id: 20}` will change the url to
258
+ `/appointments/20`.
246
259
  - `query` we can set a query set of parameters and these will then change the url to have those query parameters.
247
260
 
248
- Other modules that wish to change the url can do so, but they need to dispatch a `location-altered` event in the window. A helper
249
- class `LocationAltered` can generate it for you, so to change the location do:-
250
- ```
251
- history.pushState({}, null, '/user/23');
252
- window.dispatchEvent(new LocationAltered());
253
- ```
254
- # config
261
+ Other modules that wish to change the url can do so, but they need to dispatch a `location-altered` event in the window.
262
+ A helper function **switchPath** can do this for you. It takes two parameters, `path` and a `params` object, the latter
263
+ of which is formed into a query string and appended (with the appropriate "?") into a string. A helper function
264
+ **generateUri** takes the same two parameters and creates the final url, it just doesn't apply the result.
265
+
266
+ **navigate** is a function to be used as an event handler on an element (normally for `@click`). It calls `switchPath` with the
267
+ `path` set to the `path` attribute of the element which fired the event.
268
+
269
+ ## master-tab-promise
270
+
271
+ The **getMasterTabPromise** function returns a promise, which when resolves will tell you if you are the first (and therefore master) tab in a particular application.
272
+
273
+ 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);
274
+
275
+ ## partMap
276
+
277
+ **partMap** is a `lit` "directive", exactly analogous to the provided `classMap`, but for the `part` attribute. Use it in the same way to dynamically assign parts to an element.
278
+
279
+ ## pdf
255
280
 
256
- Newly added in release 4.1.0, this function is used for getting the client config from the server
281
+ 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 (as Blob). This is opened in a new window.
257
282
 
258
- The default export, normally named config (ie the client does `import config from @akc42/app-utils/config.js`) and allows the normal await mechanisms in `import` to
259
- allow this module to make an get request to the server of `/api/config` and for the server to return it. The `config` variable then holds (without any further waiting)
260
- the client config object returned by the server.
261
283
 
262
- Two non default async exports are also provided. `setConfig` called with no parameters will read the config from the server, called with a promise as a parameter it expects
263
- that promise to be resolved with a config (useful in testing). The other async function is `reReadConfig` with essentially causes the config to be reRead, but does also returns a promise resolving to the config.
284
+ ## submit-function
264
285
 
286
+ Acts as the on-submit handler for a form. But instead of allowing the form to send itself it creates an ajax request
287
+ with all the correct parameters. In doing this it can traverse inside custom elements looking for input elements. It
288
+ will also check for custom elements and if they have both a name and value property if can pretend to be in input. Also
289
+ slots are also traversed for all assigned nodes.
265
290
 
package/api.js ADDED
@@ -0,0 +1,94 @@
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
+ import Debug from './debug.js';
22
+
23
+ const debug = Debug('client-api', 'api');
24
+
25
+ class ApiError extends Error {
26
+ constructor(address, code) {
27
+ super('API Error: ' + address)
28
+ this.name = 'API' + code.toString();
29
+ }
30
+ }
31
+
32
+ async function api(url, params, bl) {
33
+ const blob = bl;
34
+ const controller = new AbortController();
35
+ const address = '/api/' + url;
36
+ const options = {
37
+ mode: 'cors',
38
+ credentials: 'include',
39
+ method: 'post',
40
+ headers: new Headers({
41
+ 'content-type': 'application/json'
42
+ }),
43
+ body: new Blob([JSON.stringify(params ?? {})], { type: 'application/json' }),
44
+ signal: controller.signal
45
+ };
46
+ let text;
47
+ const timer = setTimeout(() => controller.abort('timeout'),60000); //just a protection against complete loss of response.
48
+ try {
49
+ const response = await window.fetch(address, options)
50
+ if (response.ok) {
51
+ clearTimeout(timer);
52
+ if (blob) {
53
+ text = '---502---'; //Simulate a 502 (bad gateway) incase there is an error in following.
54
+ const b = await response.blob();
55
+ window.open(
56
+ URL.createObjectURL(b),
57
+ '_blank',
58
+ 'chrome=yes,centerscreen,resizable,scrollbars,status,height=800,width=800');
59
+
60
+ document.body.dispatchEvent(new CustomEvent('wait-request',{bubbles: true, composed: true, detail: false}));
61
+ debug('request returned blob');
62
+ return {};
63
+ } else {
64
+ text = await response.text();
65
+ if (text.length > 0) {
66
+ const j = JSON.parse(text);
67
+ document.body.dispatchEvent(new CustomEvent('wait-request',{bubbles: true, composed: true, detail: false}));
68
+ debug('request returned JSON', text);
69
+ return j;
70
+ }
71
+ document.body.dispatchEvent(new CustomEvent('wait-request',{bubbles: true, composed: true, detail: false}));
72
+ debug('empty return');
73
+ return {};
74
+ }
75
+
76
+ } else if (response.status < 500) throw new ApiError(address, response.status);
77
+ } catch (err) {
78
+ //no debug needed - will report at where its thrown
79
+ clearTimeout(timer);
80
+ if (!(err instanceof TypeError)) {
81
+ //not network failure so no retry
82
+ if (err instanceof SyntaxError) {
83
+ const code = Number((text?? '---502---').slice(-6, -3));
84
+ if (code > 299) throw new ApiError(address,code);
85
+ } else if (err.name === 'AbortError') throw new ApiError(address, 504); //simulate gateway timeout
86
+ throw err; //just throw what we have
87
+ }
88
+ }
89
+ clearTimeout(timer);
90
+ return {};
91
+ }
92
+
93
+ export { api , ApiError };
94
+
package/app-utils.js ADDED
@@ -0,0 +1,17 @@
1
+ import {api, ApiError} from './api.js';
2
+ import AppKeys from './app-keys.js';
3
+ import calcTextColor from './colour.js';
4
+ import config, {setConfig, reReadConfig}from './config.js'
5
+ import csv from './csv.js';
6
+ import {Debug,Logger} from './debug.js';
7
+ import domHost from './dom-host.js';
8
+ import { connectUrl, disconnectUrl} from './location.js';
9
+ import getMasterTabPromise from './master-tab-promise.js';
10
+ import {partMap} from './partMap.js';
11
+ import pdf from './pdf.js';
12
+ import Route from './route.js';
13
+ import submit from './submit-function.js';
14
+ import {switchPath, generateUri, nagivate} from './switch-path.js';
15
+
16
+ export {api,ApiError,AppKeys,calcTextColor,config, connectUrl, csv, Debug, disconnectUrl, domHost, generateUri,
17
+ getMasterTabPromise, Logger, navigate, partMap, pdf, reReadConfig, Route,setConfig, submit, switchPath };
package/colour.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2023 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
+ Given a background colour, this function calculates what the color string from
22
+ the text should be. Backgroundcolour should be a hex string, optionally
23
+ proceeded by #
24
+ */
25
+
26
+ function calcTextColor(backgroundColor) {
27
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(backgroundColor);
28
+ if (result) {
29
+ const luminance = (0.2126 * parseInt(result[1], 16) + 0.7152 * parseInt(result[2], 16) + 0.0722 * parseInt(result[3], 16));
30
+ return (luminance < 140) ? "#ffffff" : "#000000";
31
+ }
32
+ return "#000000"
33
+ }
34
+ export default calcTextColor;
package/debug.js CHANGED
@@ -19,138 +19,54 @@
19
19
  */
20
20
 
21
21
  /*
22
- The purpose of this module is to provide a debugable capability which can be dynamically switched on and off browser
23
- by setting sessionStorage value 'debug' to the config returned by the server. It will post request to '/api/debuglog'
24
- url with application/json body part containing message, topic and gap, where message is the concatenation of the debug
25
- parameters separated by space, topic is the topic of this debug message and gap is the number of milliseconds since
26
- the last debug message with the same topic.
22
+ import {Debug} from '@akc42/app-utils'
27
23
 
28
- Topic data is held in a map, so this module can be used in multiple modules in
29
- the client and if its the same topic then the gap will be since the last call
30
- from ANY module.
24
+ Debug creates an instance of a debug function
31
25
 
32
- To use the debug function in your code, import this module then set the topic
33
- as shown.
26
+ parameters:
27
+ topic - a value that can be searched for. Useful for dividing into different sections
28
+ colourspec - One of name of standard colors [app,db,api,client,log,mail,auth,error], a hex color string, an rgb,
29
+ comma seperated, string of three values 0-255
30
+ shortdate - if true, then dates will be output as YYYY-MM-DD hh:mm else YYYY-MM-DD hh:mm:ss.ms
34
31
 
35
- import Debug from 'debug.js';
36
-
37
- const debug = Debug('topic') topic should only contain the characters a-z or
38
- A-Z as is converted to lowercase.
39
-
40
- debug(..messages) //messages will be concatenated by space
41
-
42
- the debug function will only log the message on the server if sessionStorage "debug" is set to a string which is a colon separated list of topics
43
- and that list has the topic for this debug call in it.
44
-
45
- NOTE: It is normally expected for the server to provide a mechanism to update
46
- the confgi before it is returned, The main app then overwrites sessionStorage 'debug' item with a new list of topics when you want
47
- debug to switch on and off dynamically.
48
-
49
- regardless of whether the message is logged on the server, it is also added to the performance.mark buffer
50
- so that it can be sent to the server on a crash.
51
-
52
- Although Debug is the default export this module also provides the following named exports
32
+ immediate - if set, the message is output (formatted) to the console.
53
33
 
54
- initialiseDebug - this function is used to manage tracing of debug messages regardless of whether the topic is set
55
- It also adds an event handler to handle resource buffer full events. NOTE if the buffer fills up priority is given
56
-
57
- unloadDebug - this function tidies up and reverses the initialiseDebug
58
-
59
- debugDump - perform a dump to the server (and and a clearing out of the buffered info) of the debug calls made to date
60
-
34
+ Returns a function that will send a row to the server (for writing into the log there), using the parameters above and
35
+ some optional extra values these extra parameters are
36
+
37
+ crash - the literal word "crash". if set, then this will be highlighted in the output. Don't provide this as
38
+ the first parameter if a normal call
39
+ logtime - a unix millisecond timestamp. If provided if must be for today, otherwise it will be as
40
+ though it were not provided. If provided it will be the logtime, otherwise "Now" will be used.
41
+ ipaddress - an optional parameter container a string representation of an ip address. Ignored if not
42
+ a valid adddress. If provided its value will be highlighted and surrounded in "[]"
43
+ ...messages - As many parameters containing parts of the message. The message will be joined together
44
+ with a space separation and displayed with the colourspec parameter.
61
45
  */
62
46
 
63
- const BUFFER_SIZE = 50;
64
- const KEY_TOPIC = 'key'; //topic name which will get kept from a full resource buffer when we empty it.
65
- let buffer = []; //buffer of up to 50 topic/message pairs to allow us to hold some if resource buffer becomes full;
66
-
67
- const topicMap = new Map();
68
-
69
- let initialised = false;
70
-
47
+ /*
48
+ Logger is like Debug (indeed its a wrapper for it) except
71
49
 
72
- function bufferFull() {
73
- const entries = performance.getEntriesByType('mark');
74
- performance.clearMarks();
75
- const startPoint = entries.length - BUFFER_SIZE;
76
- buffer.splice(0, buffer.length + startPoint);
77
- for (let i = 0; i < entries.length; i++) {
78
- if (entries[i].name === KEY_TOPIC || i >= startPoint) {
79
- buffer.push({ topic: entries[i].name, message: entries[i].detail, time: Math.round(entries[i].startTime) });
80
- }
81
- }
82
- }
50
+ It doesn't need short date, or immediate parameters as thats whats assumed
83
51
 
84
- function initialiseDebug() {
85
- initialised = true;
86
- buffer = [];
87
- performance.setResourceTimingBufferSize(150)
88
- window.addEventListener('resourcetimingbufferfull', bufferFull)
89
- }
52
+ */
53
+ import {DebugHelper, messageFormatter} from '@akc42/server-utils';
90
54
 
91
- function unloadDebug() {
92
- window.removeEventListener('resourcetimingbufferfull', bufferFull);
93
- }
94
- function debugDump() {
95
- bufferFull(); //this clears out the marks and gives us a buffer to now send to
96
- buffer.reverse();
97
- let message = '';
98
- for (let i = 0; i < buffer.length; i++) {
99
- message += `(${buffer[i].topic}) ${buffer[i].message} :gap ${i < buffer.length - 1 ? buffer[i].time - buffer[i + 1].time : 0}ms\n`;
55
+ export function Logger(topic, colourspec) {
56
+ const debug = Debug(topic, colourspec, 1,1);
57
+ return function(c, ip, ...args) {
58
+ const output = debug(c,ip,args);
59
+ console.log(output.message);
100
60
  }
101
- const blob = new Blob([JSON.stringify({
102
- message: message
103
- })], { type: 'application/json' });
104
- navigator.sendBeacon(`/api/debuglog/clientpath`, blob);
105
-
106
- buffer = []; //we will start our buffer from scratch again
107
-
108
61
  }
109
62
 
63
+ export function Debug(topic, colourspec, shortdate, immediate = false) {
64
+ return DebugHelper(topic, colourspec, shortdate, immediate , writer)
110
65
 
111
- function Debug(t) {
112
- if (typeof t !== 'string' || t.length === 0 || !/^[a-zA-Z]+$/.test(t)) {
113
- console.error('Debug requires topic which is a non zero length string of only letters', t, 'Received');
114
- throw new Error('Invalid Debug Topic');
115
- }
116
- const tl = t.toLowerCase();
117
- if (topicMap.has(tl)) {
118
- const topic = topicMap.get(tl);
119
- return topic.debug;
120
- }
121
-
122
- const topicHandler = {
123
- topic: tl,
124
- timestamp: new Date().getTime(),
125
- defined: false, //has the config been defined yet
126
- debug: function (...args) {
127
- //do time calc before potential delay to see if we are enabled
128
- const now = new Date().getTime();
129
- const gap = now - this.timestamp;
130
- this.timestamp = now;
131
- const message = args.reduce((cum, arg) => {
132
- return `${cum} ${arg}`.trim();
133
- }, '');
134
- if (initialised) performance.mark(this.topic, { detail: message }); //save our message locally regardless of if enabled
135
- let enabled = false;
136
- const debugConf = sessionStorage.getItem('debug');
137
- if (debugConf) {
138
- const topics = debugConf.split(':');
139
- if (topics.includes(this.topic)) enabled = true;
140
- }
141
- if (enabled) {
142
- const blob = new Blob([JSON.stringify({
143
- message: message,
144
- gap: gap
145
- })], { type: 'application/json' });
146
-
147
- navigator.sendBeacon(`/api/debuglog/${this.topic}`, blob);
148
- }
149
- }
150
- }
151
- topicHandler.debug = topicHandler.debug.bind(topicHandler);
152
- topicMap.set(topicHandler.topic, topicHandler);
153
- return topicHandler.debug
154
66
  }
155
67
 
156
- export { Debug as default, initialiseDebug, unloadDebug, debugDump };
68
+ function writer (logtime, crash, shortdate,ip, topic, message, colourspec,gap, i) {
69
+ const blob = new Blob([JSON.stringify({logtime,crash, shortdate, topic,message,colourspec,gap})], { type: 'application/json' });
70
+ navigator.sendBeacon(`/api/debuglog/${i ? 1: 0}`, blob);
71
+ return messageFormatter('no id', logtime,crash,shortdate,ip,topic,message,colourspec,gap)
72
+ }
package/location.js CHANGED
@@ -55,7 +55,7 @@ export function disconnectUrl() {
55
55
  }
56
56
 
57
57
  function urlChanged() {
58
- let path = window.decodeURIComponent(window.location.pathname);
58
+ let path = window.location.pathname;
59
59
  const slashIndex = path.lastIndexOf('/');
60
60
  if (path.substring(slashIndex + 1).indexOf('.') >= 0) {
61
61
  //we have a '.' in the last part of the path, so cut off this segment
package/package.json CHANGED
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "name": "@akc42/app-utils",
3
- "version": "4.2.1",
3
+ "version": "5.0.0",
4
4
  "description": "General Utilities for SPAs",
5
- "exports": {
6
- ".": "./*.js"
7
- },
5
+ "mains": "app-utils.js",
8
6
  "scripts": {
9
7
  "test": "echo \"Error: no test specified\" && exit 1"
10
8
  },
@@ -20,5 +18,9 @@
20
18
  "bugs": {
21
19
  "url": "https://github.com/akc42/app-utils/issues"
22
20
  },
23
- "homepage": "https://github.com/akc42/app-utils#readme"
21
+ "homepage": "https://github.com/akc42/app-utils#readme",
22
+ "dependencies": {
23
+ "@akc42/server-utils": "^3.4.2",
24
+ "lit": "^3.3.1"
25
+ }
24
26
  }
package/partMap.js ADDED
@@ -0,0 +1,117 @@
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
+ * Much of this code is copied (as a model of what to do) from the classMap directive in Googles lit package.
22
+ * Copyright 2018 Google LLC
23
+ * SPDX-License-Identifier: BSD-3-Clause
24
+ */
25
+
26
+ import {noChange} from 'lit'
27
+ import {directive,Directive,PartType} from 'lit/directive';
28
+
29
+ class PartMapDirective extends Directive {
30
+
31
+ constructor(partInfo) {
32
+ super(partInfo);
33
+
34
+ if (partInfo.type !== PartType.ATTRIBUTE || partInfo.name !== 'part' || partInfo.strings?.length > 2) {
35
+ throw new Error(
36
+ '`partMap()` can only be used in the `part` attribute ' +
37
+ 'and must be the only part in the attribute.'
38
+ );
39
+ }
40
+ }
41
+
42
+ render(partInfo) {
43
+ // Add spaces to ensure separation from static parts
44
+ return (
45
+ ' ' +
46
+ Object.keys(partInfo)
47
+ .filter((key) => partInfo[key])
48
+ .join(' ') +
49
+ ' '
50
+ );
51
+ }
52
+
53
+ update(part, [partInfo]) {
54
+ // Remember dynamic parts on the first render
55
+ if (this._previousParts === undefined) {
56
+ this._previousParts = new Set();
57
+ if (part.strings !== undefined) {
58
+ this._staticParts = new Set(
59
+ part.strings
60
+ .join(' ')
61
+ .split(/\s/)
62
+ .filter((s) => s !== '')
63
+ );
64
+ }
65
+ for (const name in partInfo) {
66
+ if (partInfo[name] && !this._staticParts?.has(name)) {
67
+ this._previousParts.add(name);
68
+ }
69
+ }
70
+ return this.render(partInfo);
71
+ }
72
+ let changed = false;
73
+ // Remove old classes that no longer apply
74
+ for (const name of this._previousParts) {
75
+ if (!(name in partInfo)) {
76
+ this._previousParts.delete(name);
77
+ changed = true;
78
+ }
79
+ }
80
+
81
+ // Add or remove classes based on their classMap value
82
+ for (const name in partInfo) {
83
+ // We explicitly want a loose truthy check of `value` because it seems
84
+ // more convenient that '' and 0 are skipped.
85
+ const value = !!partInfo[name];
86
+ if (
87
+ value !== this._previousParts.has(name) &&
88
+ !this._staticParts?.has(name)
89
+ ) {
90
+ changed = true;
91
+ if (value) {
92
+ this._previousParts.add(name);
93
+ } else {
94
+ this._previousParts.delete(name);
95
+ }
96
+ }
97
+ }
98
+ if (changed) return this.render(partInfo);
99
+ return noChange;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * A directive that applies dynamic parts
105
+ *
106
+ * This must be used in the `part` attribute and must be the only part used in
107
+ * the attribute. It takes each property in the `partInfo` argument and adds
108
+ * the property name to the element's `part` if the property value is
109
+ * truthy; if the property value is falsy, the property name is removed from
110
+ * the element's `part`.
111
+ *
112
+ * For example `{foo: bar}` applies the part name `foo` if the value of `bar` is
113
+ * truthy.
114
+ *
115
+ * @param partInfo
116
+ */
117
+ export const partMap = directive(PartMapDirective);
package/route.js CHANGED
@@ -73,7 +73,7 @@ export default class Route {
73
73
  if (j <= 0) {
74
74
  return this._clearOutActive();
75
75
  }
76
- let segment = urlPieces.shift();
76
+ let segment = decodeURIComponent(urlPieces.shift());
77
77
  j--;
78
78
  if (matchedPieces[i].length !== 0) {
79
79
  if (matchedPieces[i].substring(0,1) === ':') {
package/switch-path.js CHANGED
@@ -21,7 +21,7 @@
21
21
  export function generateUri(path, params) {
22
22
  var str = [];
23
23
  if (params) {
24
- for (var param in params) {
24
+ for (const param in params) {
25
25
  //eslint-disable-next-line no-prototype-builtins
26
26
  if (params.hasOwnProperty(param)) {
27
27
  str.push(encodeURIComponent(param) + '=' + encodeURIComponent(params[param]));
@@ -38,5 +38,10 @@ export function switchPath(path, params) {
38
38
  history.pushState({}, null, generateUri(path, params));
39
39
  window.dispatchEvent(new CustomEvent('location-altered', { composed: true, bubbles: true }));
40
40
  }
41
- export default switchPath;
42
41
 
42
+ export function navigate (e) {
43
+ const link = e.currentTarget.getAttribute('path');
44
+ if (typeof link !== 'undefined') {
45
+ switchPath(link);
46
+ }
47
+ }
package/post-api.js DELETED
@@ -1,61 +0,0 @@
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
- export default async function api(url, params, blob, signal) {
23
- const address = '/api/' + url;
24
- const options = {
25
- credentials: 'same-origin',
26
- method: 'post',
27
- headers: new Headers({
28
- 'content-type': 'application/json'
29
- }),
30
- body: JSON.stringify(params ?? {})
31
- };
32
- performance.mark('fetchapi', { detail: `${address} params: ${options.body}` })
33
- if (signal) options.signal = signal;
34
- let text;
35
- try {
36
- const response = await window.fetch(address, options);
37
- if (!response.ok) throw new CustomEvent('api-error', {composed: true, bubbles: true , detail:{status:response.status, url:address}});
38
- performance.mark('fetchdone', {detail: address});
39
- performance.measure('apicalltime',{start: 'fetchapi', end:'fetchdone', detail: address});
40
- if (blob) {
41
- text = '---502---'; //Simulate a 502 (bad gateway) incase there is an error in following.
42
- const b = await response.blob();
43
- window.open(
44
- URL.createObjectURL(b),
45
- '_blank',
46
- 'chrome=yes,centerscreen,resizable,scrollbars,status,height=800,width=800');
47
- return {};
48
- } else {
49
- text = await response.text();
50
- if (text.length > 0) return JSON.parse(text);
51
- return {};
52
- }
53
- } catch (err) {
54
- if (!options.signal || !options.signal.aborted) {
55
- if (err instanceof TypeError) throw new CustomEvent('api-network', { bubbles:true, composed:true, detail: address})
56
- if (err.type === 'api-error') throw err; //just throw whatever error we had
57
- //we failed to parse the json - the actual code should be in the text near the end;
58
- throw new CustomEvent('api-error', { composed: true, bubbles: true, detail: {status:parseInt((text?? '---502---').slice(-6, -3), 10), url:address }});
59
- }
60
- }
61
- }