minsky-webform-formkit 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 (35) hide show
  1. package/.pnp.cjs +11555 -0
  2. package/.pnp.loader.mjs +2126 -0
  3. package/LICENSE +21 -0
  4. package/README.md +324 -0
  5. package/dist/index.css +1 -0
  6. package/dist/index.js +1 -0
  7. package/package.json +55 -0
  8. package/src/components/FormKit/FormKitPhoneEnhanced/FormKitPhoneEnhanced.js +100 -0
  9. package/src/components/FormKit/FormKitPhoneEnhanced/FormKitPhoneEnhanced.vue +30 -0
  10. package/src/components/FormKit/FormKitPhoneEnhanced/countryCodes.js +1214 -0
  11. package/src/components/FormKit/FormKitRecaptcha/FormKitRecaptcha.vue +46 -0
  12. package/src/components/Icon.vue +38 -0
  13. package/src/components/InfoTooltip.vue +16 -0
  14. package/src/components/MinksyWebformFormKit/MinskyWebformFormKit.vue +357 -0
  15. package/src/components/MinksyWebformFormKit/_MinskyWebformFormKit.js +282 -0
  16. package/src/composables/useExample.js +0 -0
  17. package/src/formkit.config.js +72 -0
  18. package/src/index.mjs +20 -0
  19. package/src/plugins/customLabelPlugin.js +42 -0
  20. package/src/plugins/htmlHelpPlugin.js +23 -0
  21. package/src/rules/iban.js +9 -0
  22. package/src/rules/insz.js +21 -0
  23. package/src/rules/phone.js +29 -0
  24. package/src/rules/time.js +79 -0
  25. package/src/rules/vat.js +42 -0
  26. package/src/styles/_functions.scss +96 -0
  27. package/src/styles/_mixins.scss +29 -0
  28. package/src/styles/_variables.scss +7 -0
  29. package/src/styles/main.scss +11 -0
  30. package/src/styles/webform-formkit-multistep.scss +98 -0
  31. package/src/styles/webform-formkit.scss +215 -0
  32. package/src/styles/webform.scss +400 -0
  33. package/src/utils/functions.js +78 -0
  34. package/src/utils/lodash.js +208 -0
  35. package/src/utils/messages.js +37 -0
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <div class="formkit-recaptcha">
3
+ <div class="g-recaptcha" ref="recaptchaRef" data-theme="light" data-type="image"></div>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { ref, onMounted } from 'vue';
9
+
10
+ /* ------------------------------------------------------------------
11
+ * Props / Emits
12
+ * ------------------------------------------------------------------ */
13
+ const props = defineProps({
14
+ context: Object,
15
+ });
16
+
17
+ /* ------------------------------------------------------------------
18
+ * Template refs
19
+ * ------------------------------------------------------------------ */
20
+ const recaptchaRef = ref(null);
21
+
22
+ /* ========================================================================= */
23
+ /* Methods */
24
+ /* ========================================================================= */
25
+ function onSubmit(e) {
26
+ props.context.node.input({
27
+ captcha_sid: props.context.attrs.sid,
28
+ captcha_token: props.context.attrs.token,
29
+ captcha_response: '',
30
+ 'g-recaptcha-response': e,
31
+ captcha_cacheable: '1',
32
+ });
33
+ }
34
+
35
+ /* ------------------------------------------------------------------
36
+ * Lifecycle
37
+ * ------------------------------------------------------------------ */
38
+ onMounted(() => {
39
+ grecaptcha.ready(() => {
40
+ grecaptcha.render(recaptchaRef.value, {
41
+ sitekey: props.context.attrs.siteKey,
42
+ callback: onSubmit,
43
+ });
44
+ });
45
+ });
46
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <span class="icon" :class="[name !== '' ? 'icon--' + name : '', modifiers.length ? 'icon--' + modifiers.join(' icon--') : '']">
3
+ <svg v-if="loading" role="img" id="L9" version="1.1" class="icon-svg icon-svg--loader" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" 1 enable-background="new 0 0 0 0" xml:space="preserve">
4
+ <title>Loading</title>
5
+ <path fill="currentColor" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50">
6
+ <animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="1s" from="0 50 50" to="360 50 50" repeatCount="indefinite" />
7
+ </path>
8
+ </svg>
9
+
10
+ <svg v-else role="img" class="icon__svg" :aria-hidden="!ariaTitle">
11
+ <title v-if="ariaTitle">{{ ariaTitle }}</title>
12
+ <use v-if="!!name" :xlink:href="'#sprite-' + name" />
13
+ </svg>
14
+ </span>
15
+ </template>
16
+
17
+ <script setup>
18
+ const props = defineProps({
19
+ name: {
20
+ type: String,
21
+ default: '',
22
+ },
23
+
24
+ loading: {
25
+ type: Boolean,
26
+ default: false,
27
+ },
28
+
29
+ modifiers: {
30
+ type: Array,
31
+ default: () => [],
32
+ },
33
+
34
+ ariaTitle: {
35
+ type: String,
36
+ },
37
+ });
38
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <span class="info-tooltip">
3
+ <Tooltip :triggers="['click', 'hover']">
4
+ <Icon name="info" />
5
+ <template #popper>
6
+ <div v-html="props.content" />
7
+ </template>
8
+ </Tooltip>
9
+ </span>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { Tooltip } from 'floating-vue';
14
+
15
+ const props = defineProps(['content']);
16
+ </script>
@@ -0,0 +1,357 @@
1
+ <template>
2
+ <div ref="formRef" class="webform-formkit" :class="[{ 'webform-formkit--loading': submissionLoading || loading }, `webform-formkit--${props.id}`, props.classModifier ? `webform-formkit--${props.classModifier}` : null]">
3
+ <div v-if="loading && !success && !showErrorMessage" class="webform-formkit__placeholder">
4
+ <slot name="placeholder" />
5
+ </div>
6
+ <div v-if="!loading && !success && !showErrorMessage">
7
+ <FormKitSchema :schema="schema" :data="formData" :library="props.cmpLibrary" />
8
+ <slot name="afterSchema" />
9
+ </div>
10
+ <div v-if="loading || submissionLoading" class="webform-formkit__loader">
11
+ <Icon :loading="true" />
12
+ <p v-if="loadingMessage" class="webform-formkit__message">
13
+ {{ loadingMessage }}
14
+ </p>
15
+ </div>
16
+ <div v-if="success" class="webform-formkit__success">
17
+ <h5 v-if="successTitle" class="webform-formkit__succes-title">
18
+ {{ successTitle }}
19
+ </h5>
20
+ <div v-if="successMessage" class="webform-formkit__message" v-html="successMessage"></div>
21
+ </div>
22
+ <ul v-if="showErrorMessage" class="webform-formkit__errors">
23
+ <li v-for="error in errorMessages" v-html="error.message"></li>
24
+ </ul>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
30
+ import { _keys, _pick, _merge, _defaultsDeep } from '../../utils/lodash';
31
+ import gsap from 'gsap';
32
+ import ScrollToPlugin from 'gsap/ScrollToPlugin';
33
+ import { flattenObject, hasValue } from '../../utils/functions';
34
+ import defaultMessages from '../../utils/messages';
35
+
36
+ gsap.registerPlugin(ScrollToPlugin);
37
+ const { lang } = document.documentElement;
38
+ /* ------------------------------------------------------------------
39
+ * Props / Emits
40
+ * ------------------------------------------------------------------ */
41
+ const props = defineProps({
42
+ id: {
43
+ type: String,
44
+ required: true,
45
+ },
46
+ api: {
47
+ type: String,
48
+ default: window.location.origin,
49
+ },
50
+ classModifier: String,
51
+ defaults: Object,
52
+ schemeProcessor: Function,
53
+ formDataAddons: Object,
54
+ submitProcessor: Function,
55
+ forceRedirect: {
56
+ type: Object,
57
+ default: () => ({ bool: false }),
58
+ },
59
+ cmpLibrary: {
60
+ type: Object,
61
+ default: () => ({}),
62
+ },
63
+ messages: {
64
+ type: Object,
65
+ default: () => {},
66
+ },
67
+ setUserData: Function,
68
+ });
69
+
70
+ const emit = defineEmits(['success']);
71
+
72
+ /* ------------------------------------------------------------------
73
+ * Template refs
74
+ * ------------------------------------------------------------------ */
75
+ const formRef = ref(null);
76
+
77
+ /* ------------------------------------------------------------------
78
+ * State
79
+ * ------------------------------------------------------------------ */
80
+ const schema = ref(null);
81
+ const schemaDefaults = ref({});
82
+ const msgs = ref({});
83
+ const loading = ref(true);
84
+ const success = ref(false);
85
+ const submissionLoading = ref(false);
86
+ const loadingMessage = ref(null);
87
+ const successMessage = ref(null);
88
+ const successTitle = ref(null);
89
+ const errorMessages = ref(null);
90
+ const showErrorMessage = ref(false);
91
+
92
+ const formData = reactive({
93
+ values: {},
94
+ // helpers: window.WebformHeadless.helpers,
95
+ });
96
+
97
+ /* ------------------------------------------------------------------
98
+ * Lifecycle
99
+ * ------------------------------------------------------------------ */
100
+ onMounted(() => {
101
+ // we merge messages provided by the user with default messages so there will alway be feedback when the form is submitted or missing
102
+ msgs.value = Object.assign(defaultMessages, props.messages);
103
+
104
+ formData.submit = async (data) => {
105
+ // remove vue values so they don't f*ck up the submit in drupal
106
+ delete data.form_id;
107
+ delete data.vApp;
108
+ delete data.webformId;
109
+
110
+ // check if data has multi step values + extract them
111
+ _keys(data).forEach((key) => {
112
+ if (key.includes('captcha')) {
113
+ Object.assign(data, data[key]);
114
+ delete data[key];
115
+ }
116
+ });
117
+
118
+ // we flatten all the data to a one level object so drupal can handle the data
119
+ data = flattenObject(data);
120
+ submitForm(data);
121
+ };
122
+
123
+ formData.submitInvalid = () => {
124
+ gsap.to(window, {
125
+ duration: 0.5,
126
+ scrollTo: { y: formRef.value, offsetY: 300 },
127
+ ease: 'power2',
128
+ });
129
+ };
130
+
131
+ fetchForm();
132
+
133
+ if (props.formDataAddons) {
134
+ Object.assign(formData, props.formDataAddons);
135
+ }
136
+ });
137
+
138
+ /* ------------------------------------------------------------------
139
+ * Computed
140
+ * ------------------------------------------------------------------ */
141
+ const formKeys = computed(() => _keys(formData.values));
142
+
143
+ /* ------------------------------------------------------------------
144
+ * Methods
145
+ * ------------------------------------------------------------------ */
146
+ async function fetchForm() {
147
+ const params = new URLSearchParams(window.location.search);
148
+ params.append('schema', 'form_kit');
149
+
150
+ try {
151
+ const response = await fetch(`${props.api}/webform/${props.id}/json/schema?${params.toString()}`);
152
+
153
+ // Always read body first
154
+ const data = await response.json();
155
+
156
+ if (!response.ok) {
157
+ // Try to extract a meaningful backend error
158
+ throw new Error(data.errors ? data.errors.map((err) => err.message).join(', ') : 'Unknown error');
159
+ }
160
+
161
+ if (data.schema) {
162
+ schema.value = addSlots(data.schema); // TODO
163
+ if (props.schemeProcessor) schema.value = props.schemeProcessor(data.schema);
164
+ schemaDefaults.value = data.values;
165
+ } else {
166
+ showErrorMessage.value = true;
167
+ errorMessages.value = [{ message: msgs.value[lang].messages.missing }];
168
+ }
169
+ } catch (error) {
170
+ showErrorMessage.value = true;
171
+
172
+ // show extended error only for user roles that can adjust webforms
173
+ // user roles needs to be populated with a drupal hook (check README.md)
174
+ if (window.drupalSettings) {
175
+ const userRoles = window.drupalSettings.user.roles || [];
176
+ if (userRoles.some((role) => ['administrator', 'editor'].includes(role))) {
177
+ errorMessages.value = [
178
+ { message: error },
179
+ {
180
+ message: msgs.value.global.error,
181
+ },
182
+ ];
183
+ } else {
184
+ errorMessages.value = [{ message: msgs.value[lang].messages.error }];
185
+ }
186
+ } else {
187
+ errorMessages.value = [{ message: msgs.value[lang].messages.error }];
188
+ }
189
+
190
+ console.error(`Error fetching schema of form with ID ${props.id}:`, error);
191
+ } finally {
192
+ loading.value = false;
193
+ }
194
+ }
195
+
196
+ function setDefaults() {
197
+ // set default values from schema
198
+ // use defaultsDeep to fill in the default value only 1 time AMNESTY-815
199
+ _defaultsDeep(formData.values, _pick(schemaDefaults.value, formKeys.value));
200
+
201
+ // set default values if any, like hidden fields and stuff (only those whose are available on form)
202
+ if (props.defaults) {
203
+ _merge(formData.values, _pick(props.defaults, formKeys.value));
204
+ }
205
+ }
206
+
207
+ async function submitForm(data) {
208
+ if (props.submitProcessor) {
209
+ data = props.submitProcessor(data);
210
+ }
211
+
212
+ submissionLoading.value = true;
213
+
214
+ // disable submit button
215
+ const submitButton = document.querySelector('.formkit-input[type=submit]');
216
+ if (submitButton) {
217
+ submitButton.disabled = true;
218
+ }
219
+
220
+ const params = new URLSearchParams(window.location.search);
221
+ params.append('schema', 'form_kit');
222
+
223
+ const fd = new FormData();
224
+ // convert data to formdata
225
+ for (const key in data) {
226
+ if (hasValue(data[key])) {
227
+ if (Array.isArray(data[key])) {
228
+ if (data[key].length) {
229
+ data[key].forEach((val) => {
230
+ if (this.$formkit.get(`${this.id}--${key}`).context.type === 'imageUpload') {
231
+ if (typeof val === 'object') {
232
+ formData.append(`${key}[]`, _values(val)[0]);
233
+ } else {
234
+ formData.append(`${key}[]`, val);
235
+ }
236
+ } else {
237
+ fd.append(`${key}[]`, val);
238
+ }
239
+ });
240
+ } else {
241
+ // if array is empty, sent empty string with FormData. Fix for AMNESTY-663
242
+ fd.append(key, '');
243
+ }
244
+ } else if (typeof data[key] === 'string') {
245
+ if (data[key].indexOf('json_value') === 2) {
246
+ const values = JSON.parse(data[key]).json_value;
247
+ _keys(values).forEach((subkey) => {
248
+ fd.append(`${key}[${subkey}]`, values[subkey]);
249
+ });
250
+ } else {
251
+ fd.append(key, data[key]);
252
+ }
253
+ } else {
254
+ fd.append(key, data[key]);
255
+ }
256
+ }
257
+ }
258
+
259
+ fetch(`${props.api}/webform/${props.id}/json/submission?${params.toString()}`, {
260
+ method: 'POST',
261
+ body: fd,
262
+ query: window.location.search,
263
+ })
264
+ .then((r) => {
265
+ if (r.status === 200) {
266
+ r.json().then((response) => {
267
+ handleSuccess(response);
268
+
269
+ // store user data in localstorage
270
+ if (props.setUserData) {
271
+ props.setUserData(data);
272
+ }
273
+ });
274
+ } else {
275
+ r.json().then((response) => {
276
+ showErrorMessage.value = true;
277
+ submissionLoading.value = false;
278
+ errorMessages.value = response.errors;
279
+ });
280
+ }
281
+ })
282
+ .catch(() => {
283
+ showErrorMessage.value = true;
284
+ submissionLoading.value = false;
285
+ });
286
+ }
287
+
288
+ function handleSuccess(response) {
289
+ if (!props.forceRedirect.bool) {
290
+ switch (response.confirmation.type) {
291
+ case 'inline':
292
+ successTitle.value = response.confirmation.title;
293
+ successMessage.value = response.confirmation.message;
294
+ success.value = true;
295
+
296
+ gsap.to(window, {
297
+ duration: 0.5,
298
+ scrollTo: { y: formRef.value, offsetY: 200 },
299
+ ease: 'power2',
300
+ });
301
+
302
+ submissionLoading.value = false;
303
+ break;
304
+
305
+ case 'url':
306
+ window.location = response.confirmation.url;
307
+ break;
308
+
309
+ default:
310
+ successTitle.value = msgs.value[lang].success.title;
311
+ successMessage.value = msgs.value[lang].success.message;
312
+ success.value = true;
313
+ submissionLoading.value = false;
314
+ }
315
+ } else {
316
+ window.location = props.forceRedirect.url;
317
+ }
318
+
319
+ emit('success');
320
+ }
321
+ /* ------------------------------------------------------------------
322
+ * Watchers
323
+ * ------------------------------------------------------------------ */
324
+ watch(formKeys, async () => {
325
+ setDefaults();
326
+
327
+ await nextTick();
328
+
329
+ const $form = formRef.value?.querySelector('form');
330
+ if ($form) {
331
+ $form.setAttribute('novalidate', true); // remove native js form validation because of time input error AMNESTY-785
332
+ }
333
+ });
334
+
335
+ /* ------------------------------------------------------------------
336
+ * Helpers (unchanged)
337
+ * ------------------------------------------------------------------ */
338
+ function addSlots(schema) {
339
+ // if (schema[0].children) {
340
+ // // check if actions element is present to add slot
341
+ // const actions = schema[0].children.filter((cmp) => cmp.name === 'actions');
342
+ // if (actions[0]) {
343
+ // const index = schema[0].children.indexOf(actions[0]);
344
+ // schema[0].children.splice(index, 0, {
345
+ // $el: 'div',
346
+ // name: 'before-actions-slot',
347
+ // attrs: {
348
+ // class: 'formkit-before-actions',
349
+ // },
350
+ // children: '$slots.beforeActions',
351
+ // });
352
+ // }
353
+ // }
354
+
355
+ return schema;
356
+ }
357
+ </script>