UNPKG

@recras/online-booking-js

Version:

JS library for easy integration of Recras online booking and voucher sales

560 lines (501 loc) 22.7 kB
class RecrasContactForm { constructor(options = {}) { this.languageHelper = new RecrasLanguageHelper(); if (!(options instanceof RecrasOptions)) { throw new Error(this.languageHelper.translate('ERR_OPTIONS_INVALID')); } this.options = options; if (!this.options.getFormId()) { throw new Error(this.languageHelper.translate('ERR_NO_FORM')); } this.checkboxEventListeners = []; this.eventHelper = new RecrasEventHelper(); this.eventHelper.setEvents(this.options.getAnalyticsEvents()); this.element = this.options.getElement(); this.element.classList.add('recras-contactform-wrapper'); this.languageHelper.setOptions(options); if (RecrasLanguageHelper.isValid(this.options.getLocale())) { this.languageHelper.setLocale(this.options.getLocale()); } this.fetchJson = url => RecrasHttpHelper.fetchJson(url, this.error.bind(this)); this.postJson = (url, data) => RecrasHttpHelper.postJson(this.options.getApiBase() + url, data, this.error.bind(this)); RecrasCSSHelper.loadCSS('global'); this.GENDERS = { onbekend: 'GENDER_UNKNOWN', man: 'GENDER_MALE', vrouw: 'GENDER_FEMALE', }; // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#inappropriate-for-the-control this.AUTOCOMPLETE_OPTIONS = { 'contact.naam': 'organization', 'contact.website': 'url', 'contactpersoon.achternaam': 'family-name', 'contactpersoon.adres': 'address-line1', 'contactpersoon.plaats': 'address-level2', 'contactpersoon.postcode': 'postal-code', 'contactpersoon.voornaam': 'given-name', }; } checkRequiredCheckboxes() { this.removeWarnings(); let isOkay = true; [...this.findElements('.checkboxGroupRequired')].forEach(group => { let checked = group.querySelectorAll('input:checked'); if (checked.length === 0) { group.parentNode.querySelector('label').insertAdjacentHTML('beforeend', `<div class="recrasError">${ this.languageHelper.translate('CONTACT_FORM_CHECKBOX_REQUIRED') }</div>`); isOkay = false; } }); return isOkay; } appendHtml(msg) { this.element.insertAdjacentHTML('beforeend', msg); } error(msg) { this.removeErrors('.recras-contactform'); this.showInlineError(this.findElement('.submitForm'), msg); } findElement(querystring) { return this.element.querySelector(querystring); } findElements(querystring) { return this.element.querySelectorAll(querystring); } isStandalone(options) { return !!options.standalone; } generateForm(extraOptions = {}) { let waitFor = []; if (this.hasCountryField()) { waitFor.push(this.getCountryList()); } if (this.hasBookingDateField() || this.hasRelationDateField()) { waitFor.push(RecrasCalendarHelper.loadScript()); RecrasCSSHelper.loadCSS('pikaday'); } return Promise.all(waitFor).then(() => { const standalone = this.isStandalone(extraOptions); const validateText = standalone ? 'novalidate' : ''; let html = `<form class="recras-contactform" ${ validateText }>`; if (extraOptions.voucherQuantitySelector) { html += this.quantitySelector(extraOptions.quantityTerm); } this.contactFormFields.forEach((field, idx) => { html += '<div>' + this.showField(field, idx) + '</div>'; }); if (standalone) { html += this.submitButtonHtml(); } html += '</form>'; return html; }); } generateJson() { let formEl = this.options.getElement().querySelector('.recras-contactform'); let elements = formEl.querySelectorAll('[id^="contactformulier-"], input[type="radio"]:checked'); let contactForm = {}; [...elements].forEach(field => { contactForm[field.dataset.identifier] = field.value; }); [...formEl.querySelectorAll('input[type="checkbox"]:checked')].forEach(field => { if (contactForm[field.dataset.identifier] === undefined) { contactForm[field.dataset.identifier] = []; } contactForm[field.dataset.identifier].push(field.value); }); if (contactForm['boeking.datum']) { contactForm['boeking.datum'] = RecrasDateHelper.formatStringForAPI(contactForm['boeking.datum']); } if (this.nonce) { contactForm.nonce = this.nonce; } return contactForm; } getContactFormFields() { return this.fetchJson(this.options.getApiBase() + 'contactformulieren/' + this.options.getFormId() + '?embed=Velden') .then(form => { this.contactFormFields = form.Velden; this.packages = this.sortPackages(form.Arrangementen); this.nonce = form.nonce ?? null; return this.contactFormFields; }); } getCountryList() { return this.fetchJson('https://cdn.rawgit.com/umpirsky/country-list/ddabf3a8/data/' + this.languageHelper.locale + '/country.json') .then(json => { this.countries = json; return this.countries; }); } sortPackages(packs) { return packs.sort((a, b) => { // Prioritise package name if (a.arrangement < b.arrangement) { return -1; } if (a.arrangement > b.arrangement) { return 1; } // Sort by ID in the off chance that two packages are named the same if (a.id < b.id) { return -1; } if (a.id > b.id) { return 1; } // This cannot happen return 0; }); } getInvalidFields() { let invalid = []; let required = this.getEmptyRequiredFields(); let els = this.findElements('.recras-contactform :invalid'); for (let el of els) { if (!required.includes(el)) { invalid.push(el); } } return invalid; } getEmptyRequiredFields() { let isEmpty = []; let els = this.findElements('.recras-contactform :required'); for (let el of els) { if (el.value === undefined || el.value === '') { isEmpty.push(el); } } return isEmpty; } getRelationExtraDateFields() { return this.contactFormFields.filter( field => field.soort_invoer === 'contact.extra' && field.input_type === 'date' ); } hasFieldOfType(identifier) { return this.contactFormFields.filter(field => { return field.field_identifier === identifier; }).length > 0; } hasBookingDateField() { return this.hasFieldOfType('boeking.datum'); } hasCountryField() { return this.hasFieldOfType('contact.landcode'); } hasPackageField() { return this.hasFieldOfType('boeking.arrangement'); } hasRelationDateField() { return this.getRelationExtraDateFields().length > 0; } isEmpty() { let els = this.findElements('.recras-contactform input, .recras-contactform select, .recras-contactform textarea'); let formValues = [...els].map(el => el.value); return !formValues.some(v => v !== ''); } isValid() { return this.findElement('.recras-contactform').checkValidity(); } loadingIndicatorHide() { [...document.querySelectorAll('.recrasLoadingIndicator')].forEach(el => { el.parentNode.removeChild(el); }); } loadingIndicatorShow(afterEl) { if (!afterEl) { return; } afterEl.insertAdjacentHTML('beforeend', `<span class="recrasLoadingIndicator">${ this.languageHelper.translate('LOADING') }</span>`); } quantitySelector(quantityTerm) { if (!quantityTerm) { quantityTerm = this.languageHelper.translate('VOUCHER_QUANTITY'); } return `<div> <label for="number-of-vouchers">${ quantityTerm }</label> <input type="number" id="number-of-vouchers" class="number-of-vouchers" min="1" max="100" value="1" required> </div>`; } removeErrors(parentQuery = '') { [...this.findElements(parentQuery + ' .booking-error')].forEach(el => { el.parentNode.removeChild(el); }); } removeWarnings() { [...this.findElements('.recrasError')].forEach(el => { el.parentNode.removeChild(el); }); [...this.findElements('.recras-success')].forEach(el => { el.parentNode.removeChild(el); }); } hasEmptyRequiredFields() { return this.getEmptyRequiredFields().length > 0; } showField(field, idx) { if (field.soort_invoer === 'header') { return `<h3>${ field.naam }</h3>`; } const today = RecrasDateHelper.toString(new Date()); const timePattern = '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9])'; let label = this.showLabel(field, idx); let attrRequired = field.verplicht ? 'required' : ''; let classes; let html; let placeholder; let fixedAttributes = `id="contactformulier-${ idx }" name="contactformulier${ idx }" ${ attrRequired } data-identifier="${ field.field_identifier }"`; switch (field.soort_invoer) { case 'contactpersoon.geslacht': html = `<select ${ fixedAttributes } autocomplete="sex">`; Object.keys(this.GENDERS).forEach(key => { html += `<option value="${ key }">${ this.languageHelper.translate(this.GENDERS[key]) }`; }); html += '</select>'; return label + html; case 'keuze': classes = ['checkboxGroup']; if (field.verplicht) { classes.push('checkboxGroupRequired'); } html = `<div class="${ classes.join(' ') }">`; field.mogelijke_keuzes.forEach(choice => { html += `<label><input type="checkbox" name="contactformulier${ idx }" value="${ choice }" data-identifier="${ field.field_identifier }">${ choice }</label>`; }); html += '</div>'; return label + html; case 'keuze_enkel': case 'contact.soort_klant': html = `<div class="radioGroup">`; field.mogelijke_keuzes.forEach(choice => { html += `<label><input type="radio" name="contactformulier${ idx }" value="${ choice }" ${ attrRequired } data-identifier="${ field.field_identifier }">${ choice }</label>`; }); html += `</div>`; return label + html; case 'veel_tekst': return label + `<textarea ${ fixedAttributes }></textarea>`; case 'contactpersoon.telefoon1': case 'contactpersoon.telefoon2': return label + `<input type="tel" ${ fixedAttributes } autocomplete="tel">`; case 'contactpersoon.email1': case 'contactpersoon.email2': return label + `<input type="email" ${ fixedAttributes } autocomplete="email">`; case 'contactpersoon.nieuwsbrieven': classes = ['checkboxGroup']; if (field.verplicht) { classes.push('checkboxGroupRequired'); } html = `<div class="${ classes.join(' ') }">`; Object.keys(field.newsletter_options).forEach(key => { html += `<label><input type="checkbox" name="contactformulier${ idx }" value="${ key }" data-identifier="${ field.field_identifier }">${ field.newsletter_options[key] }</label>`; }); html += '</div>'; return label + html; case 'contact.landcode': html = `<select ${ fixedAttributes } autocomplete="country">`; Object.keys(this.countries).forEach(code => { let selectedText = code.toUpperCase() === this.options.getDefaultCountry() ? 'selected' : ''; html += `<option value="${ code }" ${ selectedText }>${ this.countries[code] }`; }); html += '</select>'; return label + html; case 'boeking.datum': placeholder = this.languageHelper.translate('DATE_FORMAT'); return label + `<input type="text" ${ fixedAttributes } min="${ today }" placeholder="${ placeholder }" autocomplete="off">`; case 'boeking.groepsgrootte': return label + `<input type="number" ${ fixedAttributes } min="1">`; case 'boeking.starttijd': placeholder = this.languageHelper.translate('TIME_FORMAT'); return label + `<input type="time" ${ fixedAttributes } placeholder="${ placeholder }" pattern="${ timePattern }">`; case 'boeking.arrangement': const preFilledPackage = this.options.getPackageId(); if (field.verplicht && this.packages.length === 1) { let pack = this.packages[0]; html = `<select ${ fixedAttributes }> <option value="${ pack.id }" selected>${ pack.arrangement } </select>`; return label + html; } html = `<select ${ fixedAttributes }>`; html += `<option value="">`; this.packages.forEach(pack => { const selText = preFilledPackage && preFilledPackage === pack.id ? 'selected' : ''; html += `<option value="${ pack.id }" ${ selText }>${ pack.arrangement }`; }); html += '</select>'; return label + html; case 'contact.extra': switch (field.input_type) { case 'number': return label + `<input type="number" ${ fixedAttributes } autocomplete="off">`; case 'date': case 'text': return label + `<input type="text" ${ fixedAttributes } maxlength="200">`; case 'multiplechoice': classes = ['checkboxGroup']; if (field.verplicht) { classes.push('checkboxGroupRequired'); } html = `<div class="${ classes.join(' ') }">`; field.mogelijke_keuzes.forEach(choice => { html += `<label><input type="checkbox" name="contactformulier${ idx }" value="${ choice }" data-identifier="${ field.field_identifier }">${ choice }</label>`; }); html += '</div>'; return label + html; case 'singlechoice': html = `<div class="radioGroup">`; field.mogelijke_keuzes.forEach(choice => { html += `<label><input type="radio" name="contactformulier${ idx }" value="${ choice }" ${ attrRequired } data-identifier="${ field.field_identifier }">${ choice }</label>`; }); html += `</div>`; return label + html; default: console.debug('Unknown type', field.input_type, field); return label + `<input type="text" ${ fixedAttributes }>`; } case 'contact.website': //TODO: type=url ? default: let autocomplete = this.AUTOCOMPLETE_OPTIONS[field.soort_invoer] ? this.AUTOCOMPLETE_OPTIONS[field.soort_invoer] : ''; return label + `<input type="text" ${ fixedAttributes } autocomplete="${ autocomplete }">`; } } showForm() { this.loadingIndicatorShow(this.element); return this.getContactFormFields() .then(() => this.generateForm({ standalone: true, })) .then(html => { this.appendHtml(html); [...this.findElements('.checkboxGroupRequired')].forEach(group => { [...group.querySelectorAll('input')].forEach(el => { el.addEventListener('change', this.checkRequiredCheckboxes.bind(this)); }); }); this.findElement('.recras-contactform').addEventListener('submit', this.submitForm.bind(this)); if (this.hasBookingDateField()) { let pikadayOptions = Object.assign( RecrasCalendarHelper.defaultOptions(), { field: this.findElement('[data-identifier="boeking.datum"]'), i18n: RecrasCalendarHelper.i18n(this.languageHelper), numberOfMonths: 1, parse: RecrasDateHelper.parseMDY, } ); new Pikaday(pikadayOptions); } if (this.hasRelationDateField()) { const fields = this.getRelationExtraDateFields(); const thisYear = (new Date()).getFullYear(); let pikadayOptions = Object.assign( RecrasCalendarHelper.defaultOptions(), { i18n: RecrasCalendarHelper.i18n(this.languageHelper), numberOfMonths: 1, yearRange: [thisYear - 90, thisYear + 10] } ); delete pikadayOptions.minDate; for (let field of fields) { pikadayOptions.field = this.findElement(`[data-identifier="${ field.field_identifier }"]`); new Pikaday(pikadayOptions); } } this.loadingIndicatorHide(); }); } showInlineError(element, errorMsg) { element.parentNode.insertAdjacentHTML( 'afterend', `<div class="booking-error">${ errorMsg }</div>` ); } showErrors() { let errors = []; for (let el of this.getEmptyRequiredFields()) { const labelEl = el.parentNode.querySelector('label'); const requiredText = this.languageHelper.translate('CONTACT_FORM_FIELD_REQUIRED', { FIELD_NAME: labelEl.innerText, }); errors.push(requiredText); } for (let el of this.getInvalidFields()) { let parentEl = el.closest('div'); if (parentEl.classList.contains('radioGroup')) { parentEl = parentEl.parentNode.closest('div'); } const labelEl = parentEl.querySelector('label'); console.log(labelEl, parentEl.querySelectorAll('label')); const invalidText = this.languageHelper.translate('CONTACT_FORM_FIELD_INVALID', { FIELD_NAME: labelEl.innerText, }); errors.push(invalidText); } errors = [...new Set(errors)]; // Only unique text this.findElement('button[type="submit"]').insertAdjacentHTML( 'afterend', `<div class="booking-error"><ul><li>${ errors.join('<li>') }</ul></div>` ); } showLabel(field, idx) { let labelText = field.naam; if (field.verplicht) { labelText += `<span class="recras-contactform-required" title="${ this.languageHelper.translate('ATTR_REQUIRED') }"></span>`; } return `<label for="contactformulier-${ idx }">${ labelText }</label>`; } submitButtonHtml() { return `<button type="submit" class="submitForm">${ this.languageHelper.translate('BUTTON_SUBMIT_CONTACT_FORM') }</button>`; } submitForm(e) { e.preventDefault(); let submitButton = this.findElement('.submitForm'); this.removeErrors('.recras-contactform'); if (this.isEmpty()) { submitButton.parentNode.insertAdjacentHTML( 'beforeend', `<div class="booking-error">${ this.languageHelper.translate('ERR_CONTACT_FORM_EMPTY') }</div>` ); return false; } else if (this.hasEmptyRequiredFields() || !this.isValid()) { this.showErrors(); return false; } if (!this.checkRequiredCheckboxes()) { return false; } this.eventHelper.sendEvent( RecrasEventHelper.PREFIX_CONTACT_FORM, RecrasEventHelper.EVENT_CONTACT_FORM_SUBMIT, null, this.options.getFormId() ); this.loadingIndicatorHide(); this.loadingIndicatorShow(submitButton); submitButton.setAttribute('disabled', 'disabled'); this.postJson('contactformulieren/' + this.options.getFormId() + '/opslaan', this.generateJson()).then(json => { if (json.success) { if (this.options.getRedirectUrl()) { window.top.location.href = this.options.getRedirectUrl(); } else { this.element.scrollIntoView({ behavior: 'smooth', }); this.element.insertAdjacentHTML( 'afterbegin', `<p class="recras-success">${ this.languageHelper.translate('CONTACT_FORM_SUBMIT_SUCCESS') }</p>` ); submitButton.parentNode.reset(); } } else { this.error(this.languageHelper.translate('CONTACT_FORM_SUBMIT_FAILED')); } submitButton.removeAttribute('disabled'); this.loadingIndicatorHide(); }); return false; } }