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.
@@ -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);
@@ -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);
@@ -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">&#x2715;</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.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
  },
@@ -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, '&amp;')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#039;');
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
+ }