@salla.sa/twilight-components 2.11.53 → 2.11.55

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.
Files changed (44) hide show
  1. package/dist/cjs/salla-add-product-button.cjs.entry.js +10 -9
  2. package/dist/cjs/salla-button_36.cjs.entry.js +82 -29
  3. package/dist/cjs/salla-product-options.cjs.entry.js +26 -17
  4. package/dist/collection/components/salla-add-product-button/salla-add-product-button.js +14 -9
  5. package/dist/collection/components/salla-color-picker/salla-color-picker.js +22 -0
  6. package/dist/collection/components/salla-datetime-picker/salla-datetime-picker.js +23 -1
  7. package/dist/collection/components/salla-file-upload/salla-file-upload.js +31 -1
  8. package/dist/collection/components/salla-map/salla-map.js +22 -0
  9. package/dist/collection/components/salla-product-availability/salla-product-availability.js +42 -28
  10. package/dist/collection/components/salla-product-options/salla-product-options.js +27 -18
  11. package/dist/components/salla-add-product-button.js +10 -9
  12. package/dist/components/salla-color-picker2.js +8 -0
  13. package/dist/components/salla-datetime-picker2.js +8 -0
  14. package/dist/components/salla-file-upload2.js +16 -1
  15. package/dist/components/salla-map2.js +8 -0
  16. package/dist/components/salla-product-availability2.js +42 -28
  17. package/dist/components/salla-product-options.js +26 -17
  18. package/dist/esm/salla-add-product-button.entry.js +10 -9
  19. package/dist/esm/salla-button_36.entry.js +82 -29
  20. package/dist/esm/salla-product-options.entry.js +26 -17
  21. package/dist/esm-es5/salla-add-product-button.entry.js +2 -2
  22. package/dist/esm-es5/salla-button_36.entry.js +5 -5
  23. package/dist/esm-es5/salla-product-options.entry.js +2 -2
  24. package/dist/twilight/p-368bbdf1.entry.js +4 -0
  25. package/dist/twilight/p-6dc9f167.entry.js +36 -0
  26. package/dist/twilight/p-7075226e.system.entry.js +4 -0
  27. package/dist/twilight/p-84d8b36b.system.js +1 -1
  28. package/dist/twilight/p-9fbf1baf.system.entry.js +53 -0
  29. package/dist/twilight/p-ca92ba53.system.entry.js +4 -0
  30. package/dist/twilight/p-edc8a654.entry.js +4 -0
  31. package/dist/twilight/twilight.esm.js +1 -1
  32. package/dist/types/components/salla-color-picker/salla-color-picker.d.ts +4 -0
  33. package/dist/types/components/salla-datetime-picker/salla-datetime-picker.d.ts +5 -1
  34. package/dist/types/components/salla-file-upload/salla-file-upload.d.ts +5 -0
  35. package/dist/types/components/salla-map/salla-map.d.ts +4 -0
  36. package/dist/types/components/salla-product-options/salla-product-options.d.ts +2 -1
  37. package/dist/types/components.d.ts +18 -2
  38. package/package.json +3 -3
  39. package/dist/twilight/p-126f5fed.system.entry.js +0 -4
  40. package/dist/twilight/p-96e39318.system.entry.js +0 -53
  41. package/dist/twilight/p-ae6ad245.entry.js +0 -36
  42. package/dist/twilight/p-cdb6aa9e.entry.js +0 -4
  43. package/dist/twilight/p-d01e39b0.entry.js +0 -4
  44. package/dist/twilight/p-d9579b04.system.entry.js +0 -4
@@ -215,6 +215,13 @@ export class SallaMap {
215
215
  }
216
216
  });
217
217
  }
218
+ this.mapInput.addEventListener('invalid', e => {
219
+ this.invalidInput.emit(e);
220
+ });
221
+ this.mapInput.addEventListener('input', () => {
222
+ this.mapInput.setCustomValidity('');
223
+ this.mapInput.reportValidity();
224
+ });
218
225
  }
219
226
  /**
220
227
  * Open location component
@@ -524,6 +531,21 @@ export class SallaMap {
524
531
  "resolved": "any",
525
532
  "references": {}
526
533
  }
534
+ }, {
535
+ "method": "invalidInput",
536
+ "name": "invalidInput",
537
+ "bubbles": true,
538
+ "cancelable": true,
539
+ "composed": true,
540
+ "docs": {
541
+ "tags": [],
542
+ "text": "Event emitted when the input is invalid."
543
+ },
544
+ "complexType": {
545
+ "original": "any",
546
+ "resolved": "any",
547
+ "references": {}
548
+ }
527
549
  }]; }
528
550
  static get methods() { return {
529
551
  "open": {
@@ -16,12 +16,34 @@ export class SallaProductAvailability {
16
16
  * is current user already subscribed
17
17
  */
18
18
  this.isSubscribed = false;
19
+ this.handleSubmitOptions = async () => {
20
+ let payload = { id: this.productId };
21
+ if (!this.notifyOptionsAvailability) {
22
+ return payload;
23
+ }
24
+ let optionsElement = document.querySelector(`salla-product-options[product-id="${this.productId}"]`);
25
+ let options = Object.values(await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.getSelectedOptionsData()) || {});
26
+ //if all options not selected, show message && throw exception
27
+ if (options.length && !await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.reportValidity())) {
28
+ let errorMessage = salla.lang.get('common.messages.required_fields');
29
+ salla.error(errorMessage);
30
+ throw errorMessage;
31
+ }
32
+ payload.options = [];
33
+ options.forEach(option => {
34
+ //inject numbers only, without zeros
35
+ if (option && !isNaN(option)) {
36
+ payload.options.push(Number(option));
37
+ }
38
+ });
39
+ return payload;
40
+ };
19
41
  // helpers
