saasufy-components 1.0.0 → 1.0.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/collection-adder.js +221 -0
- package/collection-browser.js +125 -0
- package/confirm-modal.js +69 -0
- package/model-input.js +259 -0
- package/model-text.js +89 -0
- package/overlay-modal.js +88 -0
- package/package.json +6 -1
- package/render-group.js +58 -0
- package/socket.js +37 -0
- package/utils.js +160 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { getSocket } from './socket.js';
|
|
2
|
+
import AGCollection from '/node_modules/ag-collection/ag-collection.js';
|
|
3
|
+
|
|
4
|
+
class CollectionAdder extends HTMLElement {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
super();
|
|
7
|
+
options = options || {};
|
|
8
|
+
this.socket = getSocket(options.socketOptions);
|
|
9
|
+
this.typeCastFunctions = {
|
|
10
|
+
text: String,
|
|
11
|
+
checkbox: Boolean,
|
|
12
|
+
number: Number
|
|
13
|
+
};
|
|
14
|
+
this.fieldPartsRegExp = /([^,()]+(\([^)]*\))?(=[^,]+)?)/g;
|
|
15
|
+
this.inputTypeWithParamsRegExp = /^([^()]*)(\(([^)]*)\))?/;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
connectedCallback() {
|
|
19
|
+
this.isReady = true;
|
|
20
|
+
this.render();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
if (this.collection) this.collection.destroy();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static get observedAttributes() {
|
|
28
|
+
return [
|
|
29
|
+
'collection-type',
|
|
30
|
+
'collection-fields',
|
|
31
|
+
'model-values',
|
|
32
|
+
'submit-button-label'
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
37
|
+
if (!this.isReady) return;
|
|
38
|
+
this.render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getTypeCastFunction(type) {
|
|
42
|
+
return this.typeCastFunctions[type] || ((value) => value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getFieldDetails(string) {
|
|
46
|
+
let parts = ((string || '').match(this.fieldPartsRegExp) || []).map(field => field.trim());
|
|
47
|
+
let fieldTypeValues = parts.map((part) => {
|
|
48
|
+
let subParts = part.split('=');
|
|
49
|
+
let nameType = (subParts[0] || '').split(':');
|
|
50
|
+
let name = nameType[0];
|
|
51
|
+
let type = nameType[1] || 'text';
|
|
52
|
+
let value = subParts[1];
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
type,
|
|
56
|
+
value
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
let fieldNames = fieldTypeValues.map(item => item.name);
|
|
60
|
+
let fieldTypes = Object.fromEntries(
|
|
61
|
+
fieldTypeValues.map(item => [ item.name, item.type ])
|
|
62
|
+
);
|
|
63
|
+
let fieldValues = Object.fromEntries(
|
|
64
|
+
fieldTypeValues.map(
|
|
65
|
+
(item) => {
|
|
66
|
+
let type = fieldTypes[item.name];
|
|
67
|
+
let Type = this.getTypeCastFunction(type);
|
|
68
|
+
return [ item.name, Type(item.value) ];
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
return {
|
|
73
|
+
fieldNames,
|
|
74
|
+
fieldTypes,
|
|
75
|
+
fieldValues
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render() {
|
|
80
|
+
let submitButtonLabel = this.getAttribute('submit-button-label');
|
|
81
|
+
if (submitButtonLabel == null) {
|
|
82
|
+
submitButtonLabel = 'Submit';
|
|
83
|
+
}
|
|
84
|
+
let collectionType = this.getAttribute('collection-type');
|
|
85
|
+
let {
|
|
86
|
+
fieldNames,
|
|
87
|
+
fieldTypes
|
|
88
|
+
} = this.getFieldDetails(this.getAttribute('collection-fields'));
|
|
89
|
+
|
|
90
|
+
let {
|
|
91
|
+
fieldNames: modelFieldNames,
|
|
92
|
+
fieldValues: modelFieldValues
|
|
93
|
+
} = this.getFieldDetails(this.getAttribute('model-values'));
|
|
94
|
+
|
|
95
|
+
if (this.collection) this.collection.destroy();
|
|
96
|
+
|
|
97
|
+
this.collection = new AGCollection({
|
|
98
|
+
socket: this.socket,
|
|
99
|
+
type: collectionType,
|
|
100
|
+
fields: [...new Set([ ...fieldNames, ...modelFieldNames ])],
|
|
101
|
+
writeOnly: true
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let items = [];
|
|
105
|
+
for (let field of fieldNames) {
|
|
106
|
+
let inputType = fieldTypes[field];
|
|
107
|
+
let inputTypeParts = inputType.match(this.inputTypeWithParamsRegExp);
|
|
108
|
+
let inputTypeParams;
|
|
109
|
+
if (inputTypeParts) {
|
|
110
|
+
inputType = inputTypeParts[1];
|
|
111
|
+
inputTypeParams = inputTypeParts[3] ? inputTypeParts[3].split(',') : [];
|
|
112
|
+
}
|
|
113
|
+
if (inputType === 'checkbox') {
|
|
114
|
+
items.push(`
|
|
115
|
+
<label>
|
|
116
|
+
${
|
|
117
|
+
field
|
|
118
|
+
}
|
|
119
|
+
<input class="collection-adder-input" type="${
|
|
120
|
+
inputType
|
|
121
|
+
}" name="${
|
|
122
|
+
field
|
|
123
|
+
}" />
|
|
124
|
+
</label>
|
|
125
|
+
`);
|
|
126
|
+
} else if (inputType === 'text-select') {
|
|
127
|
+
items.push(
|
|
128
|
+
`
|
|
129
|
+
<div class="collection-adder-select-text-row">
|
|
130
|
+
<input class="collection-adder-input" type="text" name="${
|
|
131
|
+
field
|
|
132
|
+
}" placeholder="${
|
|
133
|
+
field
|
|
134
|
+
}" />
|
|
135
|
+
<select class="collection-adder-select">
|
|
136
|
+
<option value="" selected disabled hidden>Options</option>
|
|
137
|
+
${inputTypeParams.map(param => `<option value="${param}">${param}</option>`)}
|
|
138
|
+
</select>
|
|
139
|
+
</div>
|
|
140
|
+
`
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
items.push(
|
|
144
|
+
`<input class="collection-adder-input" type="${
|
|
145
|
+
inputType
|
|
146
|
+
}" name="${
|
|
147
|
+
field
|
|
148
|
+
}" placeholder="${
|
|
149
|
+
field
|
|
150
|
+
}" />`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.innerHTML = `
|
|
156
|
+
<form class="collection-adder-form form-container">
|
|
157
|
+
<div class="collection-adder-message-container"></div>
|
|
158
|
+
<div class="collection-adder-form-content">
|
|
159
|
+
${items.join('')}
|
|
160
|
+
<input type="submit" value="${submitButtonLabel}" />
|
|
161
|
+
</div>
|
|
162
|
+
</form>
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
let textSelectRows = this.querySelectorAll('.collection-adder-select-text-row');
|
|
166
|
+
for (let groupElement of textSelectRows) {
|
|
167
|
+
let selectElement = groupElement.querySelector('select');
|
|
168
|
+
selectElement.addEventListener('change', (event) => {
|
|
169
|
+
let inputElement = groupElement.querySelector('input[type="text"]');
|
|
170
|
+
inputElement.value = inputElement.value ? `${inputElement.value}, ${event.target.value}` : event.target.value;
|
|
171
|
+
event.target.value = '';
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let messageContainer = this.querySelector('.collection-adder-message-container');
|
|
176
|
+
|
|
177
|
+
let form = this.querySelector('.collection-adder-form');
|
|
178
|
+
form.addEventListener('submit', async (event) => {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
messageContainer.classList.remove('success');
|
|
181
|
+
messageContainer.classList.remove('error');
|
|
182
|
+
messageContainer.textContent = '';
|
|
183
|
+
|
|
184
|
+
let newModelData = {
|
|
185
|
+
...modelFieldValues,
|
|
186
|
+
...Object.fromEntries(
|
|
187
|
+
[...event.target.querySelectorAll('.collection-adder-input')]
|
|
188
|
+
.map((input) => {
|
|
189
|
+
let Type = this.getTypeCastFunction(
|
|
190
|
+
fieldTypes[input.name]
|
|
191
|
+
);
|
|
192
|
+
return [ input.name, Type(input.value) ];
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await this.collection.create(newModelData);
|
|
199
|
+
messageContainer.classList.add('success');
|
|
200
|
+
messageContainer.classList.remove('error');
|
|
201
|
+
this.dispatchEvent(
|
|
202
|
+
new CustomEvent('success', {
|
|
203
|
+
detail: newModelData
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
form.reset();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
messageContainer.classList.add('error');
|
|
209
|
+
messageContainer.classList.remove('success');
|
|
210
|
+
messageContainer.textContent = error.message;
|
|
211
|
+
this.dispatchEvent(
|
|
212
|
+
new CustomEvent('error', {
|
|
213
|
+
detail: error
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
window.customElements.define('collection-adder', CollectionAdder);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getSocket } from './socket.js';
|
|
2
|
+
import AGCollection from '/node_modules/ag-collection/ag-collection.js';
|
|
3
|
+
import { getSafeHTML } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_RELOAD_DELAY = 0;
|
|
6
|
+
|
|
7
|
+
class CollectionBrowser extends HTMLElement {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.attachShadow({ mode: 'open' });
|
|
11
|
+
this.socket = getSocket();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
this.isReady = true;
|
|
16
|
+
this.shadowRoot.addEventListener('slotchange', () => this.renderTable());
|
|
17
|
+
this.addEventListener('showModal', (event) => this.handleShowModalEvent(event));
|
|
18
|
+
this.addEventListener('crudCreate', (event) => this.handleCRUDCreateEvent(event));
|
|
19
|
+
this.addEventListener('crudDelete', (event) => this.handleCRUDDeleteEvent(event));
|
|
20
|
+
this.render();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
if (this.collection) this.collection.destroy();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static get observedAttributes() {
|
|
28
|
+
return [
|
|
29
|
+
'collection-type',
|
|
30
|
+
'collection-fields',
|
|
31
|
+
'collection-view',
|
|
32
|
+
'collection-view-params',
|
|
33
|
+
'collection-page-size'
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
38
|
+
if (!this.isReady) return;
|
|
39
|
+
this.render();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
renderTable() {
|
|
43
|
+
if (!this.collection || !this.collection.isLoaded) return;
|
|
44
|
+
let itemTemplate = this.shadowRoot.querySelector('slot[name="item"]').assignedNodes()[0];
|
|
45
|
+
if (!itemTemplate) return;
|
|
46
|
+
let listNode = this.shadowRoot.querySelector('slot[name="list"]').assignedNodes()[0];
|
|
47
|
+
if (!listNode) return;
|
|
48
|
+
let items = [];
|
|
49
|
+
|
|
50
|
+
for (let modelItem of this.collection.value) {
|
|
51
|
+
let itemString = itemTemplate.innerHTML;
|
|
52
|
+
|
|
53
|
+
for (let [ field, value ] of Object.entries(modelItem)) {
|
|
54
|
+
let regExp = new RegExp(`{{${field}}}`, 'g');
|
|
55
|
+
itemString = itemString.replace(regExp, getSafeHTML(value));
|
|
56
|
+
}
|
|
57
|
+
items.push(itemString);
|
|
58
|
+
}
|
|
59
|
+
let noItemTemplate = this.shadowRoot.querySelector('slot[name="no-item"]').assignedNodes()[0];
|
|
60
|
+
if (noItemTemplate && !items.length) {
|
|
61
|
+
listNode.innerHTML = noItemTemplate.innerHTML;
|
|
62
|
+
} else {
|
|
63
|
+
listNode.innerHTML = items.join('');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
handleShowModalEvent(event) {
|
|
68
|
+
let modal = this.shadowRoot.querySelector('slot[name="modal"]').assignedNodes()[0];
|
|
69
|
+
modal.show(event.detail.message, event.detail.callback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleCRUDCreateEvent(event) {
|
|
73
|
+
if (this.collection) {
|
|
74
|
+
this.collection.create(event.detail);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleCRUDDeleteEvent(event) {
|
|
79
|
+
if (this.collection) {
|
|
80
|
+
this.collection.delete(event.detail);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render() {
|
|
85
|
+
let collectionType = this.getAttribute('collection-type');
|
|
86
|
+
let collectionFields = this.getAttribute('collection-fields');
|
|
87
|
+
let collectionView = this.getAttribute('collection-view');
|
|
88
|
+
let collectionPageSize = this.getAttribute('collection-page-size');
|
|
89
|
+
let collectionViewParams = this.getAttribute('collection-view-params');
|
|
90
|
+
let collectionReloadDelay = Number(
|
|
91
|
+
this.getAttribute('collection-reload-delay') || DEFAULT_RELOAD_DELAY
|
|
92
|
+
);
|
|
93
|
+
let viewParams = Object.fromEntries(collectionViewParams.split(',').map((pair) => pair.split('=')));
|
|
94
|
+
if (this.collection) this.collection.destroy();
|
|
95
|
+
|
|
96
|
+
this.collection = new AGCollection({
|
|
97
|
+
socket: this.socket,
|
|
98
|
+
type: collectionType,
|
|
99
|
+
fields: collectionFields.split(',').map(field => field.trim()),
|
|
100
|
+
view: collectionView,
|
|
101
|
+
viewParams,
|
|
102
|
+
pageSize: Number(collectionPageSize || 10),
|
|
103
|
+
changeReloadDelay: collectionReloadDelay
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.shadowRoot.innerHTML = '<slot name="item"></slot><slot name="no-item"></slot><slot name="list"></slot><slot name="modal"></slot>';
|
|
107
|
+
|
|
108
|
+
(async () => {
|
|
109
|
+
await this.collection.listener('load').once();
|
|
110
|
+
if (!this.collection.value.length) {
|
|
111
|
+
this.renderTable();
|
|
112
|
+
}
|
|
113
|
+
this.setAttribute('is-loaded', '');
|
|
114
|
+
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
115
|
+
})();
|
|
116
|
+
|
|
117
|
+
(async () => {
|
|
118
|
+
for await (let event of this.collection.listener('change')) {
|
|
119
|
+
this.renderTable();
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
window.customElements.define('collection-browser', CollectionBrowser);
|
package/confirm-modal.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import './overlay-modal.js';
|
|
2
|
+
|
|
3
|
+
class ConfirmModal extends HTMLElement {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
this.hidden = true;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connectedCallback() {
|
|
10
|
+
this.isReady = true;
|
|
11
|
+
this.render();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static get observedAttributes() {
|
|
15
|
+
return [ 'message', 'title', 'confirm-button-label' ];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
19
|
+
if (!this.isReady) return;
|
|
20
|
+
this.render();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
show(message, confirmCallback) {
|
|
24
|
+
let overlayModal = this.querySelector('overlay-modal');
|
|
25
|
+
if (overlayModal) {
|
|
26
|
+
overlayModal.classList.remove('hidden');
|
|
27
|
+
}
|
|
28
|
+
this.confirmCallback = confirmCallback;
|
|
29
|
+
this.hidden = false;
|
|
30
|
+
this.setAttribute('message', message);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
let message = this.getAttribute('message');
|
|
35
|
+
let title = this.getAttribute('title') || '';
|
|
36
|
+
let confirmButtonLabel = this.getAttribute('confirm-button-label') || 'Confirm';
|
|
37
|
+
|
|
38
|
+
this.innerHTML = `
|
|
39
|
+
<overlay-modal class="${this.hidden ? 'hidden' : 'visible'}">
|
|
40
|
+
<div slot="title">${title}</div>
|
|
41
|
+
<div class="confirm-modal-content" slot="content">
|
|
42
|
+
<div>${message}</div>
|
|
43
|
+
<div class="confirm-modal-buttons-container">
|
|
44
|
+
<input class="modal-confirm-button" type="button" value="${confirmButtonLabel}" />
|
|
45
|
+
<input class="modal-cancel-button" type="button" value="Cancel" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</overlay-modal>
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
let overlayModal = this.querySelector('overlay-modal');
|
|
52
|
+
overlayModal.addEventListener('close', () => {
|
|
53
|
+
overlayModal.classList.add('hidden');
|
|
54
|
+
});
|
|
55
|
+
let cancelButton = this.querySelector('.modal-cancel-button');
|
|
56
|
+
cancelButton.addEventListener('click', () => {
|
|
57
|
+
overlayModal.classList.add('hidden');
|
|
58
|
+
this.dispatchEvent(new CustomEvent('cancel'));
|
|
59
|
+
});
|
|
60
|
+
let confirmButton = this.querySelector('.modal-confirm-button');
|
|
61
|
+
confirmButton.addEventListener('click', () => {
|
|
62
|
+
overlayModal.classList.add('hidden');
|
|
63
|
+
this.dispatchEvent(new CustomEvent('confirm'));
|
|
64
|
+
this.confirmCallback && this.confirmCallback();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
window.customElements.define('confirm-modal', ConfirmModal);
|
package/model-input.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { getSocket } from './socket.js';
|
|
2
|
+
import { getSafeHTML } from './utils.js';
|
|
3
|
+
import AGModel from '/node_modules/ag-model/ag-model.js';
|
|
4
|
+
|
|
5
|
+
class ModelInput extends HTMLElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.isReady = false;
|
|
9
|
+
this.inputElement = null;
|
|
10
|
+
this.socket = null;
|
|
11
|
+
}
|
|
12
|
+
connectedCallback() {
|
|
13
|
+
this.isReady = true;
|
|
14
|
+
this.destroy = () => {};
|
|
15
|
+
this.render();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnectedCallback() {
|
|
19
|
+
this.destroy();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static get observedAttributes() {
|
|
23
|
+
return [
|
|
24
|
+
'input-id',
|
|
25
|
+
'input-list',
|
|
26
|
+
'model-instance-property',
|
|
27
|
+
'type',
|
|
28
|
+
'placeholder',
|
|
29
|
+
'show-error-message',
|
|
30
|
+
'model-type',
|
|
31
|
+
'model-id',
|
|
32
|
+
'model-field',
|
|
33
|
+
'options',
|
|
34
|
+
'height'
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
39
|
+
if (!this.isReady) return;
|
|
40
|
+
this.destroy();
|
|
41
|
+
this.render();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
triggerChange() {
|
|
45
|
+
this.inputElement && this.inputElement.dispatchEvent(new Event('change'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set value(newValue) {
|
|
49
|
+
if (this.inputElement) {
|
|
50
|
+
let oldValue = this.inputElement.value;
|
|
51
|
+
this.inputElement.value = newValue;
|
|
52
|
+
if (oldValue !== newValue) {
|
|
53
|
+
this.triggerChange();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get value() {
|
|
59
|
+
return (this.inputElement && this.inputElement.value) || '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render() {
|
|
63
|
+
this.innerHTML = '';
|
|
64
|
+
let modelInstanceProperty = this.getAttribute('model-instance-property');
|
|
65
|
+
let showErrorMessage = this.hasAttribute('show-error-message');
|
|
66
|
+
let inputId = this.getAttribute('input-id');
|
|
67
|
+
let inputList = this.getAttribute('input-list');
|
|
68
|
+
let type = this.getAttribute('type');
|
|
69
|
+
let placeholder = this.getAttribute('placeholder');
|
|
70
|
+
let modelType = this.getAttribute('model-type');
|
|
71
|
+
let modelId = this.getAttribute('model-id');
|
|
72
|
+
let modelField = this.getAttribute('model-field');
|
|
73
|
+
let options = this.getAttribute('options');
|
|
74
|
+
let height = this.getAttribute('height');
|
|
75
|
+
let currentNode = this.parentNode;
|
|
76
|
+
let model;
|
|
77
|
+
let isModelLocal = false;
|
|
78
|
+
if (modelInstanceProperty) {
|
|
79
|
+
while (currentNode) {
|
|
80
|
+
model = currentNode[modelInstanceProperty];
|
|
81
|
+
if (model) break;
|
|
82
|
+
currentNode = currentNode.parentNode;
|
|
83
|
+
}
|
|
84
|
+
if (!model) return;
|
|
85
|
+
} else {
|
|
86
|
+
this.socket = getSocket();
|
|
87
|
+
model = new AGModel({
|
|
88
|
+
socket: this.socket,
|
|
89
|
+
type: modelType,
|
|
90
|
+
id: modelId,
|
|
91
|
+
fields: [ modelField ]
|
|
92
|
+
});
|
|
93
|
+
isModelLocal = true;
|
|
94
|
+
}
|
|
95
|
+
if (model.isLoaded) {
|
|
96
|
+
this.setAttribute('is-loaded', '');
|
|
97
|
+
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
98
|
+
} else {
|
|
99
|
+
this.isLoadedConsumer = model.listener('load').createConsumer();
|
|
100
|
+
(async () => {
|
|
101
|
+
await this.isLoadedConsumer.next();
|
|
102
|
+
this.setAttribute('is-loaded', '');
|
|
103
|
+
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
104
|
+
})();
|
|
105
|
+
}
|
|
106
|
+
let elementType;
|
|
107
|
+
if (type === 'select') {
|
|
108
|
+
elementType = 'select';
|
|
109
|
+
} else if (type === 'textarea') {
|
|
110
|
+
elementType = 'textarea';
|
|
111
|
+
} else {
|
|
112
|
+
elementType = 'input';
|
|
113
|
+
}
|
|
114
|
+
this.inputElement = document.createElement(elementType);
|
|
115
|
+
if (type === 'select') {
|
|
116
|
+
if (options) {
|
|
117
|
+
let optionElements = options
|
|
118
|
+
.split(',')
|
|
119
|
+
.map((option) => {
|
|
120
|
+
return `<option value="${option}">${option}</option>`;
|
|
121
|
+
})
|
|
122
|
+
.join('');
|
|
123
|
+
this.inputElement.innerHTML = optionElements;
|
|
124
|
+
}
|
|
125
|
+
} else if (elementType === 'input') {
|
|
126
|
+
if (inputList) {
|
|
127
|
+
this.inputElement.setAttribute('list', inputList)
|
|
128
|
+
}
|
|
129
|
+
this.inputElement.type = type;
|
|
130
|
+
}
|
|
131
|
+
if (height) {
|
|
132
|
+
this.inputElement.style.height = height;
|
|
133
|
+
}
|
|
134
|
+
if (inputId) {
|
|
135
|
+
this.inputElement.setAttribute('id', inputId);
|
|
136
|
+
}
|
|
137
|
+
if (placeholder) {
|
|
138
|
+
this.inputElement.setAttribute('placeholder', placeholder);
|
|
139
|
+
}
|
|
140
|
+
let errorMessageContainer = null;
|
|
141
|
+
if (showErrorMessage) {
|
|
142
|
+
errorMessageContainer = document.createElement('div');
|
|
143
|
+
errorMessageContainer.classList.add('error-message-container');
|
|
144
|
+
this.appendChild(errorMessageContainer);
|
|
145
|
+
}
|
|
146
|
+
this.appendChild(this.inputElement);
|
|
147
|
+
let destroyInputSync = this.syncInputElementWithModel(this.inputElement, model, modelField, errorMessageContainer);
|
|
148
|
+
this.destroy = () => {
|
|
149
|
+
destroyInputSync();
|
|
150
|
+
this.isLoadedConsumer && this.isLoadedConsumer.kill();
|
|
151
|
+
if (isModelLocal) {
|
|
152
|
+
model.destroy();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
syncInputElementWithModel(inputElement, model, fieldName, messageContainerElement) {
|
|
158
|
+
if (model.isLoaded) {
|
|
159
|
+
let fieldValue = getSafeHTML(model.value[fieldName]);
|
|
160
|
+
if (inputElement.type === 'checkbox') {
|
|
161
|
+
inputElement.checked = !!fieldValue;
|
|
162
|
+
} else {
|
|
163
|
+
inputElement.value = fieldValue == null ? '' : fieldValue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let stopSaving = this.saveInputElementOnEdit(inputElement, model, fieldName, messageContainerElement);
|
|
167
|
+
let changeConsumer = model.listener('change').createConsumer();
|
|
168
|
+
(async () => {
|
|
169
|
+
for await (let event of changeConsumer) {
|
|
170
|
+
if (event.resourceField !== fieldName || !event.isRemote) continue;
|
|
171
|
+
let fieldValue = getSafeHTML(model.value[fieldName]);
|
|
172
|
+
if (inputElement.type === 'checkbox') {
|
|
173
|
+
inputElement.checked = fieldValue;
|
|
174
|
+
} else {
|
|
175
|
+
inputElement.value = fieldValue == null ? '' : fieldValue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
return () => {
|
|
180
|
+
stopSaving();
|
|
181
|
+
changeConsumer.kill();
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
saveInputElementOnEdit(inputElement, model, fieldName, messageContainerElement) {
|
|
186
|
+
let errorStyleClass = 'error';
|
|
187
|
+
let successStyleClass = 'success';
|
|
188
|
+
let showErrorMessage = (fieldName, errorMessage) => {
|
|
189
|
+
if (!messageContainerElement) return;
|
|
190
|
+
messageContainerElement.textContent = errorMessage;
|
|
191
|
+
messageContainerElement.classList.add(errorStyleClass);
|
|
192
|
+
messageContainerElement.classList.remove('hidden');
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
let hideErrorMessage = (fieldName) => {
|
|
196
|
+
if (!messageContainerElement) return;
|
|
197
|
+
messageContainerElement.textContent = '';
|
|
198
|
+
messageContainerElement.classList.remove(errorStyleClass);
|
|
199
|
+
messageContainerElement.classList.add('hidden');
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let onInputChange = async (event) => {
|
|
203
|
+
if (event.target.value === String(model.value[fieldName])) return;
|
|
204
|
+
try {
|
|
205
|
+
if (inputElement.type === 'checkbox') {
|
|
206
|
+
await model.update(fieldName, !!inputElement.checked);
|
|
207
|
+
inputElement.classList.remove(errorStyleClass);
|
|
208
|
+
inputElement.classList.add(successStyleClass);
|
|
209
|
+
hideErrorMessage(fieldName);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (event.target.value === '') {
|
|
213
|
+
await model.delete(fieldName);
|
|
214
|
+
} else {
|
|
215
|
+
let targetValue = inputElement.type === 'number' ?
|
|
216
|
+
Number(event.target.value) : event.target.value;
|
|
217
|
+
await model.update(fieldName, targetValue);
|
|
218
|
+
}
|
|
219
|
+
inputElement.classList.remove(errorStyleClass);
|
|
220
|
+
inputElement.classList.add(successStyleClass);
|
|
221
|
+
hideErrorMessage(fieldName);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
inputElement.classList.add(errorStyleClass);
|
|
224
|
+
inputElement.classList.remove(successStyleClass);
|
|
225
|
+
showErrorMessage(fieldName, error.message);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
inputElement.addEventListener('change', onInputChange);
|
|
229
|
+
|
|
230
|
+
let onInputKeyUp;
|
|
231
|
+
if (inputElement.type !== 'checkbox') {
|
|
232
|
+
onInputKeyUp = async (event) => {
|
|
233
|
+
try {
|
|
234
|
+
if (event.target.value === '') {
|
|
235
|
+
await model.delete(fieldName);
|
|
236
|
+
} else {
|
|
237
|
+
let targetValue = inputElement.type === 'number' ?
|
|
238
|
+
Number(event.target.value) : event.target.value;
|
|
239
|
+
await model.update(fieldName, targetValue);
|
|
240
|
+
}
|
|
241
|
+
inputElement.classList.remove(errorStyleClass);
|
|
242
|
+
inputElement.classList.add(successStyleClass);
|
|
243
|
+
hideErrorMessage(fieldName);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
inputElement.classList.add(errorStyleClass);
|
|
246
|
+
inputElement.classList.remove(successStyleClass);
|
|
247
|
+
showErrorMessage(fieldName, error.message);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
inputElement.addEventListener('keyup', onInputKeyUp);
|
|
251
|
+
}
|
|
252
|
+
return () => {
|
|
253
|
+
inputElement.removeEventListener('change', onInputChange);
|
|
254
|
+
onInputKeyUp && inputElement.removeEventListener('keyup', onInputKeyUp);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
window.customElements.define('model-input', ModelInput);
|
package/model-text.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getSocket } from './socket.js';
|
|
2
|
+
import { getSafeHTML } from './utils.js';
|
|
3
|
+
|
|
4
|
+
class ModelText extends HTMLElement {
|
|
5
|
+
connectedCallback() {
|
|
6
|
+
this.isReady = true;
|
|
7
|
+
this.destroy = () => {};
|
|
8
|
+
this.socket = null;
|
|
9
|
+
this.render();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
disconnectedCallback() {
|
|
13
|
+
this.destroy();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static get observedAttributes() {
|
|
17
|
+
return [
|
|
18
|
+
'model-instance-property',
|
|
19
|
+
'model-type',
|
|
20
|
+
'model-id',
|
|
21
|
+
'model-field'
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
26
|
+
if (!this.isReady) return;
|
|
27
|
+
this.destroy();
|
|
28
|
+
this.render();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render() {
|
|
32
|
+
this.innerHTML = '';
|
|
33
|
+
let modelInstanceProperty = this.getAttribute('model-instance-property');
|
|
34
|
+
let modelType = this.getAttribute('model-type');
|
|
35
|
+
let modelId = this.getAttribute('model-id');
|
|
36
|
+
let modelField = this.getAttribute('model-field');
|
|
37
|
+
let currentNode = this.parentNode;
|
|
38
|
+
let model;
|
|
39
|
+
let isModelLocal = false;
|
|
40
|
+
if (modelInstanceProperty) {
|
|
41
|
+
while (currentNode) {
|
|
42
|
+
model = currentNode[modelInstanceProperty];
|
|
43
|
+
if (model) break;
|
|
44
|
+
currentNode = currentNode.parentNode;
|
|
45
|
+
}
|
|
46
|
+
if (!model) return;
|
|
47
|
+
} else {
|
|
48
|
+
this.socket = getSocket();
|
|
49
|
+
model = new AGModel({
|
|
50
|
+
socket: this.socket,
|
|
51
|
+
type: modelType,
|
|
52
|
+
id: modelId,
|
|
53
|
+
fields: [ modelField ]
|
|
54
|
+
});
|
|
55
|
+
isModelLocal = true;
|
|
56
|
+
}
|
|
57
|
+
if (model.isLoaded) {
|
|
58
|
+
this.setAttribute('is-loaded', '');
|
|
59
|
+
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
60
|
+
} else {
|
|
61
|
+
this.isLoadedConsumer = model.listener('load').createConsumer();
|
|
62
|
+
(async () => {
|
|
63
|
+
await this.isLoadedConsumer.next();
|
|
64
|
+
this.setAttribute('is-loaded', '');
|
|
65
|
+
this.dispatchEvent(new CustomEvent('load', { bubbles: true }));
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
let changeConsumer = model.listener('change').createConsumer();
|
|
69
|
+
(async () => {
|
|
70
|
+
for await (let event of changeConsumer) {
|
|
71
|
+
if (event.resourceField !== modelField || !event.isRemote) continue;
|
|
72
|
+
fieldValue = getSafeHTML(model.value[modelField]);
|
|
73
|
+
this.innerHTML = fieldValue;
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
if (model.isLoaded) {
|
|
77
|
+
this.innerHTML = getSafeHTML(model.value[modelField]);
|
|
78
|
+
}
|
|
79
|
+
this.destroy = () => {
|
|
80
|
+
changeConsumer.kill();
|
|
81
|
+
this.isLoadedConsumer && this.isLoadedConsumer.kill();
|
|
82
|
+
if (isModelLocal) {
|
|
83
|
+
model.destroy();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
window.customElements.define('model-text', ModelText);
|
package/overlay-modal.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
class OverlayModal extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.attachShadow({ mode: 'open' });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
connectedCallback() {
|
|
8
|
+
this.render();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
render() {
|
|
12
|
+
this.shadowRoot.innerHTML = `
|
|
13
|
+
<style>
|
|
14
|
+
.overlay-background {
|
|
15
|
+
display: flex;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
align-items: center;
|
|
18
|
+
width: 100vw;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
position: fixed;
|
|
21
|
+
top: 0;
|
|
22
|
+
right: 0;
|
|
23
|
+
bottom: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
z-index: 10000;
|
|
26
|
+
background-color: rgba(0, 0, 0, .3);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.modal-dialog {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
width: var(--modal-width, 800px);
|
|
34
|
+
height: var(--modal-height, auto);
|
|
35
|
+
min-height: var(--modal-min-height, auto);
|
|
36
|
+
background-color: #ffffff;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.modal-title-bar {
|
|
40
|
+
display: var(--title-bar-display, flex);
|
|
41
|
+
justify-content: space-between;
|
|
42
|
+
width: 100%;
|
|
43
|
+
padding: 10px;
|
|
44
|
+
color: #ffffff;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
background: #e37042;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.modal-content {
|
|
50
|
+
flex-grow: 1;
|
|
51
|
+
padding: 10px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.modal-footer {
|
|
55
|
+
display: var(--footer-display, flex)
|
|
56
|
+
padding: 10px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.close-button {
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
63
|
+
<div class="overlay-background">
|
|
64
|
+
<div class="modal-dialog">
|
|
65
|
+
<div class="modal-title-bar">
|
|
66
|
+
<div>
|
|
67
|
+
<slot name="title"></slot>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="close-button">✕</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="modal-content">
|
|
72
|
+
<slot name="content"></slot>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="modal-footer">
|
|
75
|
+
<slot name="footer"></slot>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
this.shadowRoot.querySelector('.close-button').addEventListener('click', () => {
|
|
81
|
+
this.dispatchEvent(
|
|
82
|
+
new CustomEvent('close', { bubbles: true })
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.customElements.define('overlay-modal', OverlayModal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saasufy-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Components for Saasufy.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
],
|
|
17
17
|
"author": "Jonathan Gros-Dubois",
|
|
18
18
|
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ag-collection": "^3.1.0",
|
|
21
|
+
"ag-model": "^1.3.5",
|
|
22
|
+
"socketcluster-client": "^18.3.0"
|
|
23
|
+
},
|
|
19
24
|
"bugs": {
|
|
20
25
|
"url": "https://github.com/Saasufy/saasufy-components/issues"
|
|
21
26
|
},
|
package/render-group.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
class RenderGroup extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.attachShadow({ mode: 'open' });
|
|
5
|
+
|
|
6
|
+
this.loadedElements = {};
|
|
7
|
+
|
|
8
|
+
this.loadHandler = (event) => {
|
|
9
|
+
let waitFor = this.getWaitFor();
|
|
10
|
+
let renderLoadId = event.target.getAttribute('load-id');
|
|
11
|
+
this.loadedElements[renderLoadId] = true;
|
|
12
|
+
let areAllLoaded = waitFor.every(waitForId => this.loadedElements[waitForId]);
|
|
13
|
+
if (areAllLoaded) {
|
|
14
|
+
this.showSlottedElements();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnectedCallback() {
|
|
20
|
+
this.removeEventListener('load', this.loadHandler);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
connectedCallback() {
|
|
24
|
+
this.isReady = true;
|
|
25
|
+
this.loadedElements = {};
|
|
26
|
+
let hasPending = !!this.getWaitFor().length;
|
|
27
|
+
this.shadowRoot.innerHTML = `
|
|
28
|
+
<style>
|
|
29
|
+
.hidden {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
33
|
+
<slot${hasPending ? ' class="hidden"': ''}></slot>
|
|
34
|
+
`;
|
|
35
|
+
if (hasPending) {
|
|
36
|
+
this.addEventListener('load', this.loadHandler);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
showSlottedElements() {
|
|
41
|
+
let slot = this.shadowRoot.querySelector('slot');
|
|
42
|
+
slot.classList.remove('hidden');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getWaitFor() {
|
|
46
|
+
let waitForString = this.getAttribute('wait-for');
|
|
47
|
+
if (!waitForString) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
return waitForString.split(',').map(id => id.trim());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
54
|
+
if (!this.isReady) return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.customElements.define('render-group', RenderGroup);
|
package/socket.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { create } from '/node_modules/socketcluster-client/socketcluster-client.min.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_AUTH_TOKEN_NAME = 'saasufy.authToken';
|
|
4
|
+
|
|
5
|
+
let socket;
|
|
6
|
+
|
|
7
|
+
export function createSocket(options) {
|
|
8
|
+
socket = create({
|
|
9
|
+
authTokenName: DEFAULT_AUTH_TOKEN_NAME,
|
|
10
|
+
...options
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
(async () => {
|
|
14
|
+
for await (let { error } of socket.listener('error')) {
|
|
15
|
+
console.error(error);
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
|
|
19
|
+
(async () => {
|
|
20
|
+
for await (let event of socket.listener('connect')) {
|
|
21
|
+
console.log('Socket is connected');
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
return socket;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
window.createSocket = createSocket;
|
|
29
|
+
|
|
30
|
+
export function getSocket(options) {
|
|
31
|
+
if (socket) {
|
|
32
|
+
return socket;
|
|
33
|
+
}
|
|
34
|
+
return createSocket(options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
window.getSocket = getSocket;
|
package/utils.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import AGCollection from '/node_modules/ag-collection/ag-collection.js';
|
|
2
|
+
import AGModel from '/node_modules/ag-model/ag-model.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DEBOUNCE_DELAY = 300;
|
|
5
|
+
const DEFAULT_RELOAD_DELAY = 0;
|
|
6
|
+
|
|
7
|
+
export function getSafeHTML(text) {
|
|
8
|
+
if (typeof text === 'string') {
|
|
9
|
+
return text.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/'/g, ''');
|
|
14
|
+
} else if (text != null && typeof text.toString !== 'function') {
|
|
15
|
+
return '[invalid]';
|
|
16
|
+
}
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function debouncer() {
|
|
21
|
+
let debounceTimeout = null;
|
|
22
|
+
return function (callback, duration) {
|
|
23
|
+
if (duration == null) duration = DEFAULT_DEBOUNCE_DELAY;
|
|
24
|
+
debounceTimeout && clearTimeout(debounceTimeout);
|
|
25
|
+
debounceTimeout = setTimeout(() => {
|
|
26
|
+
debounceTimeout = null;
|
|
27
|
+
callback.call(this);
|
|
28
|
+
}, duration);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSafeModelValue(value) {
|
|
33
|
+
return Object.fromEntries(
|
|
34
|
+
Object.entries(value || {}).map(
|
|
35
|
+
([key, value]) => [ key, getSafeHTML(value) ]
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createReactiveCollection(collectionOptions, callback) {
|
|
41
|
+
let collection = new AGCollection({
|
|
42
|
+
changeReloadDelay: DEFAULT_RELOAD_DELAY,
|
|
43
|
+
...collectionOptions
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
(async () => {
|
|
47
|
+
for await (let { error } of collection.listener('error')) {
|
|
48
|
+
console.error(error);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
collection.safeValue = [];
|
|
53
|
+
|
|
54
|
+
(async () => {
|
|
55
|
+
let changes = {};
|
|
56
|
+
for await (let event of collection.listener('change')) {
|
|
57
|
+
if (event.resourceId != null && changes[event.resourceId] !== false) {
|
|
58
|
+
changes[event.resourceId] = event.isRemote;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!collection.isLoaded) continue;
|
|
62
|
+
|
|
63
|
+
collection.safeValue = collection.value.map(getSafeModelValue);
|
|
64
|
+
|
|
65
|
+
callback({
|
|
66
|
+
changes
|
|
67
|
+
});
|
|
68
|
+
changes = {};
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
(async () => {
|
|
73
|
+
for await (let event of collection.listener('load')) {
|
|
74
|
+
if (collection.isLoaded && !collection.value.length) {
|
|
75
|
+
callback({
|
|
76
|
+
changes: {}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
return collection;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createReactiveModel(modelOptions, callback) {
|
|
86
|
+
let model = new AGModel({
|
|
87
|
+
...modelOptions
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
model.safeValue = {};
|
|
91
|
+
|
|
92
|
+
(async () => {
|
|
93
|
+
for await (let { error } of model.listener('error')) {
|
|
94
|
+
console.error(error);
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
(async () => {
|
|
99
|
+
let changes = {};
|
|
100
|
+
for await (let event of model.listener('change')) {
|
|
101
|
+
if (changes[event.resourceField] !== false) {
|
|
102
|
+
changes[event.resourceField] = event.isRemote;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
model.safeValue = getSafeModelValue(model.value);
|
|
106
|
+
callback({
|
|
107
|
+
changes
|
|
108
|
+
});
|
|
109
|
+
changes = {};
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
return model;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createCollection(collectionOptions) {
|
|
117
|
+
let collection = new AGCollection({
|
|
118
|
+
changeReloadDelay: DEFAULT_RELOAD_DELAY,
|
|
119
|
+
...collectionOptions
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
(async () => {
|
|
123
|
+
for await (let { error } of collection.listener('error')) {
|
|
124
|
+
console.error(error);
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
|
|
128
|
+
(async () => {
|
|
129
|
+
for await (let event of collection.listener('change')) {
|
|
130
|
+
if (!collection.isLoaded) continue;
|
|
131
|
+
collection.safeValue = collection.value.map(getSafeModelValue);
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
|
|
135
|
+
return collection;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createModel(modelOptions) {
|
|
139
|
+
let model = new AGModel({
|
|
140
|
+
...modelOptions
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
(async () => {
|
|
144
|
+
for await (let { error } of model.listener('error')) {
|
|
145
|
+
console.error(error);
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
|
|
149
|
+
(async () => {
|
|
150
|
+
for await (let event of model.listener('change')) {
|
|
151
|
+
model.safeValue = getSafeModelValue(model.value);
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
154
|
+
|
|
155
|
+
return model;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function wait(duration) {
|
|
159
|
+
return new Promise((resolve) => setTimeout(resolve, duration));
|
|
160
|
+
}
|