saasufy-components 1.4.0 → 1.5.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/README.md +7 -0
- package/collection-browser.js +3 -1
- package/input-provider.js +217 -0
- package/model-input.js +5 -4
- package/model-text.js +2 -1
- package/model-viewer.js +3 -1
- package/package.json +2 -2
- package/socket.js +38 -10
- package/utils.js +1 -0
package/README.md
CHANGED
|
@@ -25,6 +25,13 @@ Used for displaying and editing a field of a model instance in real time.
|
|
|
25
25
|
|
|
26
26
|
Used for rendering models using a template.
|
|
27
27
|
|
|
28
|
+
### input-provider
|
|
29
|
+
|
|
30
|
+
An input component which can pass data to other components (or HTML elements) via custom attributes.
|
|
31
|
+
A common use case for it is to pass user input to `collection-browser` or `model-viewer` to display relevant data loaded from Saasufy (e.g. based on a user query).
|
|
32
|
+
This component can be configured to provide its data to multiple components (consumers) via custom attributes.
|
|
33
|
+
Note that an `input-provider` only targets elements which are downstream from its parent element in the HTML hierarchy.
|
|
34
|
+
|
|
28
35
|
### confirm-modal
|
|
29
36
|
|
|
30
37
|
A modal component to prompt the user for confirmation before performing sensitive operations.
|
package/collection-browser.js
CHANGED
|
@@ -91,6 +91,7 @@ class CollectionBrowser extends SocketConsumer {
|
|
|
91
91
|
'collection-view-params',
|
|
92
92
|
'collection-page-size',
|
|
93
93
|
'collection-page-offset',
|
|
94
|
+
'type-alias',
|
|
94
95
|
'hide-error-logs'
|
|
95
96
|
];
|
|
96
97
|
}
|
|
@@ -158,11 +159,12 @@ class CollectionBrowser extends SocketConsumer {
|
|
|
158
159
|
if (noItemTemplate && !this.collection.value.length) {
|
|
159
160
|
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
160
161
|
} else if (itemTemplate) {
|
|
162
|
+
let type = this.getAttribute('type-alias') || this.collection.type;
|
|
161
163
|
let items = [];
|
|
162
164
|
for (let modelItem of this.collection.value) {
|
|
163
165
|
let itemString = renderTemplate(
|
|
164
166
|
itemTemplate.innerHTML,
|
|
165
|
-
{ [
|
|
167
|
+
{ [type]: modelItem }
|
|
166
168
|
);
|
|
167
169
|
items.push(itemString);
|
|
168
170
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { debouncer } from './utils.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DEBOUNCE_DELAY = 800;
|
|
4
|
+
|
|
5
|
+
class InputProvider extends HTMLElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.isReady = false;
|
|
9
|
+
this.inputElement = null;
|
|
10
|
+
}
|
|
11
|
+
connectedCallback() {
|
|
12
|
+
this.isReady = true;
|
|
13
|
+
this.destroy = () => {};
|
|
14
|
+
this.render();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnectedCallback() {
|
|
18
|
+
this.destroy();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static get observedAttributes() {
|
|
22
|
+
return [
|
|
23
|
+
'input-id',
|
|
24
|
+
'list',
|
|
25
|
+
'type',
|
|
26
|
+
'placeholder',
|
|
27
|
+
'consumers',
|
|
28
|
+
'debounce-delay',
|
|
29
|
+
'options',
|
|
30
|
+
'value',
|
|
31
|
+
'height'
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
36
|
+
if (!this.isReady) return;
|
|
37
|
+
this.destroy();
|
|
38
|
+
this.render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
triggerChange() {
|
|
42
|
+
this.inputElement && this.inputElement.dispatchEvent(new Event('change'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
set value(newValue) {
|
|
46
|
+
if (this.inputElement) {
|
|
47
|
+
let oldValue = this.inputElement.value;
|
|
48
|
+
this.inputElement.value = newValue;
|
|
49
|
+
if (oldValue !== newValue) {
|
|
50
|
+
this.triggerChange();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get value() {
|
|
56
|
+
return (this.inputElement && this.inputElement.value) || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
this.innerHTML = '';
|
|
61
|
+
let inputId = this.getAttribute('input-id');
|
|
62
|
+
let inputList = this.getAttribute('list');
|
|
63
|
+
let type = this.getAttribute('type') || 'text';
|
|
64
|
+
let placeholder = this.getAttribute('placeholder');
|
|
65
|
+
let options = this.getAttribute('options');
|
|
66
|
+
let height = this.getAttribute('height');
|
|
67
|
+
let value = this.getAttribute('value') || '';
|
|
68
|
+
|
|
69
|
+
let elementType;
|
|
70
|
+
if (type === 'select') {
|
|
71
|
+
elementType = 'select';
|
|
72
|
+
} else if (type === 'textarea') {
|
|
73
|
+
elementType = 'textarea';
|
|
74
|
+
} else {
|
|
75
|
+
elementType = 'input';
|
|
76
|
+
}
|
|
77
|
+
this.inputElement = document.createElement(elementType);
|
|
78
|
+
if (type === 'select') {
|
|
79
|
+
if (options) {
|
|
80
|
+
let optionElements = options
|
|
81
|
+
.split(',')
|
|
82
|
+
.map((option) => {
|
|
83
|
+
return `<option value="${option}">${option}</option>`;
|
|
84
|
+
})
|
|
85
|
+
.join('');
|
|
86
|
+
this.inputElement.innerHTML = optionElements;
|
|
87
|
+
}
|
|
88
|
+
} else if (elementType === 'input') {
|
|
89
|
+
if (inputList) {
|
|
90
|
+
this.inputElement.setAttribute('list', inputList)
|
|
91
|
+
}
|
|
92
|
+
this.inputElement.type = type;
|
|
93
|
+
}
|
|
94
|
+
if (height) {
|
|
95
|
+
this.inputElement.style.height = height;
|
|
96
|
+
}
|
|
97
|
+
if (inputId) {
|
|
98
|
+
this.inputElement.setAttribute('id', inputId);
|
|
99
|
+
}
|
|
100
|
+
if (placeholder) {
|
|
101
|
+
this.inputElement.setAttribute('placeholder', placeholder);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.appendChild(this.inputElement);
|
|
105
|
+
|
|
106
|
+
let destroyHandlers = this.updateConsumerElementsOnEdit();
|
|
107
|
+
|
|
108
|
+
if (value) {
|
|
109
|
+
this.value = value;
|
|
110
|
+
}
|
|
111
|
+
this.destroy = () => {
|
|
112
|
+
destroyHandlers();
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
updateConsumerElements(consumers, value) {
|
|
117
|
+
let parentElement = this.parentElement;
|
|
118
|
+
if (parentElement) {
|
|
119
|
+
let consumerParts = consumers.split(',')
|
|
120
|
+
.filter(part => part)
|
|
121
|
+
.map(part => {
|
|
122
|
+
part = part.trim();
|
|
123
|
+
return part.split(':').map(subPart => subPart.trim());
|
|
124
|
+
})
|
|
125
|
+
.filter(([ selector, attributeName ]) => selector);
|
|
126
|
+
|
|
127
|
+
for (let [ selector, attributeName ] of consumerParts) {
|
|
128
|
+
let matchingElements = parentElement.querySelectorAll(selector);
|
|
129
|
+
if (attributeName) {
|
|
130
|
+
for (let element of matchingElements) {
|
|
131
|
+
if (typeof value === 'boolean') {
|
|
132
|
+
if (value) {
|
|
133
|
+
element.setAttribute(attributeName, '');
|
|
134
|
+
} else {
|
|
135
|
+
element.removeAttribute(attributeName);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
element.setAttribute(attributeName, value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
for (let element of matchingElements) {
|
|
143
|
+
if (element.nodeName === 'INPUT') {
|
|
144
|
+
if (element.type === 'checkbox') {
|
|
145
|
+
if (value) {
|
|
146
|
+
element.setAttribute('checked', '');
|
|
147
|
+
} else {
|
|
148
|
+
element.removeAttribute('checked');
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
element.value = value;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
element.innerHTML = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updateConsumerElementsOnEdit() {
|
|
163
|
+
let consumers = this.getAttribute('consumers');
|
|
164
|
+
if (!consumers) return;
|
|
165
|
+
|
|
166
|
+
let inputElement = this.inputElement;
|
|
167
|
+
|
|
168
|
+
let debounceDelay = this.getAttribute('debounce-delay');
|
|
169
|
+
debounceDelay = debounceDelay ? Number(debounceDelay) : DEFAULT_DEBOUNCE_DELAY;
|
|
170
|
+
let debounce = debouncer();
|
|
171
|
+
|
|
172
|
+
let onInputChange = (event) => {
|
|
173
|
+
debounce(async () => {
|
|
174
|
+
if (event.target.value === this.lastValue) return;
|
|
175
|
+
if (inputElement.type === 'checkbox') {
|
|
176
|
+
let checked = !!inputElement.checked;
|
|
177
|
+
this.lastValue = checked;
|
|
178
|
+
this.updateConsumerElements(consumers, checked);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.lastValue = event.target.value;
|
|
182
|
+
if (event.target.value === '') {
|
|
183
|
+
this.updateConsumerElements(consumers, '');
|
|
184
|
+
} else {
|
|
185
|
+
let targetValue = inputElement.type === 'number' ?
|
|
186
|
+
Number(event.target.value) : event.target.value;
|
|
187
|
+
this.updateConsumerElements(consumers, targetValue);
|
|
188
|
+
}
|
|
189
|
+
}, debounceDelay);
|
|
190
|
+
};
|
|
191
|
+
inputElement.addEventListener('change', onInputChange);
|
|
192
|
+
|
|
193
|
+
let onInputKeyUp;
|
|
194
|
+
if (inputElement.type !== 'checkbox') {
|
|
195
|
+
onInputKeyUp = async (event) => {
|
|
196
|
+
debounce(async () => {
|
|
197
|
+
if (event.target.value === this.lastValue) return;
|
|
198
|
+
this.lastValue = event.target.value;
|
|
199
|
+
if (event.target.value === '') {
|
|
200
|
+
this.updateConsumerElements(consumers, '');
|
|
201
|
+
} else {
|
|
202
|
+
let targetValue = inputElement.type === 'number' ?
|
|
203
|
+
Number(event.target.value) : event.target.value;
|
|
204
|
+
this.updateConsumerElements(consumers, targetValue);
|
|
205
|
+
}
|
|
206
|
+
}, debounceDelay);
|
|
207
|
+
};
|
|
208
|
+
inputElement.addEventListener('keyup', onInputKeyUp);
|
|
209
|
+
}
|
|
210
|
+
return () => {
|
|
211
|
+
inputElement.removeEventListener('change', onInputChange);
|
|
212
|
+
onInputKeyUp && inputElement.removeEventListener('keyup', onInputKeyUp);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
window.customElements.define('input-provider', InputProvider);
|
package/model-input.js
CHANGED
|
@@ -21,7 +21,7 @@ class ModelInput extends SocketConsumer {
|
|
|
21
21
|
static get observedAttributes() {
|
|
22
22
|
return [
|
|
23
23
|
'input-id',
|
|
24
|
-
'
|
|
24
|
+
'list',
|
|
25
25
|
'model-instance-property',
|
|
26
26
|
'type',
|
|
27
27
|
'placeholder',
|
|
@@ -31,7 +31,8 @@ class ModelInput extends SocketConsumer {
|
|
|
31
31
|
'model-field',
|
|
32
32
|
'debounce-delay',
|
|
33
33
|
'options',
|
|
34
|
-
'height'
|
|
34
|
+
'height',
|
|
35
|
+
'hide-error-logs'
|
|
35
36
|
];
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -64,8 +65,8 @@ class ModelInput extends SocketConsumer {
|
|
|
64
65
|
let modelInstanceProperty = this.getAttribute('model-instance-property');
|
|
65
66
|
let showErrorMessage = this.hasAttribute('show-error-message');
|
|
66
67
|
let inputId = this.getAttribute('input-id');
|
|
67
|
-
let inputList = this.getAttribute('
|
|
68
|
-
let type = this.getAttribute('type');
|
|
68
|
+
let inputList = this.getAttribute('list');
|
|
69
|
+
let type = this.getAttribute('type') || 'text';
|
|
69
70
|
let placeholder = this.getAttribute('placeholder');
|
|
70
71
|
let modelType = this.getAttribute('model-type');
|
|
71
72
|
let modelId = this.getAttribute('model-id');
|
package/model-text.js
CHANGED
package/model-viewer.js
CHANGED
|
@@ -29,6 +29,7 @@ class ModelViewer extends SocketConsumer {
|
|
|
29
29
|
'model-type',
|
|
30
30
|
'model-id',
|
|
31
31
|
'model-fields',
|
|
32
|
+
'type-alias',
|
|
32
33
|
'hide-error-logs'
|
|
33
34
|
];
|
|
34
35
|
}
|
|
@@ -55,9 +56,10 @@ class ModelViewer extends SocketConsumer {
|
|
|
55
56
|
if (noItemTemplate && !this.modelValueExists(modelValue)) {
|
|
56
57
|
viewportNode.innerHTML = noItemTemplate.innerHTML;
|
|
57
58
|
} else if (itemTemplate) {
|
|
59
|
+
let type = this.getAttribute('type-alias') || this.model.type;
|
|
58
60
|
let itemString = renderTemplate(
|
|
59
61
|
itemTemplate.innerHTML,
|
|
60
|
-
{ [
|
|
62
|
+
{ [type]: modelValue }
|
|
61
63
|
);
|
|
62
64
|
viewportNode.innerHTML = itemString;
|
|
63
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saasufy-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Components for Saasufy.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"ag-collection": "^3.3.0",
|
|
21
21
|
"ag-model": "^1.4.1",
|
|
22
22
|
"expr-eval": "^2.0.2",
|
|
23
|
-
"socketcluster-client": "^19.0
|
|
23
|
+
"socketcluster-client": "^19.1.0"
|
|
24
24
|
},
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/Saasufy/saasufy-components/issues"
|
package/socket.js
CHANGED
|
@@ -41,7 +41,6 @@ export class SocketProvider extends HTMLElement {
|
|
|
41
41
|
|
|
42
42
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
43
43
|
if (!this.isReady) return;
|
|
44
|
-
this.destroySocket();
|
|
45
44
|
this.getSocket();
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -51,16 +50,14 @@ export class SocketProvider extends HTMLElement {
|
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
return this.createSocket();
|
|
53
|
+
getSanitizedURL() {
|
|
54
|
+
let url = this.getAttribute('url') || '';
|
|
55
|
+
return url.trim();
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
getSocketOptions() {
|
|
62
59
|
let authTokenName = this.getAttribute('auth-token-name') || null;
|
|
63
|
-
let url = this.
|
|
60
|
+
let url = this.getSanitizedURL();
|
|
64
61
|
let urlOptions;
|
|
65
62
|
let matchedList = url.match(urlPartsRegExp);
|
|
66
63
|
if (matchedList) {
|
|
@@ -75,12 +72,36 @@ export class SocketProvider extends HTMLElement {
|
|
|
75
72
|
path
|
|
76
73
|
};
|
|
77
74
|
} else {
|
|
75
|
+
if (url) {
|
|
76
|
+
throw new Error(`The specified URL ${url} was invalid`);
|
|
77
|
+
}
|
|
78
78
|
urlOptions = {};
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
let socketOptions = {
|
|
81
81
|
authTokenName,
|
|
82
82
|
...urlOptions
|
|
83
|
-
}
|
|
83
|
+
};
|
|
84
|
+
socketOptions.autoConnect = !!url;
|
|
85
|
+
return socketOptions;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getSocket() {
|
|
89
|
+
if (this.saasufySocket) {
|
|
90
|
+
if (this.saasufySocket.uri() !== this.getSanitizedURL()) {
|
|
91
|
+
this.saasufySocket.connect(
|
|
92
|
+
this.getSocketOptions()
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
this.saasufySocket.connect();
|
|
96
|
+
}
|
|
97
|
+
return this.saasufySocket;
|
|
98
|
+
}
|
|
99
|
+
return this.createSocket();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
createSocket() {
|
|
103
|
+
let socketOptions = this.getSocketOptions();
|
|
104
|
+
this.saasufySocket = create(socketOptions);
|
|
84
105
|
|
|
85
106
|
return this.saasufySocket;
|
|
86
107
|
}
|
|
@@ -95,6 +116,13 @@ export class SocketConsumer extends HTMLElement {
|
|
|
95
116
|
if (socket) break;
|
|
96
117
|
currentNode = currentNode.getRootNode().host || currentNode.parentNode;
|
|
97
118
|
}
|
|
119
|
+
if (!socket) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`The ${
|
|
122
|
+
this.nodeName.toLowerCase()
|
|
123
|
+
} element failed to obtain a socket - Make sure it is nested inside a socket-provider element`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
98
126
|
return socket;
|
|
99
127
|
}
|
|
100
128
|
}
|
package/utils.js
CHANGED
|
@@ -164,6 +164,7 @@ let templateFormatters = {
|
|
|
164
164
|
let valueString = String(value);
|
|
165
165
|
return `${valueString.slice(0, 1).toUpperCase()}${valueString.slice(1)}`;
|
|
166
166
|
},
|
|
167
|
+
trim: (value) => String(value).trim(),
|
|
167
168
|
fallback: (...args) => {
|
|
168
169
|
return args.filter(arg => arg)[0];
|
|
169
170
|
}
|