20
- this.typing = (e, submitMethod) => {
42
+ this.typing = (e) => {
21
43
  const error = e.target.nextElementSibling;
22
44
  e.target.classList.remove('s-has-error');
23
45
  (error === null || error === void 0 ? void 0 : error.classList.contains('s-product-availability-error-msg')) && (error.innerText = '');
24
- e.key == 'Enter' && submitMethod();
46
+ e.keyCode === 13 && this.submit();
25
47
  };
26
48
  salla.lang.onLoaded(() => {
27
49
  var _a;
@@ -43,28 +65,6 @@ export class SallaProductAvailability {
43
65
  channelsWatcher(newValue) {
44
66
  this.channels_ = !!newValue ? newValue.split(',') : [];
45
67
  }
46
- async handleSubmitOptions() {
47
- let payload = { id: this.productId };
48
- if (!this.notifyOptionsAvailability) {
49
- return payload;
50
- }
51
- let optionsElement = document.querySelector(`salla-product-options[product-id="${this.productId}"]`);
52
- let options = Object.values(await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.getSelectedOptionsData()) || {});
53
- //if all options not selected, show message && throw exception
54
- if (options.length && !await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.reportValidity())) {
55
- let errorMessage = salla.lang.get('common.messages.required_fields');
56
- salla.error(errorMessage);
57
- throw errorMessage;
58
- }
59
- payload.options = [];
60
- options.forEach(option => {
61
- //inject numbers only, without zeros
62
- if (option && !isNaN(option)) {
63
- payload.options.push(Number(option));
64
- }
65
- });
66
- return payload;
67
- }
68
68
  openModel() {
69
69
  this.handleSubmitOptions().then(isSuccess => isSuccess ? this.modal.open() : null);
70
70
  }
@@ -87,9 +87,23 @@ export class SallaProductAvailability {
87
87
  .then(() => this.btn.disable())
88
88
  .then(() => salla.api.product.availabilitySubscribe(payload))
89
89
  .then(() => {
90
+ if (!this.notifyOptionsAvailability) {
91
+ salla.storage.set(`product-${this.productId}-subscribed`, true);
92
+ return;
93
+ }
94
+ if (payload.options.length) {
95
+ let options = salla.storage.get(`product-${this.productId}-subscribed-options`) || [];
96
+ let selectedOptionsString = payload.options.join(',');
97
+ if (!options.includes(selectedOptionsString)) {
98
+ options.push(selectedOptionsString);
99
+ salla.storage.set(`product-${this.productId}-subscribed-options`, options);
100
+ }
101
+ else {
102
+ salla.log('already subscribed to this options');
103
+ }
104
+ }
90
105
  this.isSubscribed = true;
91
- salla.storage.set(`product-${this.productId}-subscribed`, true);
92
- }) //no need to wait until finishing alert animation
106
+ })
93
107
  .then(() => this.btn.stop())
94
108
  .then(() => this.modal.close())
95
109
  .catch(() => this.btn.stop() && this.btn.enable());
@@ -132,12 +146,12 @@ export class SallaProductAvailability {
132
146
  h("div", { class: "s-product-availability-body" },
133
147
  this.channels_.includes('email') ? [
134
148
  h("label", { class: "s-product-availability-label" }, salla.lang.get('common.elements.email')),
135
- h("input", { class: "s-product-availability-input", onKeyDown: e => this.typing(e, this.submit), placeholder: salla.lang.get('common.elements.email_placeholder') || 'your@email.com', ref: el => this.email = el, type: "email" }),
149
+ h("input", { class: "s-product-availability-input", onKeyDown: e => this.typing(e), placeholder: salla.lang.get('common.elements.email_placeholder') || 'your@email.com', ref: el => this.email = el, type: "email" }),
136
150
  h("span", { class: "s-product-availability-error-msg" })
137
151
  ] : '',
138
152
  this.channels_.includes('sms') ? [
139
153
  h("label", { class: "s-product-availability-label" }, salla.lang.get('common.elements.mobile')),
140
- h("salla-tel-input", { ref: el => this.mobileInput = el, onKeyDown: e => this.typing(e, this.submit) })
154
+ h("salla-tel-input", { ref: el => this.mobileInput = el, onKeyDown: e => this.typing(e) })
141
155
  ] : ''),
142
156
  h("div", { slot: "footer", class: "s-product-availability-footer" },
143
157
  h("salla-button", { class: "modal-cancel-btn", width: "wide", color: "light", fill: "outline", onClick: () => this.modal.close() }, salla.lang.get('common.elements.cancel')),
@@ -68,7 +68,7 @@ export class SallaProductOptions {
68
68
  return selectedOptions;
69
69
  }
70
70
  /**
71
- * Get the id's of the selected options.
71
+ * Report options form validity.
72
72
  * */
73
73
  async reportValidity() {
74
74
  let requiredElements = this.host.querySelectorAll('[required]');
@@ -99,6 +99,12 @@ export class SallaProductOptions {
99
99
  async getOption(option_id) {
100
100
  return this.optionsData.find(option => option.id === option_id);
101
101
  }
102
+ // @ts-ignore
103
+ invalidHandler(event, option) {
104
+ const closestProductOption = event.target.closest('.s-product-options-option');
105
+ closestProductOption.scrollIntoView({ behavior: 'smooth', block: 'center' });
106
+ closestProductOption.classList.add('s-product-options-option-error');
107
+ }
102
108
  changedHandler(event, option) {
103
109
  let data = { event: event, option: option, detail: null };
104
110
  if (option.details) {
@@ -107,6 +113,10 @@ export class SallaProductOptions {
107
113
  });
108
114
  data.detail = detail;
109
115
  }
116
+ let optionElement = event.target.closest('.s-product-options-option');
117
+ if (event.target.value || event.type == 'added') {
118
+ optionElement.classList.remove('s-product-options-option-error');
119
+ }
110
120
  const index = this.selectedOptions.findIndex(option => option.option_id === data.option.id);
111
121
  index > -1 ? this.selectedOptions[index] = Object.assign(Object.assign({}, data.detail), { option_id: data.option.id }) : this.selectedOptions.push(Object.assign(Object.assign({}, data.detail), { option_id: data.option.id }));
112
122
  this.setSelectedSkus();
@@ -168,7 +178,7 @@ export class SallaProductOptions {
168
178
  return (h(Host, { class: "s-product-options-wrapper" },
169
179
  h("salla-conditional-fields", null, this.optionsData.map((option) => h("div", Object.assign({ class: `s-product-options-option-container${option.visibility_condition ? ' hidden' : ''}`, "data-option-id": option.id }, this.getOptionShownWhen(option)), option.name == 'splitter' ?
170
180
  this.splitterOption()
171
- : h("div", { class: "s-product-options-option" },
181
+ : h("div", { class: "s-product-options-option", "data-option-type": option.type, "data-option-required": `${option.required}` },
172
182
  h("label", { htmlFor: 'options[' + option.id + ']', class: "s-product-options-option-label" },
173
183
  h("b", null,
174
184
  option.name,
@@ -185,11 +195,11 @@ export class SallaProductOptions {
185
195
  h("salla-progress-bar", { donation: option.donation }))
186
196
  : '',
187
197
  h("div", { class: "s-product-options-donation-input-group" },
188
- h("input", { type: "text", id: "donating-amount", name: "donating_amount", class: "s-form-control", value: option.value, required: true, placeholder: option.placeholder, onInput: e => salla.helpers.inputDigitsOnly(e.target), onBlur: e => this.changedHandler(e, option) }),
198
+ h("input", { type: "text", id: "donating-amount", name: "donating_amount", class: "s-form-control", value: option.value, required: true, placeholder: option.placeholder, onInput: e => salla.helpers.inputDigitsOnly(e.target), onBlur: e => this.changedHandler(e, option), onInvalid: (e) => this.invalidHandler(e, option) }),
189
199
  h("span", { class: "s-product-options-donation-amount-currency" }, salla.config.currency(salla.config.get('user.currency_code')).symbol)));
190
200
  }
191
201
  fileUploader(option, additions = null) {
192
- return h("salla-file-upload", Object.assign({}, (additions || {}), { "payload-name": "file", value: option.value, "instant-upload": true, name: `options[${option.id}]`, required: option.required, height: "120px", url: salla.cart.api.getUploadImageEndpoint(), "form-data": { cart_item_id: this.productId, product_id: this.productId }, class: { "s-product-options-image-input": true, required: option.required } }),
202
+ return h("salla-file-upload", Object.assign({}, (additions || {}), { "payload-name": "file", value: option.value, "instant-upload": true, name: `options[${option.id}]`, required: option.required, height: "120px", onAdded: (e) => this.changedHandler(e, option), url: salla.cart.api.getUploadImageEndpoint(), "form-data": { cart_item_id: this.productId, product_id: this.productId }, onInvalidInput: (e) => this.invalidHandler(e, option), class: { "s-product-options-image-input": true, required: option.required } }),
193
203
  h("div", { class: "s-product-options-filepond-placeholder" },
194
204
  h("span", { class: "s-product-options-filepond-placeholder-icon", innerHTML: additions.accept && additions.accept.split(',').every(type => type.includes('image'))
195
205
  ? CameraIcon
@@ -210,7 +220,7 @@ export class SallaProductOptions {
210
220
  }
211
221
  //@ts-ignore
212
222
  numberOption(option) {
213
- return h("input", { type: "text", value: option.value, class: "s-form-control", required: option.required, name: `options[${option.id}]`, placeholder: option.placeholder, onBlur: e => this.changedHandler(e, option), onInput: e => salla.helpers.inputDigitsOnly(e.target) });
223
+ return h("input", { type: "text", value: option.value, class: "s-form-control", required: option.required, name: `options[${option.id}]`, placeholder: option.placeholder, onBlur: e => this.changedHandler(e, option), onInvalid: (e) => this.invalidHandler(e, option), onInput: e => salla.helpers.inputDigitsOnly(e.target) });
214
224
  }
215
225
  //@ts-ignore
216
226
  splitterOption() {
@@ -219,45 +229,44 @@ export class SallaProductOptions {
219
229
  //@ts-ignore
220
230
  textOption(option) {
221
231
  return h("div", { class: "s-product-options-text" },
222
- h("input", { type: "text", value: option.value, class: 's-form-control', required: option.required, name: `options[${option.id}]`, placeholder: option.placeholder, onInput: e => this.changedHandler(e, option) }));
232
+ h("input", { type: "text", value: option.value, class: 's-form-control', required: option.required, name: `options[${option.id}]`, placeholder: option.placeholder, onInvalid: (e) => this.invalidHandler(e, option), onInput: e => this.changedHandler(e, option) }));
223
233
  }
224
234
  //@ts-ignore
225
235
  textareaOption(option) {
226
236
  //todo::remove mt-1 class, and if it's okay to remove the tag itself will be great
227
237
  return h("div", { class: "s-product-options-textarea" },
228
238
  h("div", { class: "mt-1" },
229
- h("textarea", { rows: 4, value: option.value, class: "s-form-control", required: option.required, id: `options[${option.id}]`, name: `options[${option.id}]`, placeholder: option.placeholder, onInput: (e) => this.changedHandler(e, option) })));
239
+ h("textarea", { rows: 4, value: option.value, class: "s-form-control", required: option.required, id: `options[${option.id}]`, name: `options[${option.id}]`, placeholder: option.placeholder, onInvalid: (e) => this.invalidHandler(e, option), onInput: (e) => this.changedHandler(e, option) })));
230
240
  }
231
241
  //@ts-ignore
232
242
  mapOption(option) {
233
- return h("salla-map", { zoom: 15, lat: this.getLatLng(option.value, 'lat'), lng: this.getLatLng(option.value, 'lng'), name: `options[${option.id}]`, searchable: true, required: option.required, onSelected: e => this.changedHandler(e, option) });
243
+ return h("salla-map", { zoom: 15, lat: this.getLatLng(option.value, 'lat'), lng: this.getLatLng(option.value, 'lng'), name: `options[${option.id}]`, searchable: true, required: option.required, onInvalidInput: (e) => this.invalidHandler(e, option), onSelected: e => this.changedHandler(e, option) });
234
244
  }
235
245
  colorPickerOption(option) {
236
- return h("salla-color-picker", { onSubmitted: e => this.changedHandler(e, option), name: `options[${option.id}]`, required: option.required, color: option.value || '#5dd5c4' });
246
+ return h("salla-color-picker", { onSubmitted: e => this.changedHandler(e, option), name: `options[${option.id}]`, required: option.required, onInvalidInput: (e) => this.invalidHandler(e, option), color: option.value || '#5dd5c4' });
237
247
  }
238
248
  /**
239
249
  * ============= Date Time options =============
240
250
  */
241
251
  //@ts-ignore
242
252
  timeOption(option) {
243
- return h("salla-datetime-picker", { noCalendar: true, enableTime: true, dateFormat: "h:i K", value: option.value, placeholder: option.name, required: option.required, name: `options[${option.id}]`, class: "s-product-options-time-element", onPicked: e => this.changedHandler(e, option) });
253
+ return h("salla-datetime-picker", { noCalendar: true, enableTime: true, dateFormat: "h:i K", value: option.value, placeholder: option.name, required: option.required, name: `options[${option.id}]`, class: "s-product-options-time-element", onInvalidInput: (e) => this.invalidHandler(e, option), onPicked: e => this.changedHandler(e, option) });
244
254
  }
245
255
  //@ts-ignore
246
256
  dateOption(option) {
247
257
  //todo:: consider date-range @see https://github.com/SallaApp/theme-raed/blob/master/src/assets/js/partials/product-options.js#L8-L23
248
258
  return h("div", { class: "s-product-options-date-element" },
249
- h("salla-datetime-picker", { value: option.value, placeholder: option.name, required: option.required, minDate: new Date(), name: `options[${option.id}]`, onPicked: e => this.changedHandler(e, option) }));
259
+ h("salla-datetime-picker", { value: option.value, placeholder: option.name, required: option.required, minDate: new Date(), name: `options[${option.id}]`, onInvalidInput: (e) => this.invalidHandler(e, option), onPicked: e => this.changedHandler(e, option) }));
250
260
  }
251
261
  //@ts-ignore
252
262
  datetimeOption(option) {
253
263
  //todo:: consider date-range @see https://github.com/SallaApp/theme-raed/blob/master/src/assets/js/partials/product-options.js#L8-L23
254
264
  return h("div", { class: "s-product-options-datetime-element" },
255
- h("salla-datetime-picker", { enableTime: true, value: option.value, dateFormat: "Y-m-d G:i:K", placeholder: option.name, required: option.required, name: `options[${option.id}]`, maxDate: option.to_date_time, minDate: option.from_date_time, onPicked: e => this.changedHandler(e, option) }));
265
+ h("salla-datetime-picker", { enableTime: true, value: option.value, dateFormat: "Y-m-d G:i:K", placeholder: option.name, required: option.required, name: `options[${option.id}]`, maxDate: option.to_date_time, minDate: option.from_date_time, onInvalidInput: (e) => this.invalidHandler(e, option), onPicked: e => this.changedHandler(e, option) }));
256
266
  }
257
267
  /**
258
268
  * ============= Advanced options =============
259
269
  */
260
- // <<<<<<< HEAD
261
270
  getOptionDetailName(detail, outOfStock = true, optionType) {
262
271
  if (optionType && optionType == DisplayType.COLOR) {
263
272
  return detail.name
@@ -288,7 +297,7 @@ export class SallaProductOptions {
288
297
  }
289
298
  singleOption(option) {
290
299
  return h("div", null,
291
- h("select", { name: `options[${option.id}]`, required: option.required, class: "s-form-control", onChange: e => this.changedHandler(e, option) },
300
+ h("select", { name: `options[${option.id}]`, required: option.required, class: "s-form-control", onInvalid: (e) => this.invalidHandler(e, option), onChange: e => this.changedHandler(e, option) },
292
301
  h("option", { value: "" }, option.placeholder), option === null || option === void 0 ? void 0 :
293
302
  option.details.map((detail) => {
294
303
  return h("option", { value: detail.id, disabled: this.canDisabled && this.isOptionDetailOut(detail), selected: detail.is_selected }, this.getOptionDetailName(detail));
@@ -297,14 +306,14 @@ export class SallaProductOptions {
297
306
  multipleOptions(option) {
298
307
  return h("div", { class: { "s-product-options-multiple-options-wrapper": true, 'required': option.required } }, option === null || option === void 0 ? void 0 : option.details.map((detail) => {
299
308
  return h("div", null,
300
- h("input", { type: "checkbox", value: detail.id, disabled: this.isOptionDetailOut(detail), checked: detail.is_selected, required: option.required, name: `options[${option.id}][]`, id: `field-${option.id}-${detail.id}`, onChange: (e) => this.changedHandler(e, option), "aria-describedby": `options[${option.id}]-description` }),
309
+ h("input", { type: "checkbox", value: detail.id, disabled: this.isOptionDetailOut(detail), checked: detail.is_selected, required: option.required, name: `options[${option.id}][]`, id: `field-${option.id}-${detail.id}`, onChange: (e) => this.changedHandler(e, option), onInvalid: (e) => this.invalidHandler(e, option), "aria-describedby": `options[${option.id}]-description` }),
301
310
  h("label", { htmlFor: `field-${option.id}-${detail.id}` }, this.getOptionDetailName(detail)));
302
311
  }));
303
312
  }
304
313
  //@ts-ignore
305
314
  colorOption(option) {
306
315
  return h("fieldset", { class: "s-product-options-colors-wrapper" }, option === null || option === void 0 ? void 0 : option.details.map((detail) => h("div", { class: "s-product-options-colors-item" },
307
- h("input", { type: "radio", value: detail.id, required: option.required, checked: detail.is_selected, name: `options[${option.id}]`, disabled: this.canDisabled && this.isOptionDetailOut(detail), id: `color-${this.productId}-${option.id}-${detail.id}`, onChange: e => this.changedHandler(e, option) }),
316
+ h("input", { type: "radio", value: detail.id, required: option.required, checked: detail.is_selected, name: `options[${option.id}]`, disabled: this.canDisabled && this.isOptionDetailOut(detail), id: `color-${this.productId}-${option.id}-${detail.id}`, onInvalid: (e) => this.invalidHandler(e, option), onChange: e => this.changedHandler(e, option) }),
308
317
  h("label", { htmlFor: `color-${this.productId}-${option.id}-${detail.id}` },
309
318
  h("span", { style: { "background-color": detail.color } }),
310
319
  h("div", { innerHTML: this.getOptionDetailName(detail, true, option.type) })))));
@@ -313,7 +322,7 @@ export class SallaProductOptions {
313
322
  thumbnailOption(option) {
314
323
  return h("div", { class: "s-product-options-thumbnails-wrapper" }, option.details.map((detail) => {
315
324
  return h("div", null,
316
- h("input", { type: "radio", value: detail.id, "data-itemid": detail.id, required: option.required, checked: detail.is_selected, name: `options[${option.id}]`, "data-img-id": detail.option_value, disabled: this.canDisabled && this.isOptionDetailOut(detail), id: `option_${this.productId}-${option.id}_${detail.id}`, onChange: (e) => this.changedHandler(e, option) }),
325
+ h("input", { type: "radio", value: detail.id, "data-itemid": detail.id, required: option.required, checked: detail.is_selected, name: `options[${option.id}]`, "data-img-id": detail.option_value, disabled: this.canDisabled && this.isOptionDetailOut(detail), id: `option_${this.productId}-${option.id}_${detail.id}`, onInvalid: (e) => this.invalidHandler(e, option), onChange: (e) => this.changedHandler(e, option) }),
317
326
  h("label", { htmlFor: `option_${this.productId}-${option.id}_${detail.id}`, "data-img-id": detail.option_value, class: "go-to-slide" },
318
327
  h("img", { "data-src": detail.image, src: detail.image, title: detail.name, alt: detail.name }),
319
328
  h("span", { innerHTML: CheckCircleIcon, class: "s-product-options-thumbnails-icon" }),
@@ -432,7 +441,7 @@ export class SallaProductOptions {
432
441
  "return": "Promise<boolean>"
433
442
  },
434
443
  "docs": {
435
- "text": "Get the id's of the selected options.",
444
+ "text": "Report options form validity.",
436
445
  "tags": []
437
446
  }
438
447
  },
@@ -93,10 +93,10 @@ const SallaAddProductButton$1 = /*@__PURE__*/ proxyCustomElement(class extends H
93
93
  return '';
94
94
  }
95
95
  if (this.hasSubscribedOptions) {
96
- return h(Host, null, h("salla-product-availability", Object.assign({}, this.getBtnAttributes(), { "is-subscribed": true })));
96
+ return h(Host, null, h("salla-product-availability", Object.assign({}, this.getBtnAttributes(), { "is-subscribed": true }), h("span", { class: "s-hidden" }, h("slot", null))));
97
97
  }
98
98
  if ((this.productStatus === 'out-and-notify' && this.channels) || this.hasOutOfStockOption) {
99
- return h(Host, null, h("salla-product-availability", Object.assign({}, this.getBtnAttributes())));
99
+ return h(Host, null, h("salla-product-availability", Object.assign({}, this.getBtnAttributes()), h("span", { class: "s-hidden" }, h("slot", null))));
100
100
  }
101
101
  return h(Host, null, h("salla-button", Object.assign({ color: this.productStatus === 'sale' ? 'primary' : 'light', type: "button", fill: this.productStatus === 'sale' ? 'solid' : 'outline', ref: el => this.btn = el, onClick: event => this.addProductToCart(event), disabled: this.productStatus !== 'sale' }, this.getBtnAttributes(), { "loader-position": "center" }), h("slot", null)));
102
102
  }
@@ -105,19 +105,20 @@ const SallaAddProductButton$1 = /*@__PURE__*/ proxyCustomElement(class extends H
105
105
  return;
106
106
  }
107
107
  salla.event.on('product-options::change', async (data) => {
108
- var _a;
108
+ var _a, _b;
109
109
  if (!['thumbnail', 'color', 'single-option'].includes(data.option.type)) {
110
110
  return;
111
111
  }
112
- this.hasOutOfStockOption = await ((_a = document.querySelector(`salla-product-options[product-id="${this.productId}"]`)) === null || _a === void 0 ? void 0 : _a.hasOutOfStockOption());
112
+ this.hasSubscribedOptions = false;
113
113
  this.hasLabel = false;
114
- if (!this.subscribedOptions || this.subscribedOptions === 'null') {
114
+ this.selectedOptions = await ((_a = document.querySelector(`salla-product-options[product-id="${this.productId}"]`)) === null || _a === void 0 ? void 0 : _a.getSelectedOptions());
115
+ this.hasOutOfStockOption = await ((_b = document.querySelector(`salla-product-options[product-id="${this.productId}"]`)) === null || _b === void 0 ? void 0 : _b.hasOutOfStockOption());
116
+ let subscribedDetails = salla.storage.get(`product-${this.productId}-subscribed-options`);
117
+ if (!subscribedDetails && !this.subscribedOptions || !this.hasOutOfStockOption) {
115
118
  return;
116
119
  }
117
- //check if subscribedOptions are the selected now
118
- this.hasSubscribedOptions = JSON.parse(this.subscribedOptions)
119
- .filter(ids => ids.every(id => this.selectedOptions.some(option => option.id === id)))
120
- .length > 0;
120
+ const parsedSubscribedDetails = subscribedDetails ? subscribedDetails.map(ids => ids.split(',').map(id => parseInt(id))) : [];
121
+ this.hasSubscribedOptions = parsedSubscribedDetails.length > 0 && parsedSubscribedDetails.some(ids => ids.every(id => this.selectedOptions.some(option => option.id === id)));
121
122
  });
122
123
  }
123
124
  componentDidRender() {
@@ -1011,6 +1011,7 @@ const SallaColorPicker = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
1011
1011
  super();
1012
1012
  this.__registerHost();
1013
1013
  this.colorChanged = createEvent(this, "colorChanged", 7);
1014
+ this.invalidInput = createEvent(this, "invalidInput", 7);
1014
1015
  this.submitted = createEvent(this, "submitted", 7);
1015
1016
  this.popupOpened = createEvent(this, "popupOpened", 7);
1016
1017
  this.popupClosed = createEvent(this, "popupClosed", 7);
@@ -1117,6 +1118,13 @@ const SallaColorPicker = /*@__PURE__*/ proxyCustomElement(class extends HTMLElem
1117
1118
  componentDidLoad() {
1118
1119
  this.canvas.style.backgroundColor = this.color;
1119
1120
  this.initColorPicker();
1121
+ this.colorInput.addEventListener('invalid', e => {
1122
+ this.invalidInput.emit(e);
1123
+ });
1124
+ this.colorInput.addEventListener('input', () => {
1125
+ this.colorInput.setCustomValidity('');
1126
+ this.colorInput.reportValidity();
1127
+ });
1120
1128
  }
1121
1129
  setPopIpPosition() {
1122
1130
  const popup = this.host.querySelector('.picker_wrapper');
@@ -2559,6 +2559,7 @@ const SallaDatetimePicker = /*@__PURE__*/ proxyCustomElement(class extends HTMLE
2559
2559
  super();
2560
2560
  this.__registerHost();
2561
2561
  this.picked = createEvent(this, "picked", 7);
2562
+ this.invalidInput = createEvent(this, "invalidInput", 7);
2562
2563
  /**
2563
2564
  * Two way data binding to retrieve the selected date[time] value
2564
2565
  */
@@ -2783,6 +2784,13 @@ const SallaDatetimePicker = /*@__PURE__*/ proxyCustomElement(class extends HTMLE
2783
2784
  // onClose: this.handleOnClose(selectedDates, dateStr, instance)
2784
2785
  };
2785
2786
  flatpickr(this.dateInput, options);
2787
+ this.dateInput.addEventListener('invalid', e => {
2788
+ this.invalidInput.emit(e);
2789
+ });
2790
+ this.dateInput.addEventListener('input', () => {
2791
+ this.dateInput.setCustomValidity('');
2792
+ this.dateInput.reportValidity();
2793
+ });
2786
2794
  }
2787
2795
  static get style() { return sallaDatetimePickerCss; }
2788
2796
  }, [0, "salla-datetime-picker", {
@@ -15110,6 +15110,7 @@ const SallaFileUpload = /*@__PURE__*/ proxyCustomElement(class extends HTMLEleme
15110
15110
  super();
15111
15111
  this.__registerHost();
15112
15112
  this.added = createEvent(this, "added", 7);
15113
+ this.invalidInput = createEvent(this, "invalidInput", 7);
15113
15114
  this.uploaded = createEvent(this, "uploaded", 7);
15114
15115
  this.removed = createEvent(this, "removed", 7);
15115
15116
  /**
@@ -15270,15 +15271,18 @@ const SallaFileUpload = /*@__PURE__*/ proxyCustomElement(class extends HTMLEleme
15270
15271
  return this.uploaded.emit(this.value);
15271
15272
  }
15272
15273
  removedHandler(deletedFile) {
15274
+ var _a;
15273
15275
  let fileInput = this.getFormDataFileInput();
15274
15276
  fileInput.type = 'hidden';
15275
15277
  fileInput.value = '';
15278
+ (_a = this.host.closest('.s-product-options-option')) === null || _a === void 0 ? void 0 : _a.removeAttribute('data-has-value');
15276
15279
  if (deletedFile.getMetadata('id')) {
15277
15280
  salla.cart.api.deleteImage(deletedFile.getMetadata('id'));
15278
15281
  }
15279
15282
  if (this.height) {
15280
15283
  setTimeout(() => this.host.querySelector('.filepond--root').style.height = this.height, 1000);
15281
15284
  }
15285
+ this.hiddenInput.value = '';
15282
15286
  fileInput.dispatchEvent(new window.Event('change', { bubbles: true }));
15283
15287
  return this.removed.emit(deletedFile);
15284
15288
  }
@@ -15311,6 +15315,7 @@ const SallaFileUpload = /*@__PURE__*/ proxyCustomElement(class extends HTMLEleme
15311
15315
  return this.host.querySelector('.filepond--data input');
15312
15316
  }
15313
15317
  getFiles() {
15318
+ var _a;
15314
15319
  if (!this.value && !this.files) {
15315
15320
  return [];
15316
15321
  }
@@ -15318,6 +15323,9 @@ const SallaFileUpload = /*@__PURE__*/ proxyCustomElement(class extends HTMLEleme
15318
15323
  let files = this.files
15319
15324
  ? JSON.parse(this.files)
15320
15325
  : this.value.split(',').map(file => ({ url: file }));
15326
+ if (files.length) {
15327
+ (_a = this.host.closest('.s-product-options-option')) === null || _a === void 0 ? void 0 : _a.setAttribute('data-has-value', 'true');
15328
+ }
15321
15329
  return files.map(file => ({
15322
15330
  source: file.id ? `${file.id}` : file.url,
15323
15331
  options: {
@@ -15345,7 +15353,7 @@ const SallaFileUpload = /*@__PURE__*/ proxyCustomElement(class extends HTMLEleme
15345
15353
  return (h(Host, { class: {
15346
15354
  "s-file-upload": true,
15347
15355
  "s-file-upload-profile-image": this.profileImage,
15348
- } }, h("input", { type: "file", name: this.name, value: this.value, ref: ele => this.fileUploader = ele, required: this.required, class: "s-file-upload-wrapper s-file-upload-input", accept: this.accept })));
15356
+ } }, h("input", { type: "file", name: this.name, value: this.value, ref: ele => this.fileUploader = ele, required: this.required, class: "s-file-upload-wrapper s-file-upload-input", accept: this.accept }), h("input", { class: "s-file-upload-hidden-input", name: 'hidden-' + this.name, required: this.required, value: this.value, ref: input => this.hiddenInput = input })));
15349
15357
  }
15350
15358
  componentDidLoad() {
15351
15359
  let files = this.getFiles();
@@ -15437,6 +15445,13 @@ const SallaFileUpload = /*@__PURE__*/ proxyCustomElement(class extends HTMLEleme
15437
15445
  resolve(true);
15438
15446
  }),
15439
15447
  });
15448
+ this.hiddenInput.addEventListener('invalid', e => {
15449
+ this.invalidInput.emit(e);
15450
+ });
15451
+ this.hiddenInput.addEventListener('change', () => {
15452
+ this.hiddenInput.setCustomValidity('');
15453
+ this.hiddenInput.reportValidity();
15454
+ });
15440
15455
  }
15441
15456
  get host() { return this; }
15442
15457
  static get style() { return sallaFileUploadCss; }
@@ -447,6 +447,7 @@ const SallaMap = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
447
447
  this.selected = createEvent(this, "selected", 7);
448
448
  this.mapClicked = createEvent(this, "mapClicked", 7);
449
449
  this.currentLocationChanged = createEvent(this, "currentLocationChanged", 7);
450
+ this.invalidInput = createEvent(this, "invalidInput", 7);
450
451
  this.defaultLat = 21.419421; //Mecca 🕋
451
452
  this.defaultLng = 39.82553; //Mecca 🕋
452
453
  // state variables
@@ -651,6 +652,13 @@ const SallaMap = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
651
652
  }
652
653
  });
653
654
  }
655
+ this.mapInput.addEventListener('invalid', e => {
656
+ this.invalidInput.emit(e);
657
+ });
658
+ this.mapInput.addEventListener('input', () => {
659
+ this.mapInput.setCustomValidity('');
660
+ this.mapInput.reportValidity();
661
+ });
654
662
  }
655
663
  /**
656
664
  * Open location component
@@ -25,12 +25,34 @@ const SallaProductAvailability = /*@__PURE__*/ proxyCustomElement(class extends
25
25
  * is current user already subscribed
26
26
  */
27
27
  this.isSubscribed = false;
28
+ this.handleSubmitOptions = async () => {
29
+ let payload = { id: this.productId };
30
+ if (!this.notifyOptionsAvailability) {
31
+ return payload;
32
+ }
33
+ let optionsElement = document.querySelector(`salla-product-options[product-id="${this.productId}"]`);
34
+ let options = Object.values(await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.getSelectedOptionsData()) || {});
35
+ //if all options not selected, show message && throw exception
36
+ if (options.length && !await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.reportValidity())) {
37
+ let errorMessage = salla.lang.get('common.messages.required_fields');
38
+ salla.error(errorMessage);
39
+ throw errorMessage;
40
+ }
41
+ payload.options = [];
42
+ options.forEach(option => {
43
+ //inject numbers only, without zeros
44
+ if (option && !isNaN(option)) {
45
+ payload.options.push(Number(option));
46
+ }
47
+ });
48
+ return payload;
49
+ };
28
50
  // helpers
29
- this.typing = (e, submitMethod) => {
51
+ this.typing = (e) => {
30
52
  const error = e.target.nextElementSibling;
31
53
  e.target.classList.remove('s-has-error');
32
54
  (error === null || error === void 0 ? void 0 : error.classList.contains('s-product-availability-error-msg')) && (error.innerText = '');
33
- e.key == 'Enter' && submitMethod();
55
+ e.keyCode === 13 && this.submit();
34
56
  };
35
57
  salla.lang.onLoaded(() => {
36
58
  var _a;
@@ -52,28 +74,6 @@ const SallaProductAvailability = /*@__PURE__*/ proxyCustomElement(class extends
52
74
  channelsWatcher(newValue) {
53
75
  this.channels_ = !!newValue ? newValue.split(',') : [];
54
76
  }
55
- async handleSubmitOptions() {
56
- let payload = { id: this.productId };
57
- if (!this.notifyOptionsAvailability) {
58
- return payload;
59
- }
60
- let optionsElement = document.querySelector(`salla-product-options[product-id="${this.productId}"]`);
61
- let options = Object.values(await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.getSelectedOptionsData()) || {});
62
- //if all options not selected, show message && throw exception
63
- if (options.length && !await (optionsElement === null || optionsElement === void 0 ? void 0 : optionsElement.reportValidity())) {
64
- let errorMessage = salla.lang.get('common.messages.required_fields');
65
- salla.error(errorMessage);
66
- throw errorMessage;
67
- }
68
- payload.options = [];
69
- options.forEach(option => {
70
- //inject numbers only, without zeros
71
- if (option && !isNaN(option)) {
72
- payload.options.push(Number(option));
73
- }
74
- });
75
- return payload;
76
- }
77
77
  openModel() {
78
78
  this.handleSubmitOptions().then(isSuccess => isSuccess ? this.modal.open() : null);
79
79
  }
@@ -96,9 +96,23 @@ const SallaProductAvailability = /*@__PURE__*/ proxyCustomElement(class extends
96
96
  .then(() => this.btn.disable())
97
97
  .then(() => salla.api.product.availabilitySubscribe(payload))
98
98
  .then(() => {
99
+ if (!this.notifyOptionsAvailability) {
100
+ salla.storage.set(`product-${this.productId}-subscribed`, true);
101
+ return;
102
+ }
103
+ if (payload.options.length) {
104
+ let options = salla.storage.get(`product-${this.productId}-subscribed-options`) || [];
105
+ let selectedOptionsString = payload.options.join(',');
106
+ if (!options.includes(selectedOptionsString)) {
107
+ options.push(selectedOptionsString);
108
+ salla.storage.set(`product-${this.productId}-subscribed-options`, options);
109
+ }
110
+ else {
111
+ salla.log('already subscribed to this options');
112
+ }
113
+ }
99
114
  this.isSubscribed = true;
100
- salla.storage.set(`product-${this.productId}-subscribed`, true);
101
- }) //no need to wait until finishing alert animation
115
+ })
102
116
  .then(() => this.btn.stop())
103
117
  .then(() => this.modal.close())
104
118
  .catch(() => this.btn.stop() && this.btn.enable());
@@ -134,11 +148,11 @@ const SallaProductAvailability = /*@__PURE__*/ proxyCustomElement(class extends
134
148
  renderModal() {
135
149
  return (h("salla-modal", { ref: modal => this.modal = modal, "modal-title": this.title_, subTitle: salla.lang.get('pages.products.notify_availability_subtitle'), width: "sm" }, h("span", { slot: 'icon', class: "s-product-availability-header-icon", innerHTML: BellRing }), h("div", { class: "s-product-availability-body" }, this.channels_.includes('email') ? [
136
150
  h("label", { class: "s-product-availability-label" }, salla.lang.get('common.elements.email')),
137
- h("input", { class: "s-product-availability-input", onKeyDown: e => this.typing(e, this.submit), placeholder: salla.lang.get('common.elements.email_placeholder') || 'your@email.com', ref: el => this.email = el, type: "email" }),
151
+ h("input", { class: "s-product-availability-input", onKeyDown: e => this.typing(e), placeholder: salla.lang.get('common.elements.email_placeholder') || 'your@email.com', ref: el => this.email = el, type: "email" }),
138
152
  h("span", { class: "s-product-availability-error-msg" })
139
153
  ] : '', this.channels_.includes('sms') ? [
140
154
  h("label", { class: "s-product-availability-label" }, salla.lang.get('common.elements.mobile')),
141
- h("salla-tel-input", { ref: el => this.mobileInput = el, onKeyDown: e => this.typing(e, this.submit) })
155
+ h("salla-tel-input", { ref: el => this.mobileInput = el, onKeyDown: e => this.typing(e) })
142
156
  ] : ''), h("div", { slot: "footer", class: "s-product-availability-footer" }, h("salla-button", { class: "modal-cancel-btn", width: "wide", color: "light", fill: "outline", onClick: () => this.modal.close() }, salla.lang.get('common.elements.cancel')), h("salla-button", { class: "submit-btn", "loader-position": 'center', width: "wide", ref: btn => this.btn = btn, onClick: () => this.submit() }, salla.lang.get('common.elements.submit')))));
143
157
  }
144
158
  get host() { return this; }