@stubber/form-fields 1.0.0

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 (151) hide show
  1. package/README.md +303 -0
  2. package/dist/Field.svelte +36 -0
  3. package/dist/Field.svelte.d.ts +31 -0
  4. package/dist/Form.svelte +34 -0
  5. package/dist/Form.svelte.d.ts +29 -0
  6. package/dist/NullFieldWrapper.svelte +6 -0
  7. package/dist/NullFieldWrapper.svelte.d.ts +25 -0
  8. package/dist/fields/component_parts/arraybuilder/FieldWrapper.svelte +69 -0
  9. package/dist/fields/component_parts/arraybuilder/FieldWrapper.svelte.d.ts +29 -0
  10. package/dist/fields/component_parts/fieldbuilder/FieldWrapper.svelte +8 -0
  11. package/dist/fields/component_parts/fieldbuilder/FieldWrapper.svelte.d.ts +25 -0
  12. package/dist/fields/components/AgGrid.svelte +53 -0
  13. package/dist/fields/components/AgGrid.svelte.d.ts +18 -0
  14. package/dist/fields/components/Arraybuilder.svelte +101 -0
  15. package/dist/fields/components/Arraybuilder.svelte.d.ts +25 -0
  16. package/dist/fields/components/Checkbox.svelte +100 -0
  17. package/dist/fields/components/Checkbox.svelte.d.ts +23 -0
  18. package/dist/fields/components/CheckboxAutocomplete.svelte +92 -0
  19. package/dist/fields/components/CheckboxAutocomplete.svelte.d.ts +23 -0
  20. package/dist/fields/components/Contactselector.svelte +348 -0
  21. package/dist/fields/components/Contactselector.svelte.d.ts +25 -0
  22. package/dist/fields/components/Currency.svelte +258 -0
  23. package/dist/fields/components/Currency.svelte.d.ts +23 -0
  24. package/dist/fields/components/Dataindication.svelte +35 -0
  25. package/dist/fields/components/Dataindication.svelte.d.ts +23 -0
  26. package/dist/fields/components/Date.svelte +94 -0
  27. package/dist/fields/components/Date.svelte.d.ts +23 -0
  28. package/dist/fields/components/Datetime.svelte +94 -0
  29. package/dist/fields/components/Datetime.svelte.d.ts +23 -0
  30. package/dist/fields/components/Email.svelte +124 -0
  31. package/dist/fields/components/Email.svelte.d.ts +23 -0
  32. package/dist/fields/components/Fieldbuilder.svelte +340 -0
  33. package/dist/fields/components/Fieldbuilder.svelte.d.ts +25 -0
  34. package/dist/fields/components/Fieldsbuilder.svelte +122 -0
  35. package/dist/fields/components/Fieldsbuilder.svelte.d.ts +25 -0
  36. package/dist/fields/components/File.svelte +230 -0
  37. package/dist/fields/components/File.svelte.d.ts +25 -0
  38. package/dist/fields/components/Heading.svelte +17 -0
  39. package/dist/fields/components/Heading.svelte.d.ts +23 -0
  40. package/dist/fields/components/Hidden.svelte +7 -0
  41. package/dist/fields/components/Hidden.svelte.d.ts +23 -0
  42. package/dist/fields/components/Hiddenlocation.svelte +28 -0
  43. package/dist/fields/components/Hiddenlocation.svelte.d.ts +23 -0
  44. package/dist/fields/components/Html.svelte +13 -0
  45. package/dist/fields/components/Html.svelte.d.ts +23 -0
  46. package/dist/fields/components/Jsoneditor.svelte +94 -0
  47. package/dist/fields/components/Jsoneditor.svelte.d.ts +23 -0
  48. package/dist/fields/components/Map.svelte +192 -0
  49. package/dist/fields/components/Map.svelte.d.ts +25 -0
  50. package/dist/fields/components/Multicheckbox.svelte +119 -0
  51. package/dist/fields/components/Multicheckbox.svelte.d.ts +23 -0
  52. package/dist/fields/components/Multistep.svelte +86 -0
  53. package/dist/fields/components/Multistep.svelte.d.ts +25 -0
  54. package/dist/fields/components/Note.svelte +92 -0
  55. package/dist/fields/components/Note.svelte.d.ts +23 -0
  56. package/dist/fields/components/Number.svelte +118 -0
  57. package/dist/fields/components/Number.svelte.d.ts +23 -0
  58. package/dist/fields/components/Objectbuilder.svelte +152 -0
  59. package/dist/fields/components/Objectbuilder.svelte.d.ts +25 -0
  60. package/dist/fields/components/Qrcodescanner.svelte +198 -0
  61. package/dist/fields/components/Qrcodescanner.svelte.d.ts +23 -0
  62. package/dist/fields/components/Radio.svelte +116 -0
  63. package/dist/fields/components/Radio.svelte.d.ts +23 -0
  64. package/dist/fields/components/Renderfield.svelte +58 -0
  65. package/dist/fields/components/Renderfield.svelte.d.ts +25 -0
  66. package/dist/fields/components/Screenrecorder.svelte +277 -0
  67. package/dist/fields/components/Screenrecorder.svelte.d.ts +25 -0
  68. package/dist/fields/components/Screenshot.svelte +270 -0
  69. package/dist/fields/components/Screenshot.svelte.d.ts +25 -0
  70. package/dist/fields/components/Scrollandreaddisplay.svelte +122 -0
  71. package/dist/fields/components/Scrollandreaddisplay.svelte.d.ts +23 -0
  72. package/dist/fields/components/Section.svelte +64 -0
  73. package/dist/fields/components/Section.svelte.d.ts +25 -0
  74. package/dist/fields/components/Select.svelte +229 -0
  75. package/dist/fields/components/Select.svelte.d.ts +23 -0
  76. package/dist/fields/components/Selectresource.svelte +291 -0
  77. package/dist/fields/components/Selectresource.svelte.d.ts +25 -0
  78. package/dist/fields/components/Signature.svelte +153 -0
  79. package/dist/fields/components/Signature.svelte.d.ts +25 -0
  80. package/dist/fields/components/Slider.svelte +101 -0
  81. package/dist/fields/components/Slider.svelte.d.ts +23 -0
  82. package/dist/fields/components/SmartText.svelte +330 -0
  83. package/dist/fields/components/SmartText.svelte.d.ts +23 -0
  84. package/dist/fields/components/Telephone.svelte +153 -0
  85. package/dist/fields/components/Telephone.svelte.d.ts +23 -0
  86. package/dist/fields/components/Text.svelte +106 -0
  87. package/dist/fields/components/Text.svelte.d.ts +23 -0
  88. package/dist/fields/components/Voicenote.svelte +268 -0
  89. package/dist/fields/components/Voicenote.svelte.d.ts +25 -0
  90. package/dist/fields/components/index.d.ts +81 -0
  91. package/dist/fields/components/index.js +82 -0
  92. package/dist/fields/definitions/_all.json +38 -0
  93. package/dist/fields/definitions/_valid_fieldtype.json +220 -0
  94. package/dist/fields/definitions/arraybuilder.json +39 -0
  95. package/dist/fields/definitions/checkbox.json +44 -0
  96. package/dist/fields/definitions/contactselector.json +15 -0
  97. package/dist/fields/definitions/currency.json +42 -0
  98. package/dist/fields/definitions/dataindication.json +16 -0
  99. package/dist/fields/definitions/date.json +16 -0
  100. package/dist/fields/definitions/datetime.json +15 -0
  101. package/dist/fields/definitions/email.json +16 -0
  102. package/dist/fields/definitions/fieldbuilder.json +64 -0
  103. package/dist/fields/definitions/fieldsbuilder.json +38 -0
  104. package/dist/fields/definitions/file.json +42 -0
  105. package/dist/fields/definitions/grid.json +47 -0
  106. package/dist/fields/definitions/heading.json +38 -0
  107. package/dist/fields/definitions/hidden.json +89 -0
  108. package/dist/fields/definitions/hiddenlocation.json +15 -0
  109. package/dist/fields/definitions/html.json +34 -0
  110. package/dist/fields/definitions/index.d.ts +86 -0
  111. package/dist/fields/definitions/index.js +96 -0
  112. package/dist/fields/definitions/jsoneditor.json +33 -0
  113. package/dist/fields/definitions/map.json +36 -0
  114. package/dist/fields/definitions/multicheckbox.json +47 -0
  115. package/dist/fields/definitions/multistep.json +35 -0
  116. package/dist/fields/definitions/note.json +16 -0
  117. package/dist/fields/definitions/number.json +42 -0
  118. package/dist/fields/definitions/objectbuilder.json +39 -0
  119. package/dist/fields/definitions/qrcodescanner.json +16 -0
  120. package/dist/fields/definitions/radio.json +47 -0
  121. package/dist/fields/definitions/renderfield.json +36 -0
  122. package/dist/fields/definitions/richtext.json +16 -0
  123. package/dist/fields/definitions/screenrecorder.json +42 -0
  124. package/dist/fields/definitions/screenshot.json +42 -0
  125. package/dist/fields/definitions/scrollandreaddisplay.json +49 -0
  126. package/dist/fields/definitions/section.json +50 -0
  127. package/dist/fields/definitions/select.json +47 -0
  128. package/dist/fields/definitions/selectresource.json +48 -0
  129. package/dist/fields/definitions/signature.json +16 -0
  130. package/dist/fields/definitions/slider.json +78 -0
  131. package/dist/fields/definitions/smart_text.json +101 -0
  132. package/dist/fields/definitions/telephone.json +16 -0
  133. package/dist/fields/definitions/text.json +35 -0
  134. package/dist/fields/definitions/voicenote.json +43 -0
  135. package/dist/index.d.ts +2 -0
  136. package/dist/index.js +3 -0
  137. package/dist/thirdparty/mapbox/GeoCoder.svelte +10 -0
  138. package/dist/thirdparty/mapbox/GeoCoder.svelte.d.ts +25 -0
  139. package/dist/thirdparty/mapbox/Map.svelte +30 -0
  140. package/dist/thirdparty/mapbox/Map.svelte.d.ts +20 -0
  141. package/dist/thirdparty/mapbox/MapMarker.svelte +13 -0
  142. package/dist/thirdparty/mapbox/MapMarker.svelte.d.ts +31 -0
  143. package/dist/utils/createField.d.ts +6 -0
  144. package/dist/utils/createField.js +33 -0
  145. package/dist/utils/createForm.d.ts +1 -0
  146. package/dist/utils/createForm.js +501 -0
  147. package/dist/utils/index.d.ts +18 -0
  148. package/dist/utils/index.js +126 -0
  149. package/dist/utils/syncing.d.ts +11 -0
  150. package/dist/utils/syncing.js +134 -0
  151. package/package.json +78 -0
