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,282 @@
1
+ import _keys from 'lodash/keys';
2
+ import _values from 'lodash/values';
3
+ import gsap from 'gsap';
4
+ import ScrollToPlugin from 'gsap/ScrollToPlugin';
5
+ import defaultMessages from '../../utils/messages';
6
+
7
+ gsap.registerPlugin(ScrollToPlugin);
8
+
9
+
10
+ export default {
11
+ props: {
12
+ id: {
13
+ type: String,
14
+ required: true,
15
+ },
16
+ api: {
17
+ type: String,
18
+ default: window.location.origin,
19
+ },
20
+ defaults: {
21
+ type: Object,
22
+ },
23
+ schemeProcessor: {
24
+ type: Function,
25
+ },
26
+ formDataAddons: {
27
+ type: Object,
28
+ },
29
+ cmpLibrary: {
30
+ type: Object,
31
+ default: () => { },
32
+ },
33
+ forceRedirect: {
34
+ type: Object,
35
+ default: {
36
+ bool: false,
37
+ },
38
+ },
39
+ messages: {
40
+ type: Object,
41
+ default: () => { }
42
+ }
43
+ },
44
+
45
+ components: {
46
+
47
+ },
48
+
49
+ mounted() {
50
+ //we merge messages provided by the user with default messages so there will alway be feedback when the form is submitted or missing
51
+ this.msgs = Object.assign(defaultMessages, this.messages);
52
+ console.log('BOEM');
53
+
54
+ this.formData.submit = async (data) => {
55
+ //we remove vue values so they don't f*ck up the submit in drupal
56
+ delete data.form_id;
57
+ delete data.vApp;
58
+ delete data.webformId;
59
+ //we flatten all the data to a one level object so drupal can handle the data
60
+ data = flattenObject(data);
61
+
62
+ this.submitForm(data);
63
+ };
64
+
65
+ // fetch the form
66
+ this.fetchForm();
67
+
68
+ if (this.formDataAddons) {
69
+ this.formData = Object.assign(this.formData, this.formDataAddons);
70
+ }
71
+ },
72
+
73
+ data() {
74
+ return {
75
+ otherSchema: [],
76
+ schema: null,
77
+ schemaDefaults: {},
78
+ settings: null,
79
+ loading: true,
80
+ success: false,
81
+ submissionLoading: false,
82
+ loadingMessage: null,
83
+ successMessage: null,
84
+ successTitle: null,
85
+ errorMessages: null,
86
+ showErrorMessage: false,
87
+ multiStep: null,
88
+ msgs: {},
89
+ formData: {
90
+ values: {},
91
+ },
92
+ };
93
+ },
94
+
95
+ computed: {
96
+ formKeys: {
97
+ get() {
98
+ return _keys(this.formData.values);
99
+ },
100
+ },
101
+ },
102
+
103
+ methods: {
104
+ async fetchForm() {
105
+ // add page params
106
+ const params = new URLSearchParams(window.location.search);
107
+ params.append('schema', 'form_kit');
108
+
109
+ fetch(`${this.api}/webform/${this.id}/json/schema?${params.toString()}`)
110
+ .then((r) => r.json())
111
+ .then((result) => {
112
+ if (result.schema) {
113
+ this.schema = this.schemeProcessor ? this.schemeProcessor(result.schema) : result.schema;
114
+ this.schemaDefaults = result.values;
115
+ } else {
116
+ this.errorMessage = this.msgs.missing;
117
+ this.showErrorMessage = true;
118
+ }
119
+
120
+ this.loading = false;
121
+ });
122
+ },
123
+
124
+ setDefaults() {
125
+ _keys(this.defaults).forEach((key) => {
126
+ if (this.$formkit.get(`${this.id}--${key}`)) {
127
+ this.$formkit.get(`${this.id}--${key}`).context.node.input(this.defaults[key]);
128
+ }
129
+ });
130
+
131
+ _keys(this.schemaDefaults).forEach((key) => {
132
+ if (this.$formkit.get(`${this.id}--${key}`)) {
133
+ if (this.schemaDefaults[key].fids) return;
134
+ this.$formkit.get(`${this.id}--${key}`).context.node.input(this.schemaDefaults[key]);
135
+ }
136
+ });
137
+ },
138
+
139
+ async submitForm(data) {
140
+ /* eslint-disable */
141
+ this.submissionLoading = true;
142
+
143
+ // disable submit button
144
+ const submitButton = document.querySelector('.formkit-input[type=submit]');
145
+ if (submitButton) {
146
+ submitButton.disabled = true;
147
+ }
148
+
149
+ // add page params
150
+ const params = new URLSearchParams(window.location.search);
151
+ params.append('schema', 'form_kit');
152
+
153
+ // convert data to formdata
154
+ const formData = new FormData();
155
+ for (const key in data) {
156
+ // only add to formdata if has value
157
+ if (hasValue(data[key])) {
158
+ if (Array.isArray(data[key])) {
159
+ data[key].forEach((val) => {
160
+ if (this.$formkit.get(`${this.id}--${key}`).context.type === 'imageUpload') {
161
+ if (typeof val === 'object') {
162
+ formData.append(`${key}[]`, _values(val)[0]);
163
+ } else {
164
+ formData.append(`${key}[]`, val);
165
+ }
166
+ }
167
+ });
168
+ } else if (typeof data[key] === 'string') {
169
+ if (data[key].indexOf('json_value') === 2) {
170
+ const values = JSON.parse(data[key]).json_value;
171
+ _keys(values).forEach(subkey => {
172
+ formData.append(`${key}[${subkey}]`, values[subkey]);
173
+ })
174
+ } else {
175
+ formData.append(key, data[key]);
176
+ }
177
+ }
178
+ else {
179
+ formData.append(key, data[key]);
180
+ }
181
+ }
182
+ }
183
+ /* eslint-enable */
184
+ fetch(`${this.api}/webform/${this.id}/json/submission?${params.toString()}`, {
185
+ method: 'POST',
186
+ body: formData,
187
+ query: window.location.search,
188
+ })
189
+ .then((r) => {
190
+ if (r.status === 200) {
191
+ r.json().then((response) => {
192
+ this.handleSuccess(response);
193
+ });
194
+ } else {
195
+ r.json().then((response) => {
196
+ this.showErrorMessage = true;
197
+ this.submissionLoading = false;
198
+ this.errorMessages = response.errors;
199
+ });
200
+ }
201
+ })
202
+ .catch(() => {
203
+ this.showErrorMessage = true;
204
+ this.submissionLoading = false;
205
+ });
206
+ },
207
+
208
+ handleSuccess(response) {
209
+ if (!this.forceRedirect.bool) {
210
+ switch (response.confirmation.type) {
211
+ case 'inline':
212
+ this.successTitle = response.confirmation.title;
213
+ this.successMessage = response.confirmation.message;
214
+ this.success = true;
215
+ // scroll to success
216
+ gsap.to(window, {
217
+ duration: 0.5,
218
+ scrollTo: { y: this.$refs.form, offsetY: 200 },
219
+ ease: 'power2',
220
+ });
221
+ // remove toggle
222
+ this.submissionLoading = false;
223
+ break;
224
+ case 'url':
225
+ window.location = response.confirmation.url;
226
+ break;
227
+ default:
228
+ this.successTitle = this.msgs.success.title;
229
+ this.successMessage = this.msgs.success.description;
230
+ this.success = true;
231
+ // remove toggle
232
+ this.submissionLoading = false;
233
+ break;
234
+ }
235
+ } else {
236
+ window.location = this.forceRedirect.url;
237
+ }
238
+ },
239
+ },
240
+
241
+ watch: {
242
+ formKeys() {
243
+ this.setDefaults();
244
+
245
+ const $form = this.$refs.form.querySelector('form');
246
+ if ($form) $form.setAttribute('novalidate', true); // remove native js form validation because of time input error AMNESTY-785
247
+ },
248
+ },
249
+ };
250
+
251
+ function flattenObject(obj) {
252
+ const flattened = {};
253
+
254
+ function flatten(_obj, parentKey = '') {
255
+ for (const key in _obj) {
256
+ if (Array.isArray(_obj[key])) {
257
+ flattened[key] = _obj[key];
258
+ } else if (typeof _obj[key] === 'object' && _obj[key] !== null) {
259
+ flatten(_obj[key], `${parentKey + key}.`);
260
+ } else {
261
+ flattened[key] = _obj[key];
262
+ }
263
+ }
264
+ }
265
+
266
+ flatten(obj);
267
+
268
+ return flattened;
269
+ }
270
+
271
+ function hasValue(val) {
272
+ let _hasValue = false;
273
+ if (val || val === 0) {
274
+ _hasValue = true;
275
+ }
276
+
277
+ if (Array.isArray(val)) {
278
+ _hasValue = Boolean(val.length);
279
+ }
280
+
281
+ return _hasValue;
282
+ }
File without changes
@@ -0,0 +1,72 @@
1
+ import { nl, en, fr } from '@formkit/i18n';
2
+ import { createMultiStepPlugin } from '@formkit/addons';
3
+ import { createInput } from '@formkit/vue';
4
+
5
+ // import plugins
6
+ import customLabelPlugin from './plugins/customLabelPlugin';
7
+ import htmlHelpPlugin from './plugins/htmlHelpPlugin';
8
+
9
+ // import custom components
10
+ import FormKitPhoneEnhanced from './components/FormKit/FormKitPhoneEnhanced/FormKitPhoneEnhanced.vue';
11
+ import FormKitRecaptcha from './components/FormKit/FormKitRecaptcha/FormKitRecaptcha.vue';
12
+
13
+ // import rules
14
+ import { internationalPhone, phone } from './rules/phone';
15
+ import iban from './rules/iban';
16
+ import vat from './rules/vat';
17
+ import insz from './rules/insz';
18
+ import { timeRule, timeMessage } from './rules/time';
19
+
20
+ // config
21
+ const config = {
22
+ plugins: [
23
+ customLabelPlugin,
24
+ htmlHelpPlugin,
25
+ createMultiStepPlugin({ allowIncomplete: false }),
26
+ ],
27
+ rules: {
28
+ internationalPhone,
29
+ phone,
30
+ iban,
31
+ vat,
32
+ insz,
33
+ timeRule,
34
+ },
35
+ messages: {
36
+ nl: {
37
+ validation: {
38
+ phone: 'Dit is geen geldig telefoonnummer.',
39
+ required: 'Dit veld is verplicht.',
40
+ vat: 'Dit is geen geldig Btw-nummer.',
41
+ is: 'Dit veld is verplicht.',
42
+ timeRule({ node }) {
43
+ return timeMessage(node);
44
+ },
45
+ },
46
+ },
47
+ en: {
48
+ validation: {
49
+ phone: 'This is not a valid phone number.',
50
+ required: 'This field is required.',
51
+ is: 'This field is required.',
52
+ vat: 'This is not a valid VAT number.',
53
+ },
54
+ },
55
+ fr: {
56
+ validation: {
57
+ phone: "Ce numΓ©ro de tΓ©lΓ©phone n'est pas valide.",
58
+ required: 'Ce champ est obligatoire.',
59
+ is: 'Ce champ est obligatoire.',
60
+ vat: "Ce numΓ©ro de TVA n'est pas valide.",
61
+ },
62
+ },
63
+ },
64
+ locales: { nl, en, fr },
65
+ locale: document.documentElement.lang,
66
+ inputs: {
67
+ phoneEnhanced: createInput(FormKitPhoneEnhanced),
68
+ recaptcha: createInput(FormKitRecaptcha),
69
+ },
70
+ };
71
+
72
+ export default config;
package/src/index.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import MinskyWebformFormKit from './components/MinksyWebformFormKit/MinskyWebformFormKit.vue';
2
+ import { plugin, defaultConfig, FormKitMessages } from '@formkit/vue';
3
+ import Icon from './components/Icon.vue';
4
+ import InfoTooltip from './components/InfoTooltip.vue';
5
+ import formkitConfig from './formkit.config.js';
6
+ import { _merge } from './utils/lodash.js';
7
+ import './styles/main.scss';
8
+
9
+ export { MinskyWebformFormKit };
10
+
11
+ export default {
12
+ install(app, userConfig) {
13
+ const mergedConfig = _merge({}, formkitConfig, userConfig);
14
+ app.component('MinskyWebformFormKit', MinskyWebformFormKit);
15
+ app.use(plugin, defaultConfig(mergedConfig));
16
+ app.component('Icon', Icon);
17
+ app.component('InfoTooltip', InfoTooltip);
18
+ app.component('FormKitMessages', FormKitMessages);
19
+ },
20
+ };
@@ -0,0 +1,42 @@
1
+ const legends = ['checkbox_multi', 'radio_multi', 'repeater', 'transferlist'];
2
+
3
+ function customLabelPlugin(node) {
4
+ if (['button', 'submit', 'hidden', 'group', 'list', 'meta'].includes(node.props.type)) return;
5
+
6
+ node.on('created', () => {
7
+ const legendOrLabel = legends.includes(`${node.props.type}${node.props.options ? '_multi' : ''}`) ? 'legend' : 'label';
8
+
9
+ if (node.props.definition.schemaMemoKey) {
10
+ node.props.definition.schemaMemoKey += `${node.props.options ? '_multi' : ''}_add_asterisk`;
11
+ }
12
+
13
+ const schemaFn = node.props.definition.schema;
14
+
15
+ node.props.definition.schema = (sectionsSchema = {}) => {
16
+ sectionsSchema[legendOrLabel] = {
17
+ children: [
18
+ '$label',
19
+ {
20
+ $cmp: 'InfoTooltip',
21
+ if: '$attrs.tooltip',
22
+ props: {
23
+ content: '$attrs.tooltip',
24
+ },
25
+ },
26
+ {
27
+ $el: 'span',
28
+ if: '$state.required',
29
+ attrs: {
30
+ class: 'formkit-asterisk',
31
+ },
32
+ children: ['*'],
33
+ },
34
+ ],
35
+ };
36
+
37
+ return schemaFn(sectionsSchema);
38
+ };
39
+ });
40
+ }
41
+
42
+ export default customLabelPlugin;
@@ -0,0 +1,23 @@
1
+ function htmlHelpPlugin(node) {
2
+ if (['button', 'submit', 'hidden', 'group', 'list', 'meta'].includes(node.props.type)) return;
3
+
4
+ node.on('created', () => {
5
+ const schemaFn = node.props.definition.schema;
6
+
7
+ node.props.definition.schema = (sectionsSchema = {}) => {
8
+ sectionsSchema.help = {
9
+ children: [
10
+ {
11
+ $el: 'div',
12
+ attrs: {
13
+ innerHTML: '$help',
14
+ },
15
+ },
16
+ ],
17
+ };
18
+
19
+ return schemaFn(sectionsSchema);
20
+ };
21
+ });
22
+ }
23
+ export default htmlHelpPlugin;
@@ -0,0 +1,9 @@
1
+ import { IBAN } from 'ibankit';
2
+ /**
3
+ * A contrived validation rule that ensures the input’s value is a valid iban number.
4
+ */
5
+ const iban = function (node) {
6
+ return IBAN.isValid(node.value);
7
+ };
8
+
9
+ export default iban;
@@ -0,0 +1,21 @@
1
+ const insz = function IsRRNoValid(node) {
2
+ // keep only digits
3
+ const n = node.value.replace(/[^0-9]/g, '');
4
+
5
+ // RR numbers need to be 11 chars long
6
+ if (n.length != 11) return false;
7
+
8
+ const checkDigit = n.substr(n.length - 2, 2);
9
+ const modFunction = function(nr) { return 97 - (nr % 97); };
10
+ let nrToCheck = parseInt(n.substr(0, 9));
11
+
12
+ // first check without 2
13
+ if (modFunction(nrToCheck) == checkDigit) return true;
14
+
15
+ // then check with 2 appended for y2k+ births
16
+ nrToCheck = parseInt('2' + n.substr(0, 9));
17
+
18
+ return (modFunction(nrToCheck) == checkDigit);
19
+ }
20
+
21
+ export default insz;
@@ -0,0 +1,29 @@
1
+ import { parsePhoneNumber } from 'libphonenumber-js';
2
+ /**
3
+ * A contrived validation rule that ensures the input’s value is a valid phone number.
4
+ */
5
+ export const internationalPhone = function (node) {
6
+ if (node.value !== '') {
7
+ try {
8
+ return parsePhoneNumber(node.value).isValid();
9
+ } catch (error) {
10
+ return false;
11
+ }
12
+ } else {
13
+ return true;
14
+ }
15
+ };
16
+
17
+ export const phone = function (node) {
18
+ if (node.value !== '') {
19
+ try {
20
+ return parsePhoneNumber(node.value, 'BE').isValid();
21
+ } catch (error) {
22
+ return false;
23
+ }
24
+ } else {
25
+ return true;
26
+ }
27
+ };
28
+
29
+ export default { internationalPhone, phone };
@@ -0,0 +1,79 @@
1
+ const timeRule = function (node) {
2
+ const value = node.value;
3
+ const { step, min, max } = node.props.attrs;
4
+
5
+ if (!value) return true;
6
+
7
+ const total = parseTimeToSeconds(value)
8
+ const minSeconds = parseTimeToSeconds(min)
9
+ const maxSeconds = parseTimeToSeconds(max)
10
+ const stepSize = Number(step)
11
+
12
+ // ⏰ Before min
13
+ if (total < minSeconds) {
14
+ return false;
15
+ }
16
+
17
+ // πŸ•’ After max
18
+ if (total > maxSeconds) {
19
+ return false;
20
+ }
21
+
22
+ // πŸ”’ Between steps
23
+ const delta = total - minSeconds
24
+ const remainder = delta % stepSize
25
+ if (remainder === 0) return true;
26
+
27
+ return false;
28
+ }
29
+
30
+ const timeMessage = function (node) {
31
+ const value = node.value;
32
+ const { step, min, max } = node.props.attrs;
33
+
34
+ if (!value) return ''
35
+
36
+ const total = parseTimeToSeconds(value)
37
+ const minSeconds = parseTimeToSeconds(min)
38
+ const maxSeconds = parseTimeToSeconds(max)
39
+ const stepSize = Number(step)
40
+
41
+ // ⏰ Before min
42
+ if (total < minSeconds) {
43
+ const minStr = formatSecondsToTime(minSeconds)
44
+ return `De waarde moet ${minStr} uur of later zijn.`
45
+ }
46
+
47
+ // πŸ•’ After max
48
+ if (total > maxSeconds) {
49
+ const maxStr = formatSecondsToTime(maxSeconds)
50
+ return `De waarde moet ${maxStr} uur of eerder zijn.`
51
+ }
52
+
53
+ // πŸ”’ Between steps
54
+ const delta = total - minSeconds
55
+ const remainder = delta % stepSize
56
+ if (remainder === 0) return ''
57
+
58
+ const lower = total - remainder
59
+ const upper = lower + stepSize
60
+
61
+ const lowerStr = formatSecondsToTime(Math.max(lower, minSeconds))
62
+ const upperStr = formatSecondsToTime(Math.min(upper, maxSeconds))
63
+
64
+ return `Voer een geldige tijd in. De twee dichtstbijzijnde geldige waarden zijn ${lowerStr} en ${upperStr}.`
65
+ }
66
+
67
+ function parseTimeToSeconds(timeStr) {
68
+ if (!timeStr) return 0
69
+ const [h, m = 0, s = 0] = timeStr.split(':').map(Number)
70
+ return h * 3600 + m * 60 + s
71
+ }
72
+
73
+ function formatSecondsToTime(seconds) {
74
+ const hh = Math.floor(seconds / 3600)
75
+ const mm = Math.floor((seconds % 3600) / 60)
76
+ return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`
77
+ }
78
+
79
+ export { timeRule, timeMessage };
@@ -0,0 +1,42 @@
1
+ import { checkVAT, countries } from '@accountable/jsvat';
2
+
3
+ const isVatValid = (vat) => {
4
+ // BE0xxxxxxx β†’ mod 97 on first 8 digits
5
+ if (vat[0] === '0') {
6
+ const base = Number(vat.slice(0, 8));
7
+ const checkDigits = Number(vat.slice(8, 10));
8
+ return 97 - (base % 97) === checkDigits;
9
+ }
10
+
11
+ // BE1xxxxxxx β†’ check if is 10 digits
12
+ if (vat[0] === '1') {
13
+ return vat.length === 10;
14
+ }
15
+
16
+ return false; // other leading digits not allowed
17
+ };
18
+
19
+ const newBelgium = {
20
+ name: 'newBelgium',
21
+ codes: ['BE', 'BEL', '056'],
22
+ calcWithFormatFn: (vat) => {
23
+ if (vat.length === 10) {
24
+ return { vat: `BE${vat}`, isValid: isVatValid(vat) };
25
+ }
26
+
27
+ // Invalid length
28
+ return { vat: `BE${vat}`, isValid: false };
29
+ },
30
+ rules: {
31
+ multipliers: {},
32
+ regex: [/^(BE)(\d{10})$/],
33
+ },
34
+ };
35
+
36
+ const newCountries = countries.map((obj) => (obj.name === 'Belgium' ? newBelgium : obj));
37
+
38
+ const vat = function (node) {
39
+ return checkVAT(node.value, newCountries).isValid;
40
+ };
41
+
42
+ export default vat;