saasufy-components 1.0.6 → 1.1.1
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/app-router.js +89 -0
- package/collection-adder.js +5 -4
- package/collection-browser.js +25 -19
- package/model-input.js +14 -5
- package/model-text.js +16 -6
- package/package.json +1 -1
- package/socket.js +95 -7
- package/utils.js +23 -0
package/app-router.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getSafeHTML } from './utils.js';
|
|
2
|
+
import { renderTemplate } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export class AppRouter extends HTMLElement {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.attachShadow({ mode: 'open' });
|
|
8
|
+
|
|
9
|
+
this.pages = {};
|
|
10
|
+
this.pageRouteInfos = [];
|
|
11
|
+
|
|
12
|
+
this.hashStartRegex = /^#/;
|
|
13
|
+
|
|
14
|
+
this.handleHashChangeEvent = () => {
|
|
15
|
+
this.renderCurrentPage();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
this.handleSlotChangeEvent = () => {
|
|
19
|
+
let pageTemplates = this.shadowRoot.querySelector('slot[name="page"]').assignedNodes();
|
|
20
|
+
|
|
21
|
+
for (let template of pageTemplates) {
|
|
22
|
+
let routePath = template.getAttribute('route-path');
|
|
23
|
+
this.pages[routePath] = template;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.pageRouteInfos = Object.keys(this.pages).map((route) => {
|
|
27
|
+
return {
|
|
28
|
+
regExp: new RegExp(`^${route.replace(/\/:[^\/]+/g, '/([^/]*)').replace(/\//g, '\\/')}$`, 'g'),
|
|
29
|
+
route,
|
|
30
|
+
params: [ ...route.matchAll(/:[^\/]*/g) ].map(paramMatch => paramMatch[0].replace(':', ''))
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.renderCurrentPage();
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
computeRouteArgs(page, regExp, params) {
|
|
39
|
+
let match = page.matchAll(regExp).next().value || [];
|
|
40
|
+
let argEntries = match
|
|
41
|
+
.slice(1)
|
|
42
|
+
.map((groupMatch, i) => [ params[i], groupMatch ]);
|
|
43
|
+
return Object.fromEntries(argEntries);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
connectedCallback() {
|
|
47
|
+
window.addEventListener('hashchange', this.handleHashChangeEvent);
|
|
48
|
+
this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
|
|
49
|
+
this.render();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
disconnectedCallback() {
|
|
53
|
+
window.removeEventListener('hashchange', this.handleHashChangeEvent);
|
|
54
|
+
this.shadowRoot.removeEventListener('slotchange', this.handleSlotChangeEvent);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
renderCurrentPage() {
|
|
58
|
+
let routerViewport = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
|
|
59
|
+
if (!routerViewport) return;
|
|
60
|
+
|
|
61
|
+
let { hash } = location;
|
|
62
|
+
let pagePath = hash.replace(this.hashStartRegex, '');
|
|
63
|
+
|
|
64
|
+
let { route, regExp, params } = this.pageRouteInfos.find(
|
|
65
|
+
({ regExp }) => pagePath.match(regExp)
|
|
66
|
+
) || {};
|
|
67
|
+
|
|
68
|
+
let pageTemplate = this.pages[route];
|
|
69
|
+
if (!pageTemplate) return;
|
|
70
|
+
|
|
71
|
+
let routeArgs;
|
|
72
|
+
if (route) {
|
|
73
|
+
routeArgs = this.computeRouteArgs(pagePath, regExp, params);
|
|
74
|
+
} else {
|
|
75
|
+
routeArgs = {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
routerViewport.innerHTML = renderTemplate(pageTemplate.innerHTML, routeArgs);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render() {
|
|
82
|
+
this.shadowRoot.innerHTML = `
|
|
83
|
+
<slot name="page"></slot>
|
|
84
|
+
<slot name="viewport"></slot>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
window.customElements.define('app-router', AppRouter);
|
package/collection-adder.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SocketConsumer } from './socket.js';
|
|
2
2
|
import AGCollection from '/node_modules/ag-collection/ag-collection.js';
|
|
3
3
|
|
|
4
|
-
class CollectionAdder extends
|
|
4
|
+
class CollectionAdder extends SocketConsumer {
|
|
5
5
|
constructor(options) {
|
|
6
6
|
super();
|
|
7
|
-
options = options || {};
|
|
8
|
-
|
|
7
|
+
this.options = options || {};
|
|
8
|
+
|
|
9
9
|
this.typeCastFunctions = {
|
|
10
10
|
text: String,
|
|
11
11
|
checkbox: Boolean,
|
|
@@ -16,6 +16,7 @@ class CollectionAdder extends HTMLElement {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
connectedCallback() {
|
|
19
|
+
this.socket = this.getSocket();
|
|
19
20
|
this.isReady = true;
|
|
20
21
|
this.render();
|
|
21
22
|
}
|
package/collection-browser.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SocketConsumer } from './socket.js';
|
|
2
2
|
import AGCollection from '/node_modules/ag-collection/ag-collection.js';
|
|
3
|
-
import {
|
|
3
|
+
import { renderTemplate } from './utils.js';
|
|
4
4
|
|
|
5
5
|
const DEFAULT_RELOAD_DELAY = 0;
|
|
6
6
|
|
|
7
|
-
class CollectionBrowser extends
|
|
7
|
+
class CollectionBrowser extends SocketConsumer {
|
|
8
8
|
constructor() {
|
|
9
9
|
super();
|
|
10
10
|
this.attachShadow({ mode: 'open' });
|
|
11
|
-
this.socket = getSocket();
|
|
12
11
|
|
|
13
12
|
this.handleShowModalEvent = (event) => {
|
|
14
13
|
let modal = this.shadowRoot.querySelector('slot[name="modal"]').assignedNodes()[0];
|
|
@@ -40,12 +39,13 @@ class CollectionBrowser extends HTMLElement {
|
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
this.handleSlotChangeEvent = () => {
|
|
43
|
-
this.
|
|
42
|
+
this.renderList();
|
|
44
43
|
};
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
connectedCallback() {
|
|
48
47
|
this.isReady = true;
|
|
48
|
+
this.socket = this.getSocket();
|
|
49
49
|
this.shadowRoot.addEventListener('slotchange', this.handleSlotChangeEvent);
|
|
50
50
|
this.addEventListener('showModal', this.handleShowModalEvent);
|
|
51
51
|
this.addEventListener('crudCreate', this.handleCRUDCreateEvent);
|
|
@@ -146,28 +146,23 @@ class CollectionBrowser extends HTMLElement {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
renderList() {
|
|
150
150
|
if (!this.collection || !this.collection.isLoaded) return;
|
|
151
151
|
let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
|
|
152
152
|
if (!itemTemplate) return;
|
|
153
|
-
let
|
|
154
|
-
if (!
|
|
153
|
+
let viewportNode = this.shadowRoot.querySelector('slot[name="viewport"]').assignedNodes()[0];
|
|
154
|
+
if (!viewportNode) return;
|
|
155
155
|
let items = [];
|
|
156
156
|
|
|
157
157
|
for (let modelItem of this.collection.value) {
|
|
158
|
-
let itemString = itemTemplate.innerHTML;
|
|
159
|
-
|
|
160
|
-
for (let [ field, value ] of Object.entries(modelItem)) {
|
|
161
|
-
let regExp = new RegExp(`{{${field}}}`, 'g');
|
|
162
|
-
itemString = itemString.replace(regExp, getSafeHTML(value));
|
|
163
|
-
}
|
|
158
|
+
let itemString = renderTemplate(itemTemplate.innerHTML, modelItem, this.collection.type);
|
|
164
159
|
items.push(itemString);
|
|
165
160
|
}
|
|
166
161
|
let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
|
|
167
162
|
if (noItemTemplate && !items.length) {
|
|
168
|
-
|
|
163
|
+
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
169
164
|
} else {
|
|
170
|
-
|
|
165
|
+
viewportNode.innerHTML = items.join('');
|
|
171
166
|
}
|
|
172
167
|
}
|
|
173
168
|
|
|
@@ -178,6 +173,7 @@ class CollectionBrowser extends HTMLElement {
|
|
|
178
173
|
let collectionPageSize = this.getAttribute('collection-page-size');
|
|
179
174
|
let collectionViewParams = this.getAttribute('collection-view-params');
|
|
180
175
|
let collectionPageOffset = this.getAttribute('collection-page-offset');
|
|
176
|
+
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
181
177
|
let collectionReloadDelay = Number(
|
|
182
178
|
this.getAttribute('collection-reload-delay') || DEFAULT_RELOAD_DELAY
|
|
183
179
|
);
|
|
@@ -198,7 +194,7 @@ class CollectionBrowser extends HTMLElement {
|
|
|
198
194
|
this.shadowRoot.innerHTML = `
|
|
199
195
|
<slot name="item"></slot>
|
|
200
196
|
<slot name="no-item"></slot>
|
|
201
|
-
<slot name="
|
|
197
|
+
<slot name="viewport"></slot>
|
|
202
198
|
<slot name="previous-page"></slot>
|
|
203
199
|
<slot name="page-number"></slot>
|
|
204
200
|
<slot name="next-page"></slot>
|
|
@@ -221,7 +217,7 @@ class CollectionBrowser extends HTMLElement {
|
|
|
221
217
|
(async () => {
|
|
222
218
|
await this.collection.listener('load').once();
|
|
223
219
|
if (!this.collection.value.length) {
|
|
224
|
-
this.
|
|
220
|
+
this.renderList();
|
|
225
221
|
}
|
|
226
222
|
this.setAttribute('is-loaded', '');
|
|
227
223
|
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
@@ -229,10 +225,20 @@ class CollectionBrowser extends HTMLElement {
|
|
|
229
225
|
|
|
230
226
|
(async () => {
|
|
231
227
|
for await (let event of this.collection.listener('change')) {
|
|
232
|
-
this.
|
|
228
|
+
this.renderList();
|
|
233
229
|
this.updatePageButtons();
|
|
234
230
|
}
|
|
235
231
|
})();
|
|
232
|
+
|
|
233
|
+
if (!hideErrorLogs) {
|
|
234
|
+
(async () => {
|
|
235
|
+
for await (let { error } of this.collection.listener('error')) {
|
|
236
|
+
console.error(
|
|
237
|
+
`Collection browser encountered an error: ${error.message}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
}
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
|
package/model-input.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SocketConsumer } from './socket.js';
|
|
2
2
|
import { getSafeHTML, debouncer } from './utils.js';
|
|
3
3
|
import AGModel from '/node_modules/ag-model/ag-model.js';
|
|
4
4
|
|
|
5
|
-
class ModelInput extends
|
|
5
|
+
class ModelInput extends SocketConsumer {
|
|
6
6
|
constructor() {
|
|
7
7
|
super();
|
|
8
8
|
this.isReady = false;
|
|
9
9
|
this.inputElement = null;
|
|
10
|
-
this.socket = null;
|
|
11
10
|
}
|
|
12
11
|
connectedCallback() {
|
|
13
12
|
this.isReady = true;
|
|
@@ -73,6 +72,7 @@ class ModelInput extends HTMLElement {
|
|
|
73
72
|
let modelField = this.getAttribute('model-field');
|
|
74
73
|
let options = this.getAttribute('options');
|
|
75
74
|
let height = this.getAttribute('height');
|
|
75
|
+
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
76
76
|
let currentNode = this.parentNode;
|
|
77
77
|
let model;
|
|
78
78
|
let isModelLocal = false;
|
|
@@ -80,17 +80,26 @@ class ModelInput extends HTMLElement {
|
|
|
80
80
|
while (currentNode) {
|
|
81
81
|
model = currentNode[modelInstanceProperty];
|
|
82
82
|
if (model) break;
|
|
83
|
-
currentNode = currentNode.parentNode;
|
|
83
|
+
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
84
84
|
}
|
|
85
85
|
if (!model) return;
|
|
86
86
|
} else {
|
|
87
|
-
this.socket = getSocket();
|
|
87
|
+
this.socket = this.getSocket();
|
|
88
88
|
model = new AGModel({
|
|
89
89
|
socket: this.socket,
|
|
90
90
|
type: modelType,
|
|
91
91
|
id: modelId,
|
|
92
92
|
fields: [ modelField ]
|
|
93
93
|
});
|
|
94
|
+
if (!hideErrorLogs) {
|
|
95
|
+
(async () => {
|
|
96
|
+
for await (let { error } of model.listener('error')) {
|
|
97
|
+
console.error(
|
|
98
|
+
`Model input encountered an error: ${error.message}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
}
|
|
94
103
|
isModelLocal = true;
|
|
95
104
|
}
|
|
96
105
|
if (model.isLoaded) {
|
package/model-text.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SocketConsumer } from './socket.js';
|
|
2
2
|
import { getSafeHTML } from './utils.js';
|
|
3
|
+
import AGModel from '/node_modules/ag-model/ag-model.js';
|
|
3
4
|
|
|
4
|
-
class ModelText extends
|
|
5
|
+
class ModelText extends SocketConsumer {
|
|
5
6
|
connectedCallback() {
|
|
6
7
|
this.isReady = true;
|
|
7
8
|
this.destroy = () => {};
|
|
8
|
-
this.socket = null;
|
|
9
9
|
this.render();
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -34,6 +34,7 @@ class ModelText extends HTMLElement {
|
|
|
34
34
|
let modelType = this.getAttribute('model-type');
|
|
35
35
|
let modelId = this.getAttribute('model-id');
|
|
36
36
|
let modelField = this.getAttribute('model-field');
|
|
37
|
+
let hideErrorLogs = this.hasAttribute('hide-error-logs');
|
|
37
38
|
let currentNode = this.parentNode;
|
|
38
39
|
let model;
|
|
39
40
|
let isModelLocal = false;
|
|
@@ -41,17 +42,26 @@ class ModelText extends HTMLElement {
|
|
|
41
42
|
while (currentNode) {
|
|
42
43
|
model = currentNode[modelInstanceProperty];
|
|
43
44
|
if (model) break;
|
|
44
|
-
currentNode = currentNode.parentNode;
|
|
45
|
+
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
45
46
|
}
|
|
46
47
|
if (!model) return;
|
|
47
48
|
} else {
|
|
48
|
-
this.socket = getSocket();
|
|
49
|
+
this.socket = this.getSocket();
|
|
49
50
|
model = new AGModel({
|
|
50
51
|
socket: this.socket,
|
|
51
52
|
type: modelType,
|
|
52
53
|
id: modelId,
|
|
53
54
|
fields: [ modelField ]
|
|
54
55
|
});
|
|
56
|
+
if (!hideErrorLogs) {
|
|
57
|
+
(async () => {
|
|
58
|
+
for await (let { error } of model.listener('error')) {
|
|
59
|
+
console.error(
|
|
60
|
+
`Model input encountered an error: ${error.message}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
}
|
|
55
65
|
isModelLocal = true;
|
|
56
66
|
}
|
|
57
67
|
if (model.isLoaded) {
|
|
@@ -69,7 +79,7 @@ class ModelText extends HTMLElement {
|
|
|
69
79
|
(async () => {
|
|
70
80
|
for await (let event of changeConsumer) {
|
|
71
81
|
if (event.resourceField !== modelField || !event.isRemote) continue;
|
|
72
|
-
fieldValue = getSafeHTML(model.value[modelField]);
|
|
82
|
+
let fieldValue = getSafeHTML(model.value[modelField]);
|
|
73
83
|
this.innerHTML = fieldValue;
|
|
74
84
|
}
|
|
75
85
|
})();
|
package/package.json
CHANGED
package/socket.js
CHANGED
|
@@ -2,19 +2,107 @@ import { create } from '/node_modules/socketcluster-client/socketcluster-client.
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_AUTH_TOKEN_NAME = 'saasufy.authToken';
|
|
4
4
|
|
|
5
|
-
let
|
|
5
|
+
let globalSocket;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
let urlPartsRegExp = /(^[^:]+):\/\/([^:\/]*)(:[0-9]*)?(\/.*)/;
|
|
8
|
+
|
|
9
|
+
export function createGlobalSocket(options) {
|
|
10
|
+
globalSocket = create({
|
|
9
11
|
authTokenName: DEFAULT_AUTH_TOKEN_NAME,
|
|
10
12
|
...options
|
|
11
13
|
});
|
|
12
|
-
return
|
|
14
|
+
return globalSocket;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getGlobalSocket(options) {
|
|
18
|
+
if (globalSocket) {
|
|
19
|
+
return globalSocket;
|
|
20
|
+
}
|
|
21
|
+
return createGlobalSocket(options);
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
export
|
|
16
|
-
|
|
24
|
+
export class SocketProvider extends HTMLElement {
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
this.saasufySocket = null;
|
|
28
|
+
this.isReady = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
connectedCallback() {
|
|
32
|
+
this.isReady = true;
|
|
33
|
+
this.getSocket();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
disconnectedCallback() {
|
|
37
|
+
this.destroySocket();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static get observedAttributes() {
|
|
41
|
+
return [
|
|
42
|
+
'url',
|
|
43
|
+
'auth-token-name'
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
48
|
+
if (!this.isReady) return;
|
|
49
|
+
this.destroySocket();
|
|
50
|
+
this.getSocket();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
destroySocket() {
|
|
54
|
+
if (this.saasufySocket) {
|
|
55
|
+
this.saasufySocket.disconnect();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getSocket() {
|
|
60
|
+
if (this.saasufySocket) {
|
|
61
|
+
return this.saasufySocket;
|
|
62
|
+
}
|
|
63
|
+
return this.createSocket();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
createSocket() {
|
|
67
|
+
let authTokenName = this.getAttribute('auth-token-name') || DEFAULT_AUTH_TOKEN_NAME;
|
|
68
|
+
let url = this.getAttribute('url') || '';
|
|
69
|
+
let urlOptions;
|
|
70
|
+
let matchedList = url.match(urlPartsRegExp);
|
|
71
|
+
if (matchedList) {
|
|
72
|
+
let [ fullMatch, protocolScheme, hostname, port, path ] = matchedList;
|
|
73
|
+
if (port) {
|
|
74
|
+
port = port.replace(/^:/, '');
|
|
75
|
+
}
|
|
76
|
+
urlOptions = {
|
|
77
|
+
protocolScheme,
|
|
78
|
+
hostname,
|
|
79
|
+
port,
|
|
80
|
+
path
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
urlOptions = {};
|
|
84
|
+
}
|
|
85
|
+
this.saasufySocket = create({
|
|
86
|
+
authTokenName,
|
|
87
|
+
...urlOptions
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return this.saasufySocket;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class SocketConsumer extends HTMLElement {
|
|
95
|
+
getSocket() {
|
|
96
|
+
let socket = null;
|
|
97
|
+
let currentNode = this.parentNode;
|
|
98
|
+
while (currentNode) {
|
|
99
|
+
socket = currentNode.saasufySocket;
|
|
100
|
+
if (socket) break;
|
|
101
|
+
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
102
|
+
}
|
|
17
103
|
return socket;
|
|
18
104
|
}
|
|
19
|
-
return createSocket(options);
|
|
20
105
|
}
|
|
106
|
+
|
|
107
|
+
window.customElements.define('socket-provider', SocketProvider);
|
|
108
|
+
window.customElements.define('socket-consumer', SocketConsumer);
|
package/utils.js
CHANGED
|
@@ -155,6 +155,29 @@ export function createModel(modelOptions) {
|
|
|
155
155
|
return model;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
let templateFormatters = {
|
|
159
|
+
url: (value) => String(value).toLowerCase().replace(/ /g, '-'),
|
|
160
|
+
lowerCase: (value) => String(value).toLowerCase(),
|
|
161
|
+
upperCase: (value) => String(value).toUpperCase(),
|
|
162
|
+
capitalize: (value) => {
|
|
163
|
+
let valueString = String(value);
|
|
164
|
+
return `${valueString.slice(0, 1).toUpperCase()}${valueString.slice(1)}`;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export function renderTemplate(templateString, data, dataType) {
|
|
169
|
+
for (let [ field, value ] of Object.entries(data)) {
|
|
170
|
+
let safeValue = getSafeHTML(value);
|
|
171
|
+
for (let [ formatName, formatFunction ] of Object.entries(templateFormatters)) {
|
|
172
|
+
let formatRegExp = new RegExp(`{{${formatName}:${dataType ? dataType + '.' : ''}${field}}}`, 'g');
|
|
173
|
+
templateString = templateString.replace(formatRegExp, formatFunction(safeValue));
|
|
174
|
+
}
|
|
175
|
+
let regExp = new RegExp(`{{${dataType ? dataType + '.' : ''}${field}}}`, 'g');
|
|
176
|
+
templateString = templateString.replace(regExp, safeValue);
|
|
177
|
+
}
|
|
178
|
+
return templateString;
|
|
179
|
+
}
|
|
180
|
+
|
|
158
181
|
export function wait(duration) {
|
|
159
182
|
return new Promise((resolve) => setTimeout(resolve, duration));
|
|
160
183
|
}
|