@@ -0,0 +1,348 @@
1
+ <script>
2
+ import { onMount, onDestroy } from "svelte";
3
+ import _ from "lodash-es";
4
+ import * as utils from "../../utils/index.js";
5
+ import { deepEqual } from "fast-equals";
6
+ import { writable } from "svelte/store";
7
+ import { syncStoreToStore } from "../../utils/syncing";
8
+
9
+ export let form;
10
+ export let field;
11
+
12
+ const internal = writable();
13
+
14
+ let filter_text = "";
15
+ let is_focused = false;
16
+ let input;
17
+
18
+ let dependencies = form.dependencies;
19
+ let clienthub = dependencies?.clienthub;
20
+ let createWantToModal = dependencies?.createWantToModal;
21
+ let stubber = dependencies?.stubber;
22
+ let socket = clienthub?.socket;
23
+ let stubref = stubber?.stubref;
24
+ let orguuid = stubber?.orguuid;
25
+
26
+ let clickOutside = utils.clickOutside;
27
+
28
+ $: state_key = $field.state?.state_key;
29
+ $: label = $field.spec?.title;
30
+ $: hide_label = $field.spec?.hide_label;
31
+
32
+ $: isValid = !$field.state?.validation || $field.state?.validation?.valid;
33
+ $: validationMessage = $field.state?.validation?.message;
34
+
35
+ onMount(() => {
36
+ // set field values that aren't set yet
37
+ let f = _.cloneDeep($field);
38
+ let initial_state_internal = {
39
+ selected_item: null,
40
+ raw_items: [],
41
+ focused_index: -1,
42
+ };
43
+ let initial_data = {
44
+ base: f?.data?.base,
45
+ base_label: f?.data?.base_label,
46
+ };
47
+ _.set(f, "data", initial_data);
48
+ _.set(f, "state.internal", initial_state_internal);
49
+ if (!deepEqual(f, $field)) $field = f;
50
+
51
+ syncStoreToStore(
52
+ field,
53
+ internal,
54
+ (a, b) => {
55
+ let _clone = _.cloneDeep(a.state?.internal) || {};
56
+
57
+ // get parts from data
58
+ if (a?.data?.base) {
59
+ _.set(_clone, "selected_item._value", a?.data?.base);
60
+ _.set(_clone, "selected_item._label", a?.data?.base_label);
61
+ }
62
+ filter_text = a?.data?.base_label ?? "";
63
+
64
+ // set field state if changed
65
+ if (!deepEqual(a?.state?.internal, _clone)) {
66
+ $field.state.internal = _clone;
67
+ }
68
+ return _clone;
69
+ },
70
+ (a, b) => {
71
+ let _clone = _.cloneDeep(a) || {};
72
+ // update the state
73
+ _.set(_clone, "state.internal", _.cloneDeep(b));
74
+ // update the data
75
+ _.set(_clone, "data.base", b?.selected_item?._value);
76
+ _.set(_clone, "data.base_label", b?.selected_item?._label ?? "");
77
+ return _clone;
78
+ }
79
+ );
80
+ });
81
+
82
+ $: items = _.isArray($internal?.raw_items)
83
+ ? $internal?.raw_items
84
+ .sort((a, b) => {
85
+ // show all is_personal items last
86
+ if (a.is_personal && !b.is_personal) return 1;
87
+ if (!a.is_personal && b.is_personal) return -1;
88
+ return 0;
89
+ })
90
+ .map((item) => {
91
+ let _label = item[$field.spec?.params?.label || $field.spec.label] ?? item._default_label;
92
+ let _value = item;
93
+ return { _label, _value };
94
+ })
95
+ : [];
96
+
97
+ let debounceLoad = utils.debounce(loadResults, 200);
98
+ $: debounceLoad(filter_text);
99
+ async function loadResults(ft) {
100
+ let comparison = _.cloneDeep($internal);
101
+
102
+ if (!clienthub) return;
103
+ let details = {
104
+ resource_name: "contacts",
105
+ };
106
+
107
+ details.params = structuredClone($field.spec.params) || {};
108
+ // add required/dynamic params (orguuid, stubref, input, and limit)
109
+ details.params.orguuid = orguuid;
110
+ details.params.stubref = stubref;
111
+ details.params.input = ft;
112
+ if (details.params.limit === undefined) {
113
+ details.params.limit = 50;
114
+ }
115
+
116
+ // emit join page event
117
+ socket.emit(
118
+ "request",
119
+ {
120
+ type: "resource",
121
+ details,
122
+ },
123
+ (res) => {
124
+ // TODO : handle failure
125
+ if (res.success) {
126
+ comparison.raw_items = res.payload?.contacts;
127
+
128
+ if (!deepEqual(comparison, $internal)) {
129
+ $internal = _.cloneDeep(comparison);
130
+ }
131
+ }
132
+ }
133
+ );
134
+ }
135
+
136
+ let teleportedNode = null;
137
+ onDestroy(() => {
138
+ if (teleportedNode) {
139
+ teleportedNode.remove();
140
+ teleportedNode = null;
141
+ }
142
+ });
143
+
144
+ // used to show the dropdown panel in the body instead of inside the component
145
+ // fixes an issue where the dropdown panel would be cut off by the parent container
146
+ function teleport(node) {
147
+ // Get the original position and width
148
+ let rect = node.getBoundingClientRect();
149
+ let originalWidth = node.offsetWidth;
150
+ let originalHeight = node.offsetHeight;
151
+
152
+ // Teleport to the body
153
+ let teleportContainer = document.body;
154
+ teleportContainer.appendChild(node);
155
+
156
+ teleportedNode = node;
157
+
158
+ // Apply the original width and position to the teleported element
159
+ node.style.width = originalWidth + "px";
160
+ node.style.height = originalHeight + "px";
161
+ node.style.position = "absolute";
162
+ node.style.top = rect.top + window.scrollY + "px";
163
+ node.style.left = rect.left + "px";
164
+
165
+ // set z-index to 1000
166
+ node.style.zIndex = 1000;
167
+ }
168
+
169
+ const setSelected = (item) => {
170
+ if (item?.is_personal) {
171
+ initImport(selectedItem);
172
+ return;
173
+ }
174
+ filter_text = item._label;
175
+ let comparison = _.cloneDeep($internal);
176
+ comparison.focused_index = -1;
177
+ comparison.selected_item = item;
178
+
179
+ if (!deepEqual(comparison, $internal)) {
180
+ $internal = _.cloneDeep(comparison);
181
+ }
182
+
183
+ is_focused = false;
184
+ input.blur();
185
+ };
186
+
187
+ function handleDeselect() {
188
+ let comparison = _.cloneDeep($internal);
189
+ comparison.selected_item = null;
190
+
191
+ if (!deepEqual(comparison, $internal)) {
192
+ $internal = _.cloneDeep(comparison);
193
+ }
194
+ filter_text = "";
195
+ }
196
+
197
+ let createNew = false;
198
+ const toggleCreateNew = () => {
199
+ createNew = !createNew;
200
+ if (createNew) {
201
+ createWantToModal("_create_new_contact", stubref, orguuid);
202
+ }
203
+ };
204
+
205
+ const handleCloseModal = async (contact) => {
206
+ let res = await loadResults(contact.descname);
207
+ if (res.success) {
208
+ let firstItem = items[0];
209
+ if (!firstItem?.is_personal) firstItem && setSelected(firstItem);
210
+ else {
211
+ loadResults(filter_text);
212
+ is_focused = true;
213
+ }
214
+ }
215
+ };
216
+
217
+ const initImport = (contact) => {
218
+ createWantToModal(
219
+ "_copy_personal_contact",
220
+ stubref,
221
+ orguuid,
222
+ { _personal_contact_copy: contact },
223
+ () => {
224
+ handleCloseModal(contact);
225
+ return true;
226
+ } // need to do it like this because cb must not be async
227
+ );
228
+ };
229
+ </script>
230
+
231
+ {#if $internal}
232
+ <div
233
+ use:clickOutside={() => {
234
+ if (is_focused) {
235
+ is_focused = false;
236
+ filter_text = $field?.data?.base_label ?? "";
237
+ }
238
+ }}
239
+ class="relative flex flex-col w-full text-surface-900"
240
+ >
241
+ <label for="input_{state_key}" class="block text-label {hide_label ? 'hidden' : ''}">
242
+ {label}
243
+ </label>
244
+ <div class="relative flex mt-2 rounded-md">
245
+ <input
246
+ on:keydown={(e) => {
247
+ switch (e.key) {
248
+ case "Enter":
249
+ if ($internal.focused_index >= 0 && $internal.focused_index < items.length) {
250
+ setSelected(items[$internal.focused_index]);
251
+ }
252
+ e.preventDefault();
253
+ break;
254
+ case "ArrowDown":
255
+ $internal.focused_index = ($internal.focused_index + 1) % items.length;
256
+ e.preventDefault();
257
+ break;
258
+ case "ArrowUp":
259
+ $internal.focused_index = ($internal.focused_index - 1 + items.length) % items.length;
260
+ e.preventDefault();
261
+ break;
262
+ }
263
+ }}
264
+ on:focus={() => {
265
+ if (!is_focused) {
266
+ is_focused = true;
267
+ filter_text = "";
268
+ }
269
+ }}
270
+ bind:this={input}
271
+ type="select"
272
+ id="input_mask{state_key}"
273
+ placeholder={label}
274
+ class="block w-full rounded-md border-0 py-1.5 pl-3 pr-9 text-field text-surface-900 ring-1 ring-inset {!isValid
275
+ ? 'ring-danger-500'
276
+ : 'ring-surface-300 focus:ring-primary-400'} focus:outline-none placeholder:text-surface-400 focus:ring-2 focus:ring-inset"
277
+ bind:value={filter_text}
278
+ autocomplete="off"
279
+ />
280
+ {#if !$internal.selected_item}
281
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
282
+ <i class="text-surface-300 fa-regular fa-arrows-up-down fa-fw" />
283
+ </div>
284
+ {/if}
285
+ {#if $internal.selected_item}
286
+ <button
287
+ on:click={handleDeselect}
288
+ class="absolute inset-y-0 right-0 flex items-center pr-2 text-surface-300 hover:text-danger-500"
289
+ >
290
+ <i class="fa-regular fa-x fa-fw" />
291
+ </button>
292
+ {/if}
293
+ </div>
294
+ {#if is_focused}
295
+ <div use:teleport class="z-10 w-full absolute inset-y-[70px]">
296
+ {#if items?.length > 0}
297
+ <ul
298
+ class="block max-h-[200px] overflow-y-auto w-full bg-white shadow-lg rounded-md ring-1 ring-surface-300"
299
+ >
300
+ {#each items as item, index (item?._value?._key)}
301
+ <li
302
+ class="group {$internal.focused_index === index
303
+ ? 'bg-primary-400'
304
+ : ''} hover:bg-primary-400 focus-within:bg-primary-400 text-field cursor-default"
305
+ >
306
+ <button
307
+ on:click|preventDefault={() => setSelected(item)}
308
+ class="selectitem text-surface-900 group-hover:text-white focus:text-white focus:outline-none px-4 py-1.5 w-full flex items-center"
309
+ >
310
+ {item._label}
311
+ {#if item.is_personal}
312
+ <div class="mx-2 bg-surface-300 rounded-full text-xs p-0.5 px-2 text-white">
313
+ Import from personal
314
+ </div>
315
+ {/if}
316
+ {#if item?._value?._key == $internal?.selected_item?._value?._key}
317
+ <i class="text-primary-400 ml-auto fa-regular fa-check" />
318
+ {/if}
319
+ </button>
320
+ </li>
321
+ {/each}
322
+ </ul>
323
+ {:else}
324
+ <div
325
+ class="h-[100px] flex items-center justify-center w-full bg-white shadow-lg rounded-md ring-1 ring-surface-300"
326
+ >
327
+ <span class="text-paragraph text-surface-400">Start typing to search...</span>
328
+ </div>
329
+ {/if}
330
+ </div>
331
+ {/if}
332
+ <div class="mt-0 ml-auto">
333
+ <button
334
+ on:click={toggleCreateNew}
335
+ type="button"
336
+ class="text-surface-500 text-sm hover:text-surface-700"
337
+ >
338
+ <i class="fa-regular fa-plus" />
339
+ Create new contact
340
+ </button>
341
+ </div>
342
+ {#if validationMessage}
343
+ <p class="text-label {!isValid ? `text-danger-500` : `text-success-500`}">
344
+ {validationMessage}
345
+ </p>
346
+ {/if}
347
+ </div>
348
+ {/if}
@@ -0,0 +1,25 @@
1
+ /** @typedef {typeof __propDef.props} ContactselectorProps */
2
+ /** @typedef {typeof __propDef.events} ContactselectorEvents */
3
+ /** @typedef {typeof __propDef.slots} ContactselectorSlots */
4
+ export default class Contactselector extends SvelteComponentTyped<{
5
+ form: any;
6
+ field: any;
7
+ }, {
8
+ [evt: string]: CustomEvent<any>;
9
+ }, {}> {
10
+ }
11
+ export type ContactselectorProps = typeof __propDef.props;
12
+ export type ContactselectorEvents = typeof __propDef.events;
13
+ export type ContactselectorSlots = typeof __propDef.slots;
14
+ import { SvelteComponentTyped } from "svelte";
15
+ declare const __propDef: {
16
+ props: {
17
+ form: any;
18
+ field: any;
19
+ };
20
+ events: {
21
+ [evt: string]: CustomEvent<any>;
22
+ };
23
+ slots: {};
24
+ };
25
+ export {};
@@ -0,0 +1,258 @@
1
+ <script>
2
+ import { syncStoreToStore } from "../../utils/syncing";
3
+ import { deepEqual } from "fast-equals";
4
+ import _ from "lodash-es";
5
+ import { onMount } from "svelte";
6
+ import { writable } from "svelte/store";
7
+ import currency from "currency.js";
8
+ import getSymbolFromCurrency from "currency-symbol-map";
9
+ import * as utils from "../../utils/index.js";
10
+
11
+ export let field;
12
+
13
+ let { clickOutside, inputRegexMask } = utils;
14
+
15
+ const currencyRegex = /^([0-9]+(\.?[0-9]?[0-9]?)?)$/;
16
+ const currencyName = new Intl.DisplayNames("en-US", {
17
+ type: "currency",
18
+ });
19
+
20
+ const internal = writable();
21
+ let show_currency_list;
22
+
23
+ function toggleCurrencySelectorPopout() {
24
+ show_currency_list = !show_currency_list;
25
+ }
26
+
27
+ function handleFocusOut(e) {
28
+ let targetIsOption = e.relatedTarget?.classList?.contains("selectitem");
29
+ if (!targetIsOption) {
30
+ show_currency_list = false;
31
+ }
32
+ }
33
+
34
+ $: state_key = $field.state?.state_key;
35
+ $: label = $field.spec?.title;
36
+ $: hide_label = $field.spec?.hide_label;
37
+ $: isValid = !$field.state?.validation || $field.state?.validation?.valid;
38
+ $: validationMessage = $field.state?.validation?.message;
39
+ $: currencyFixed = $field.spec?.params?.currency;
40
+ $: currencyWhitelist = $field.spec?.params?.currency_whitelist ?? [];
41
+ $: currencyBlacklist = $field.spec?.params?.currency_blacklist ?? [];
42
+ $: currencyList =
43
+ currencyWhitelist.length > 0
44
+ ? currencyWhitelist.filter(Boolean).map((c) => {
45
+ let name;
46
+ try {
47
+ name = currencyName.of(c);
48
+ } catch (err) {
49
+ console.warn(err);
50
+ }
51
+ return {
52
+ value: c,
53
+ symbol: getSymbolFromCurrency(c),
54
+ name,
55
+ };
56
+ })
57
+ : Intl.supportedValuesOf("currency")
58
+ .filter((c) => {
59
+ return !currencyBlacklist.includes(c);
60
+ })
61
+ .map((c) => {
62
+ return {
63
+ value: c,
64
+ symbol: getSymbolFromCurrency(c),
65
+ name: currencyName.of(c),
66
+ };
67
+ });
68
+
69
+ function setSelectedCurrency(c) {
70
+ let _clone = _.cloneDeep($internal) || {};
71
+ _.set(_clone, "currencycode", c?.value);
72
+ _.set(_clone, "currencysymbol", c?.symbol);
73
+ $internal = _clone;
74
+ show_currency_list = false;
75
+ }
76
+
77
+ $: onRawChange($internal?.raw);
78
+ function onRawChange(val) {
79
+ if (!$internal) return;
80
+ let _clone = _.cloneDeep($internal) || {};
81
+ _clone.cents = currency(val).intValue;
82
+ _clone.amount = currency(val).value;
83
+ if (!deepEqual(_clone, $internal)) $internal = _clone;
84
+ }
85
+
86
+ onMount(() => {
87
+ // set field values that aren't set yet
88
+ let f = _.cloneDeep($field);
89
+ let data_currencycode =
90
+ f?.data?.base_details?.currencycode &&
91
+ currencyList.find((c) => c.value === f?.data?.base_details?.currencycode)
92
+ ? f?.data?.base_details?.currencycode
93
+ : null;
94
+ let fixed_currencycode = currencyList.find((c) => c.value === currencyFixed)
95
+ ? currencyFixed
96
+ : null;
97
+ let currencycode = data_currencycode || fixed_currencycode || currencyList?.[0]?.value || "USD";
98
+ let currencysymbol = getSymbolFromCurrency(currencycode);
99
+ let cents = currency(f?.data?.base).intValue;
100
+ let amount = currency(f?.data?.base).value;
101
+ let initial_value = amount?.toFixed(2).toString() ?? "0.00";
102
+ let initial_data = {
103
+ ...f?.data,
104
+ base: initial_value,
105
+ };
106
+ if (!f?.spec?.without_value_details) {
107
+ initial_data.base_details = {
108
+ currencycode,
109
+ currencysymbol,
110
+ cents,
111
+ amount,
112
+ };
113
+ }
114
+
115
+ let initial_state_internal = {
116
+ ...f?.state?.internal,
117
+ currencycode,
118
+ currencysymbol,
119
+ cents,
120
+ amount,
121
+ raw: initial_value,
122
+ };
123
+
124
+ _.set(f, "data", initial_data);
125
+ _.set(f, "state.internal", initial_state_internal);
126
+ if (!deepEqual(f, $field)) $field = f;
127
+
128
+ syncStoreToStore(
129
+ field,
130
+ internal,
131
+ (a, b) => {
132
+ let _clone = _.cloneDeep(a.state?.internal) || {};
133
+
134
+ // get parts from data
135
+ _clone.raw = a?.data?.base;
136
+ if (!a?.spec?.without_value_details) {
137
+ _clone.cents = a?.data?.base_details?.cents;
138
+ _clone.amount = a?.data?.base_details?.amount;
139
+ _clone.currencycode = a?.data?.base_details?.currencycode;
140
+ _clone.currencysymbol = a?.data?.base_details?.currencysymbol;
141
+ }
142
+
143
+ // set field state if changed
144
+ if (!deepEqual(a?.state?.internal, _clone)) {
145
+ $field.state.internal = _clone;
146
+ }
147
+
148
+ return _clone;
149
+ },
150
+ (a, b) => {
151
+ let _clone = _.cloneDeep(a) || {};
152
+ // update the state
153
+ _.set(_clone, "state.internal", _.cloneDeep(b));
154
+ // update the data
155
+ _.set(_clone, "data.base", b?.raw);
156
+ if (!a?.spec?.without_value_details) {
157
+ _.set(_clone, "data.base_details.amount", b?.amount);
158
+ _.set(_clone, "data.base_details.cents", b?.cents);
159
+ _.set(_clone, "data.base_details.currencycode", b?.currencycode);
160
+ _.set(_clone, "data.base_details.currencysymbol", b?.currencysymbol);
161
+ }
162
+ return _clone;
163
+ },
164
+ null,
165
+ 300
166
+ );
167
+ });
168
+ </script>
169
+
170
+ {#if $internal}
171
+ <div
172
+ use:clickOutside={() => {
173
+ if (show_currency_list) {
174
+ show_currency_list = false;
175
+ }
176
+ }}
177
+ class="flex flex-col w-full text-surface-900"
178
+ >
179
+ <label for="input_{state_key}" class="block text-label {hide_label ? 'hidden' : ''}">
180
+ {label}
181
+ </label>
182
+ <div
183
+ class="flex relative mt-2 rounded-md border-0 ring-1 {!isValid
184
+ ? 'ring-danger-500'
185
+ : 'ring-surface-300 focus:ring-primary-400'} focus-within:ring-2"
186
+ >
187
+ <div class="pointer-events-none flex items-center pl-3">
188
+ <span class="text-label text-surface-600">
189
+ {getSymbolFromCurrency($internal?.currencycode)}
190
+ </span>
191
+ </div>
192
+ <input
193
+ on:keydown={(e) => {
194
+ if (e.key === "Enter") {
195
+ e.preventDefault();
196
+ }
197
+ }}
198
+ use:inputRegexMask={currencyRegex}
199
+ id="input_{state_key}"
200
+ placeholder={label}
201
+ class="block w-full rounded-md py-2 px-3 focus:outline-none text-surface-900 placeholder:text-surface-400"
202
+ name={state_key}
203
+ inputmode="decimal"
204
+ bind:value={$internal.raw}
205
+ on:focus={(e) => e.target.select()}
206
+ />
207
+ <!-- Here be selector stuff -->
208
+ {#if currencyList && !currencyFixed}
209
+ <button
210
+ type="button"
211
+ on:click={toggleCurrencySelectorPopout}
212
+ class="w-20 pl-3 space-x-2 flex items-center bg-white hover:bg-surface-50 focus:outline-none focus:bg-surface-100 rounded-r-md"
213
+ >
214
+ <span class="text-field text-surface-700">{$internal?.currencycode}</span>
215
+ <i
216
+ class="text-surface-700 fa fa-solid fa-xs fa-caret-{show_currency_list ? 'up' : 'down'}"
217
+ />
218
+ </button>
219
+ {/if}
220
+ {#if show_currency_list && currencyList}
221
+ <div class="z-10 absolute inset-y-[42px] right-0">
222
+ <ul
223
+ class="items block max-h-[250px] overflow-y-auto w-[250px] md:w-[450px] bg-white shadow-lg ring-1 ring-surface-300"
224
+ >
225
+ {#each currencyList as item}
226
+ <!-- {#if currencyWhitelist.contains(item.value) && !currencyBlacklist.contains(item.value)} -->
227
+ <li
228
+ class="{item.value == $internal?.currencycode
229
+ ? 'selecteditem'
230
+ : ''} group hover:bg-surface-100 focus-within:bg-surface-100 text-field cursor-default"
231
+ >
232
+ <button
233
+ on:click|preventDefault={() => setSelectedCurrency(item)}
234
+ on:focusout={(e) => handleFocusOut(e)}
235
+ class="px-4 py-1 w-full flex items-center space-x-3 selectitem focus:outline-none"
236
+ >
237
+ <span class="text-surface-900 text-field">{item.value}</span>
238
+ <span class="text-surface-700 text-field">{item.symbol}</span>
239
+ <span class="text-surface-500 text-field">{item.name}</span>
240
+ {#if item.value == $internal?.currencycode}
241
+ <i class="text-primary-400 ml-auto fa-regular fa-check" />
242
+ {/if}
243
+ </button>
244
+ </li>
245
+ <!-- {/if} -->
246
+ {/each}
247
+ </ul>
248
+ </div>
249
+ {/if}
250
+ <!-- Here end selector stuff -->
251
+ </div>
252
+ {#if validationMessage}
253
+ <p class="text-label {!isValid ? `text-danger-500` : `text-success-500`}">
254
+ {validationMessage}
255
+ </p>
256
+ {/if}
257
+ </div>
258
+ {/if}
@@ -0,0 +1,23 @@
1
+ /** @typedef {typeof __propDef.props} CurrencyProps */
2
+ /** @typedef {typeof __propDef.events} CurrencyEvents */
3
+ /** @typedef {typeof __propDef.slots} CurrencySlots */
4
+ export default class Currency extends SvelteComponentTyped<{
5
+ field: any;
6
+ }, {
7
+ [evt: string]: CustomEvent<any>;
8
+ }, {}> {
9
+ }
10
+ export type CurrencyProps = typeof __propDef.props;
11
+ export type CurrencyEvents = typeof __propDef.events;
12
+ export type CurrencySlots = typeof __propDef.slots;
13
+ import { SvelteComponentTyped } from "svelte";
14
+ declare const __propDef: {
15
+ props: {
16
+ field: any;
17
+ };
18
+ events: {
19
+ [evt: string]: CustomEvent<any>;
20
+ };
21
+ slots: {};
22
+ };
23
+ export {};
@@ -0,0 +1,35 @@
1
+ <script>
2
+ import _ from "lodash-es";
3
+
4
+ export let field;
5
+
6
+ $: state_key = $field.state?.state_key;
7
+ $: label = $field.spec?.title;
8
+ $: hide_label = $field.spec?.hide_label;
9
+ $: isValid = !$field.state?.validation || $field.state?.validation?.valid;
10
+ $: validationMessage = $field.state?.validation?.message;
11
+ $: value_is_string = _.isString($field.data?.base);
12
+ $: value = !value_is_string ? JSON.stringify($field.data?.base, null, 2) : $field.data?.base;
13
+ </script>
14
+
15
+ <div class="flex flex-col w-full text-surface-900">
16
+ <label for="input_{state_key}" class="block text-label {hide_label ? 'hidden' : ''}">
17
+ {label}
18
+ </label>
19
+ <div class="relative mt-2 rounded-md">
20
+ <div
21
+ class="block bg-surface-50 w-full text-field rounded-md border-0 py-2 pl-3 text-surface-900 ring-1 ring-inset ring-surface-100 focus:ring-primary-400 focus:outline-none placeholder:text-surface-400 focus:ring-2 focus:ring-inset"
22
+ >
23
+ {#if value_is_string}
24
+ {value}
25
+ {:else}
26
+ <pre>{value}</pre>
27
+ {/if}
28
+ </div>
29
+ </div>
30
+ {#if validationMessage}
31
+ <p class="text-label {!isValid ? `text-danger-500` : `text-success-500`}">
32
+ {validationMessage}
33
+ </p>
34
+ {/if}
35
+ </div>