diff --git a/.gitignore b/.gitignore index f6a209f..539cac1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ .DS_Store .sass-cache/ node_modules/ +userforms-cms.js.map +userforms.js.map +userforms-cms.css.map +userforms.css.map + diff --git a/client/dist/js/userforms.js b/client/dist/js/userforms.js index 0533c2b..244f954 100644 --- a/client/dist/js/userforms.js +++ b/client/dist/js/userforms.js @@ -1 +1 @@ -!function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};e.m=t,e.c=r,e.i=function(t){return t},e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s="./client/src/bundles/bundle.js")}({"./client/src/bundles/UserForms.js":function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{default:t}}var i=r(1),s=n(i),o=r(0),a=n(o);(0,s.default)(document).ready(function(t){function e(e){return this.$el=e instanceof t?e:t(e),this.$el.find("h4").text(a.default._t("UserForms.ERROR_CONTAINER_HEADER","Please correct the following errors and try again:")),this}function r(r){var n=this;this.$el=r instanceof t?r:t(r);var i=this.$el.closest(".userform").data("inst");return this.$elButton=t(".step-button-wrapper[data-for='"+this.$el.prop("id")+"']"),this.viewed=!1,this.valid=!1,this.id=null,this.hide(),u.DISPLAY_ERROR_MESSAGES_AT_TOP&&(this.errorContainer=new e(this.$el.find(".error-container")),i.$el.on("userform.form.error",function(e,r){n.$el.is(":visible")&&t.each(r.errorList,function(e,r){n.errorContainer.updateErrorMessage(t(r.element),r.message)})}),i.$el.on("userform.form.valid",function(t,e){n.errorContainer.removeErrorMessage(e)})),this.$elButton.on("userform.field.hide userform.field.show",function(){i.$el.trigger("userform.form.conditionalstep")}),this}function n(e){var r=this;this.$el=e instanceof t?e:t(e),this.$buttons=this.$el.find(".step-button-jump"),this.$jsAlign=this.$el.find(".js-align");var n=this.$el.closest(".userform").data("inst");return this.$buttons.each(function(e,n){t(n).on("click",function(e){e.preventDefault();var n=parseInt(t(e.target).data("step"),10);r.$el.trigger("userform.progress.changestep",n)})}),n.$el.on("userform.form.changestep",function(t,e){r.update(e)}),n.$el.on("userform.form.conditionalstep",function(){var e=r.$buttons.filter(":visible");e.each(function(e,r){t(r).text(e+1)}),r.$el.find(".progress-bar").attr("aria-valuemax",e.length),r.$el.find(".total-step-number").text(e.length)}),this.$jsAlign.each(function(e,n){var i=t(n),s=100/(r.$jsAlign.length-1)*e,o=s+"%",a=i.innerWidth()/2*-1;i.css({left:o,marginLeft:a}),e===r.$jsAlign.length-1?i.css({marginLeft:2*a}):0===e&&i.css({marginLeft:0})}),this}function i(e){var r=this;this.$el=e instanceof t?e:t(e);var n=this.$el.closest(".userform");this.userformInstance=n.data("inst"),this.$prevButton=this.$el.find(".step-button-prev"),this.$nextButton=this.$el.find(".step-button-next"),this.$prevButton.parent().attr("aria-hidden",!1).show(),this.$nextButton.parent().attr("aria-hidden",!1).show();var i=function(){var e=n.offset();t("html, body").animate({scrollTop:e.top},"slow")};return this.$prevButton.on("click",function(t){t.preventDefault(),i(),r.$el.trigger("userform.action.prev")}),this.$nextButton.on("click",function(t){t.preventDefault(),i(),r.$el.trigger("userform.action.next")}),this.userformInstance.$el.on("userform.form.changestep userform.form.conditionalstep",function(){r.update()}),this}function s(r){var n=this;return this.$el=r instanceof t?r:t(r),this.steps=[],this.errorContainer=new e(this.$el.children(".error-container")),this.$el.on("userform.action.prev",function(){n.prevStep()}),this.$el.on("userform.action.next",function(){n.nextStep()}),this.$el.find(".userform-progress").on("userform.progress.changestep",function(t,e){n.jumpToStep(e-1)}),this.$el.on("userform.form.valid",function(t,e){n.errorContainer.removeStepLink(e)}),this.$el.validate(this.validationOptions),this.$el.find(".optionset.requiredField input").each(function(e,r){t(r).rules("add",{required:!0})}),this}function o(o,d){var f=this,c=t(d);if(0!==c.length){u.ENABLE_LIVE_VALIDATION=void 0!==c.data("livevalidation"),u.DISPLAY_ERROR_MESSAGES_AT_TOP=void 0!==c.data("toperrors"),!1===u.ENABLE_LIVE_VALIDATION&&t.extend(s.prototype.validationOptions,{onfocusout:!1}),u.DISPLAY_ERROR_MESSAGES_AT_TOP&&t.extend(s.prototype.validationOptions,{invalidHandler:function(t,e){c.trigger("userform.form.error",[e])},onfocusout:!1}),c.find(".userform-progress, .step-navigation").attr("aria-hidden",!1).show(),t.extend(r.prototype,l),t.extend(e.prototype,l);var h=new s(c);c.data("inst",h),u.HIDE_FIELD_LABELS&&c.find("label.left").each(function(){var e=t(f);t('[name="'+e.attr("for")+'"]').attr("placeholder",e.text()),e.remove()}),h.$el.find(".form-step").each(function(t,e){var n=new r(e);h.addStep(n)}),h.setCurrentStep(h.steps[0]);var p=c.find(".userform-progress");p.length&&new n(p).update(0);var m=c.find(".step-navigation");m.length&&new i(m).update(),t(document).on("click","input.text[data-showcalendar]",function(){var e=t(f);e.ssDatepicker(),e.data("datepicker")&&e.datepicker("show")}),setInterval(function(){t.ajax({url:"UserDefinedFormController/ping"})},18e4),void 0!==c.areYouSure&&c.areYouSure({message:a.default._t("UserForms.LEAVE_CONFIRMATION","You have unsaved changes!")})}}var u={},l={show:function(){this.$el.attr("aria-hidden",!1).show()},hide:function(){this.$el.attr("aria-hidden",!0).hide()}};e.prototype.hasErrors=function(){return this.$el.find(".error-list").children().length>0},e.prototype.removeErrorMessage=function(t){this.$el.find("#"+t+"-top-error").remove(),this.hasErrors()||this.hide()},e.prototype.addStepLink=function(e){var r=this.$el.closest(".userform").data("inst"),n=e.$el.attr("id")+"-error-link",i=this.$el.find("#"+n),s=e.$el.attr("id"),o=e.$el.data("title");i.length||(i=t('
  • '+o+"
  • "),i.on("click",function(t){t.preventDefault(),r.jumpToStep(e.id)}),this.$el.find(".error-list").append(i))},e.prototype.removeStepLink=function(e){var r=t("#"+e).closest(".form-step").attr("id");this.$el.find("#"+r+"-error-link").remove(),this.$el.find(".error-list").is(":empty")&&this.hide()},e.prototype.updateErrorMessage=function(e,r){var n=this,i=e.attr("id"),s="#"+i,o=i+"-top-error",a=t("#"+o),u=e.attr("aria-describedby");if(!r)return void a.addClass("fixed");a.removeClass("fixed"),this.show(),1===a.length?a.show().find("a").html(r):(e.closest(".field[id]").each(function(){var e=t(n).attr("id");e&&(s="#"+e)}),a=t("
  • "),a.attr("id",o).find("a").attr("href",location.pathname+location.search+s).html(r),this.$el.find("ul").append(a),u?u.match(new RegExp("\\b"+o+"\\b"))||(u+=" "+o):u=o,e.attr("aria-describedby",u))},r.prototype.conditionallyHidden=function(){return!this.$elButton.find("button").is(":visible")},n.prototype.update=function(e){var r=t(this.$el.parent(".userform").find(".form-step")[e]),n=0,i=e/(this.$buttons.length-1)*100;this.$buttons.each(function(r,i){return!(r>e||(t(i).is(":visible")&&(n+=1),0))}),this.$el.find(".current-step-number").each(function(e,r){t(r).text(n)}),this.$el.find("[aria-valuenow]").each(function(e,r){t(r).attr("aria-valuenow",n)}),this.$buttons.each(function(e,r){var i=t(r),s=i.parent();if(parseInt(i.data("step"),10)===n&&i.is(":visible"))return s.addClass("current viewed"),void i.removeAttr("disabled");s.removeClass("current")}),this.$el.siblings(".progress-title").text(r.data("title")),i=i?i+"%":"",this.$el.find(".progress-bar").width(i)},i.prototype.update=function(){var t=this.userformInstance.steps.length,e=this.userformInstance.currentStep?this.userformInstance.currentStep.id:0,r=null,n=null;for(this.$el.find(".step-button-prev")[0===e?"hide":"show"](),r=t-1;r>=0;r--)if(n=this.userformInstance.steps[r],!n.conditionallyHidden()){this.$el.find(".step-button-next")[e>=r?"hide":"show"](),this.$el.find(".btn-toolbar")[e>=r?"show":"hide"]();break}},s.prototype.validationOptions={ignore:":hidden,ul",errorClass:"error",errorElement:"span",errorPlacement:function(t,e){t.addClass("message"),e.is(":radio")||e.parents(".checkboxset").length>0?t.appendTo(e.closest(".middleColumn, .field")):e.parents(".checkbox").length>0?t.appendTo(e.closest(".field")):t.insertAfter(e)},invalidHandler:function(t,e){setTimeout(function(){e.currentElements.filter(".error").first().focus()},0)},submitHandler:function(e){var r=!0,n=t(e).closest(".userform").data("inst");if(n.currentStep&&(n.currentStep.valid=t(e).valid()),t.each(n.steps,function(t,e){e.valid||e.conditionallyHidden()||(r=!1,n.errorContainer.addStepLink(e))}),r){var i=t(e).find(".field.requiredField.hide input");i.length>0&&i.removeAttr("required aria-required data-rule-required").valid(),t(e).removeClass("dirty"),e.submit(),n.$el.trigger("userform.form.submit")}else n.errorContainer.show()},success:function(e){var r=t(e).closest(".userform").data("inst"),n=t(e).attr("id"),i=n.substr(0,n.indexOf("-error")).replace(/[\\[\\]]/,"");e.remove(),r.$el.trigger("userform.form.valid",[i])}},s.prototype.addStep=function(t){t instanceof r&&(t.id=this.steps.length,this.steps.push(t))},s.prototype.setCurrentStep=function(t){t instanceof r&&(this.currentStep=t,this.currentStep.show(),this.currentStep.viewed=!0,this.currentStep.$el.addClass("viewed"))},s.prototype.jumpToStep=function(t,e){var r=this.steps[t],n=!1,i=void 0===e||e;if(void 0!==r){if(r.conditionallyHidden())return void(i?this.jumpToStep(t+1,e):this.jumpToStep(t-1,e));n=this.$el.valid(),this.currentStep.valid=n,!1===n&&!1===r.viewed||(this.currentStep.hide(),this.setCurrentStep(r),this.$el.trigger("userform.form.changestep",[r.id]))}},s.prototype.nextStep=function(){this.jumpToStep(this.steps.indexOf(this.currentStep)+1,!0)},s.prototype.prevStep=function(){this.jumpToStep(this.steps.indexOf(this.currentStep)-1,!1)},t(".userform").each(o)})},"./client/src/bundles/bundle.js":function(t,e,r){"use strict";r("./client/src/bundles/UserForms.js")},0:function(t,e){t.exports=i18n},1:function(t,e){t.exports=jQuery}}); \ No newline at end of file +!function(e){function t(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var r={};t.m=e,t.c=r,t.i=function(e){return e},t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s="./client/src/bundles/bundle.js")}({"./client/src/bundles/UserForms.js":function(e,t,r){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e){return"none"!==e.style.display&&"hidden"!==e.style.visibility&&!e.classList.contains("hide")}var u=function(){function e(e,t){for(var r=0;ra?r(u+" cannot be greater than "+a):e()})});var l=n.getAttribute("data-rule-minlength"),c=n.getAttribute("data-rule-maxlength");null===l&&null===c||(r[n.getAttribute("name")].asyncValidator=function(e,t){return new Promise(function(e,r){null!==l&&t.lengthc?r(u+" cannot be longer than "+c):e()})})}}),r}},{key:"validate",value:function(e){var t=this,r=this.getValidationsDescriptors(e);if(Object.keys(r).length){var n=new o.default(r),i=new FormData(this.userForm.dom),u={};return i.forEach(function(e,r){var n=e,i=t.getInputByName(r);n&&i&&t.isInputNumeric(i)&&(n=parseFloat(n)),u[r]=n}),this.step.querySelectorAll('input[type="radio"],input[type="checkbox"]').forEach(function(e){var t=e.getAttribute("name");void 0===u[t]&&(u[t]="")}),new Promise(function(e,r){n.validate(u,function(n){n&&n.length?(t.displayErrorMessages(n),r(n)):(t.displayErrorMessages([]),e())})})}return new Promise(function(e){e()})}},{key:"enableLiveValidation",value:function(){var e=this;this.step.querySelectorAll("input, textarea, select").forEach(function(t){t.addEventListener("focusin",function(){t.classList.add("focused")}),t.addEventListener("change",function(){t.classList.add("dirty")}),t.addEventListener("focusout",function(){e.validate(!0).then(function(){}).catch(function(){})})})}},{key:"displayErrorMessages",value:function(e){var t=this,r=[];e.forEach(function(e){var n=t.userForm.dom.querySelector("#"+e.field);if(n){var i=n.querySelector("span.error");i||(i=document.createElement("span"),i.classList.add("error"),i.setAttribute("data-id",e.field)),r.push(e.field),i.innerHTML=e.message,n.append(i)}}),this.step.querySelectorAll("span.error").forEach(function(e){var t=e.getAttribute("data-id");-1===r.indexOf(t)&&e.remove()})}}]),e}(),c=function(){function e(t,r){n(this,e),this.dom=t,this.userForm=r,this.prevButton=t.querySelector(".step-button-prev"),this.nextButton=t.querySelector(".step-button-next"),this.init()}return u(e,[{key:"init",value:function(){var e=this;this.prevButton.addEventListener("click",function(t){t.preventDefault(),window.triggerDispatchEvent(e.userForm.dom,"userform.action.prev")}),this.nextButton.addEventListener("click",function(t){t.preventDefault(),window.triggerDispatchEvent(e.userForm.dom,"userform.action.next")}),this.update(),this.userForm.dom.addEventListener("userform.form.changestep",function(){e.update()}),this.userForm.dom.addEventListener("userform.form.conditionalstep",function(){e.update()})}},{key:"update",value:function(){var e=this.userForm.getNumberOfSteps(),t=this.userForm.getCurrentStepID(),r=null,n=null;for(r=e-1;r>=0;r--)if(n=this.userForm.getStep(r),!n.conditionallyHidden()){t>=r?this.nextButton.parentNode.classList.add("hide"):this.nextButton.parentNode.classList.remove("hide"),t>0&&t<=r?this.prevButton.parentNode.classList.remove("hide"):this.prevButton.parentNode.classList.add("hide"),t>=r?this.dom.querySelector(".btn-toolbar").classList.remove("hide"):this.dom.querySelector(".btn-toolbar").classList.add("hide");break}}}]),e}(),f=function(){function e(t){n(this,e),this.dom=t,this.CONSTANTS={},this.steps=[],this.progressBar=null,this.actions=null,this.currentStep=null,this.CONSTANTS.ENABLE_LIVE_VALIDATION=void 0!==this.dom.getAttribute("livevalidation"),this.CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP=void 0!==this.dom.getAttribute("toperrors"),this.CONSTANTS.ENABLE_ARE_YOU_SURE=void 0!==this.dom.getAttribute("enableareyousure")}return u(e,[{key:"init",value:function(){this.initialiseFormSteps(),this.CONSTANTS.ENABLE_ARE_YOU_SURE&&this.initAreYouSure()}},{key:"initialiseFormSteps",value:function(){var e=this;this.dom.querySelectorAll(".form-step").forEach(function(t){var r=new l(t,e);r.hide(),e.addStep(r),e.CONSTANTS.ENABLE_LIVE_VALIDATION&&r.enableLiveValidation()}),this.setCurrentStep(this.steps[0]);var t=this.dom.querySelector(".userform-progress");t&&(this.progressBar=new a(t,this));var r=this.dom.querySelector(".step-navigation");r&&(this.formActions=new c(r,this),this.formActions.update()),this.setUpPing(),this.dom.addEventListener("userform.action.next",function(){e.nextStep()}),this.dom.addEventListener("userform.action.prev",function(){e.prevStep()}),this.dom.addEventListener("submit",function(t){e.validateForm(t)})}},{key:"validateForm",value:function(e){var t=this;e.preventDefault(),this.currentStep.validate().then(function(e){e||t.dom.submit()}).catch(function(){})}},{key:"setCurrentStep",value:function(e){e instanceof l&&(this.currentStep=e,this.currentStep.show())}},{key:"addStep",value:function(e){e instanceof l&&(e.setId(this.steps.length),this.steps.push(e))}},{key:"getNumberOfSteps",value:function(){return this.steps.length}},{key:"getCurrentStepID",value:function(){return this.currentStep.id?this.currentStep.id:0}},{key:"getStep",value:function(e){return this.steps[e]}},{key:"nextStep",value:function(){var e=this;this.currentStep.validate().then(function(){e.jumpToStep(e.steps.indexOf(e.currentStep)+1,!0)}).catch(function(){})}},{key:"prevStep",value:function(){this.jumpToStep(this.steps.indexOf(this.currentStep)-1,!0)}},{key:"jumpToStep",value:function(e,t){var r=this.steps[e],n=void 0===t||t;if(void 0!==r){if(r.conditionallyHidden())return void(n?this.jumpToStep(e+1,t):this.jumpToStep(e-1,t));this.currentStep&&this.currentStep.hide(),this.setCurrentStep(r),window.triggerDispatchEvent(this.dom,"userform.form.changestep",{stepId:r.id})}}},{key:"setUpPing",value:function(){window.setInterval(function(){fetch("UserDefinedFormController/ping")},18e4)}},{key:"doConfirm",value:function(e){if(0===this.dom.querySelectorAll(".dirty").length)return!0;if(navigator.userAgent.toLowerCase().match(/msie|chrome/)){if(window.hasUserFormsPropted)return!0;window.hasUserFormsPropted=!0,window.setTimeout(function(){window.hasUserFormsPropted=!1},900)}return e.preventDefault(),void 0!==window.i18n?event.returnValue=window.i18n._t("UserForms.LEAVE_CONFIRMATION","You have unsaved changes!"):event.returnValue="You have unsaved changes!",!0}},{key:"initAreYouSure",value:function(){var e=this.doConfirm.bind(this);this.dom.addEventListener("submit",function(t){window.removeEventListener("beforeunload",e)}),window.addEventListener("beforeunload",e)}}]),e}();document.addEventListener("DOMContentLoaded",function(){document.querySelectorAll("form.userform").forEach(function(e){new f(e).init()})})},"./client/src/bundles/bundle.js":function(e,t,r){"use strict";r("./client/src/bundles/UserForms.js")},"./node_modules/async-validator/dist-web/index.js":function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),function(e){function n(){return n=Object.assign?Object.assign.bind():function(e){for(var t=1;t1?t-1:0),n=1;n=u)return e;switch(e){case"%s":return String(r[i++]);case"%d":return Number(r[i++]);case"%j":try{return JSON.stringify(r[i++])}catch(e){return"[Circular]"}break;default:return e}}):e}function p(e){return"string"===e||"url"===e||"hex"===e||"email"===e||"date"===e||"pattern"===e}function h(e,t){return void 0===e||null===e||!("array"!==t||!Array.isArray(e)||e.length)||!(!p(t)||"string"!=typeof e||e)}function m(e,t,r){function n(e){i.push.apply(i,e||[]),++u===s&&r(i)}var i=[],u=0,s=e.length;e.forEach(function(e){t(e,n)})}function v(e,t,r){function n(s){if(s&&s.length)return void r(s);var o=i;i+=1,o()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,hex:/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i},N={integer:function(e){return N.number(e)&&parseInt(e,10)===e},float:function(e){return N.number(e)&&!N.integer(e)},array:function(e){return Array.isArray(e)},regexp:function(e){if(e instanceof RegExp)return!0;try{return!!new RegExp(e)}catch(e){return!1}},date:function(e){return"function"==typeof e.getTime&&"function"==typeof e.getMonth&&"function"==typeof e.getYear&&!isNaN(e.getTime())},number:function(e){return!isNaN(e)&&"number"==typeof e},object:function(e){return"object"==typeof e&&!N.array(e)},method:function(e){return"function"==typeof e},email:function(e){return"string"==typeof e&&e.length<=320&&!!e.match(j.email)},url:function(e){return"string"==typeof e&&e.length<=2048&&!!e.match(k())},hex:function(e){return"string"==typeof e&&!!e.match(j.hex)}},P=function(e,t,r,n,i){if(e.required&&void 0===t)return void L(e,t,r,n,i);var u=["integer","float","array","regexp","object","method","email","number","date","url","hex"],s=e.type;u.indexOf(s)>-1?N[s](t)||n.push(d(i.messages.types[s],e.fullField,e.type)):s&&typeof t!==e.type&&n.push(d(i.messages.types[s],e.fullField,e.type))},_=function(e,t,r,n,i){var u="number"==typeof e.len,s="number"==typeof e.min,o="number"==typeof e.max,a=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,l=t,c=null,f="number"==typeof t,p="string"==typeof t,h=Array.isArray(t);if(f?c="number":p?c="string":h&&(c="array"),!c)return!1;h&&(l=t.length),p&&(l=t.replace(a,"_").length),u?l!==e.len&&n.push(d(i.messages[c].len,e.fullField,e.len)):s&&!o&&le.max?n.push(d(i.messages[c].max,e.fullField,e.max)):s&&o&&(le.max)&&n.push(d(i.messages[c].range,e.fullField,e.min,e.max))},D=function(e,t,r,n,i){e.enum=Array.isArray(e.enum)?e.enum:[],-1===e.enum.indexOf(t)&&n.push(d(i.messages.enum,e.fullField,e.enum.join(", ")))},I=function(e,t,r,n,i){if(e.pattern)if(e.pattern instanceof RegExp)e.pattern.lastIndex=0,e.pattern.test(t)||n.push(d(i.messages.pattern.mismatch,e.fullField,t,e.pattern));else if("string"==typeof e.pattern){var u=new RegExp(e.pattern);u.test(t)||n.push(d(i.messages.pattern.mismatch,e.fullField,t,e.pattern))}},V={required:L,whitespace:T,type:P,range:_,enum:D,pattern:I},R=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t,"string")&&!e.required)return r();V.required(e,t,n,u,i,"string"),h(t,"string")||(V.type(e,t,n,u,i),V.range(e,t,n,u,i),V.pattern(e,t,n,u,i),!0===e.whitespace&&V.whitespace(e,t,n,u,i))}r(u)},C=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&V.type(e,t,n,u,i)}r(u)},B=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(""===t&&(t=void 0),h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&(V.type(e,t,n,u,i),V.range(e,t,n,u,i))}r(u)},M=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&V.type(e,t,n,u,i)}r(u)},H=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),h(t)||V.type(e,t,n,u,i)}r(u)},U=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&(V.type(e,t,n,u,i),V.range(e,t,n,u,i))}r(u)},$=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&(V.type(e,t,n,u,i),V.range(e,t,n,u,i))}r(u)},z=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if((void 0===t||null===t)&&!e.required)return r();V.required(e,t,n,u,i,"array"),void 0!==t&&null!==t&&(V.type(e,t,n,u,i),V.range(e,t,n,u,i))}r(u)},Y=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&V.type(e,t,n,u,i)}r(u)},J=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i),void 0!==t&&V.enum(e,t,n,u,i)}r(u)},Z=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t,"string")&&!e.required)return r();V.required(e,t,n,u,i),h(t,"string")||V.pattern(e,t,n,u,i)}r(u)},G=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t,"date")&&!e.required)return r();if(V.required(e,t,n,u,i),!h(t,"date")){var s;s=t instanceof Date?t:new Date(t),V.type(e,s,n,u,i),s&&V.range(e,s.getTime(),n,u,i)}}r(u)},W=function(e,t,r,n,i){var u=[],s=Array.isArray(t)?"array":typeof t;V.required(e,t,n,u,i,s),r(u)},K=function(e,t,r,n,i){var u=e.type,s=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t,u)&&!e.required)return r();V.required(e,t,n,s,i,u),h(t,u)||V.type(e,t,n,s,i)}r(s)},Q=function(e,t,r,n,i){var u=[];if(e.required||!e.required&&n.hasOwnProperty(e.field)){if(h(t)&&!e.required)return r();V.required(e,t,n,u,i)}r(u)},X={string:R,method:C,number:B,boolean:M,regexp:H,integer:U,float:$,array:z,object:Y,enum:J,pattern:Z,date:G,url:K,hex:K,email:K,required:W,any:Q},ee=A(),te=function(){function e(e){this.rules=null,this._messages=ee,this.define(e)}var t=e.prototype;return t.define=function(e){var t=this;if(!e)throw new Error("Cannot configure a schema with no rules");if("object"!=typeof e||Array.isArray(e))throw new Error("Rules must be an object");this.rules={},Object.keys(e).forEach(function(r){var n=e[r];t.rules[r]=Array.isArray(n)?n:[n]})},t.messages=function(e){return e&&(this._messages=q(A(),e)),this._messages},t.validate=function(t,r,i){function u(e){for(var t=[],r={},n=0;n1)for(var r=1;rlegend{padding-left:4px;padding-right:4px;border:0;width:auto}.right-title{clear:both;display:block}.checkbox .right-title{display:inline}.userform .left{margin-bottom:5px;font-weight:700} \ No newline at end of file +.userform-progress .progress{position:relative;height:1em;background:#ced5e1}.userform-progress .progress-bar{position:absolute;height:1em;background:#566b8d}.userform-progress .step-buttons{margin-left:0;padding-left:0;width:100%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;position:relative}.userform-progress .step-button-wrapper{display:inline-block;list-style-type:none}.userform-progress .step-button-wrapper.viewed .step-button-jump{opacity:1}.userform-progress .step-button-jump{opacity:.7}.step-navigation .step-buttons{margin-left:0}.step-navigation .step-button-wrapper{display:inline-block;list-style-type:none}.userform{clear:both;width:100%;max-width:100%}.userform .hide{display:none}.userform .field label.right{color:#303b4d}.userformsgroup{border:1px solid #aebace;border-radius:4px;padding:8px;margin-top:12px;margin-bottom:12px}.userformsgroup>legend{padding-left:4px;padding-right:4px;border:0;width:auto}.right-title{clear:both;display:block}.checkbox .right-title{display:inline}.userform .left{margin-bottom:5px;font-weight:700} \ No newline at end of file diff --git a/client/src/bundles/UserForms.js b/client/src/bundles/UserForms.js index aeea167..0288867 100644 --- a/client/src/bundles/UserForms.js +++ b/client/src/bundles/UserForms.js @@ -1,774 +1,577 @@ /** * @file Manages the multi-step navigation. */ +import Schema from 'async-validator'; -import jQuery from 'jquery'; -import i18n from 'i18n'; +const DIRTY_CLASS = 'dirty'; +const FOCUSED_CLASS = 'focused'; -jQuery(document).ready(($) => { - // Settings that come from the CMS. - const CONSTANTS = {}; +function isVisible(element) { + return element.style.display !== 'none' + && element.style.visibility !== 'hidden' + && !element.classList.contains('hide'); +} - // Common functions that extend multiple classes. - const commonMixin = { - /** - * @func show - * @desc Show the form step. Looks after aria attributes too. - */ - show() { - this.$el.attr('aria-hidden', false).show(); - }, - /** - * @func hide - * @desc Hide the form step. Looks after aria attributes too. - */ - hide() { - this.$el.attr('aria-hidden', true).hide(); - }, - }; - - /** - * @func ErrorContainer - * @constructor - * @param {object} element - The error container element. - * @return {object} - The ErrorContainer instance. - * @desc Creates an error container. Used to display step error messages at the top. - */ - function ErrorContainer(element) { - this.$el = element instanceof $ ? element : $(element); - - // Set the error container's heading. - this.$el.find('h4').text(i18n._t('UserForms.ERROR_CONTAINER_HEADER', - 'Please correct the following errors and try again:')); - - return this; +class ProgressBar { + constructor(dom, userForm) { + this.dom = dom; + this.userForm = userForm; + this.progressTitle = this.userForm.dom.querySelector('.progress-title'); + this.buttons = this.dom.querySelectorAll('.step-button-jump'); + this.currentStepNumber = this.dom.querySelector('.current-step-number'); + this.init(); } - /** - * @func hasErrors - * @return boolean - * @desc Checks if the error container has any error messages. - */ - ErrorContainer.prototype.hasErrors = function hasErrors() { - return this.$el.find('.error-list').children().length > 0; - }; - - /** - * @func removeErrorMessage - * @desc Removes an error message from the error container. - */ - ErrorContainer.prototype.removeErrorMessage = function removeErrorMessage(fieldId) { - this.$el.find(`#${fieldId}-top-error`).remove(); - - // If there are no more error then hide the container. - if (!this.hasErrors()) { - this.hide(); - } - }; - - /** - * @func addStepLink - * @param {object} step - FormStep instance. - * @desc Adds a link to a form step as an error message. - */ - ErrorContainer.prototype.addStepLink = function addStepLink(step) { - const userform = this.$el.closest('.userform').data('inst'); - const itemID = `${step.$el.attr('id')}-error-link`; - let $itemElement = this.$el.find(`#${itemID}`); - const stepID = step.$el.attr('id'); - const stepTitle = step.$el.data('title'); - - // If the item already exists we don't need to do anything. - if ($itemElement.length) { - return; - } - - $itemElement = $(`
  • ${stepTitle}
  • `); - - $itemElement.on('click', (e) => { - e.preventDefault(); - userform.jumpToStep(step.id); - }); - - this.$el.find('.error-list').append($itemElement); - }; - - /** - * @func removeStepLink - * @param {object} step - FormStep instance. - * @desc Removes a step link from the error container. - */ - ErrorContainer.prototype.removeStepLink = function removeStepLink(fieldId) { - const stepID = $(`#${fieldId}`).closest('.form-step').attr('id'); - - this.$el.find(`#${stepID}-error-link`).remove(); - - // Hide the error container if we've just removed the last error. - if (this.$el.find('.error-list').is(':empty')) { - this.hide(); - } - }; - - /** - * @func ErrorContainer.updateErrorMessage - * @param {object} $input - The jQuery input object which contains the field to validate. - * @param {object} message - The error message to display (html escaped). - * @desc Update an error message (displayed at the top of the form). - */ - ErrorContainer.prototype.updateErrorMessage = function updateErrorMessage($input, message) { - const inputID = $input.attr('id'); - let anchor = `#${inputID}`; - const elementID = `${inputID}-top-error`; - let messageElement = $(`#${elementID}`); - let describedBy = $input.attr('aria-describedby'); - - // The 'message' param will be an empty string if the field is valid. - if (!message) { - // Style issues as fixed if they already exist - messageElement.addClass('fixed'); - return; - } - - messageElement.removeClass('fixed'); - - this.show(); - - if (messageElement.length === 1) { - // Update the existing error message. - messageElement.show().find('a').html(message); - } else { - // Generate better link to field - $input.closest('.field[id]').each(() => { - const anchorID = $(this).attr('id'); - - if (!anchorID) { - return; - } - - anchor = `#${anchorID}`; - }); - - // Add a new error message - messageElement = $('
  • '); - messageElement - .attr('id', elementID) - .find('a') - .attr('href', location.pathname + location.search + anchor) - .html(message); - - this.$el.find('ul').append(messageElement); - - // Link back to original input via aria - // Respect existing non-error aria-describedby - if (!describedBy) { - describedBy = elementID; - } else if (!describedBy.match(new RegExp(`\\b${elementID}\\b`))) { - // Add to end of list if not already present - describedBy += ` ${elementID}`; - } - - $input.attr('aria-describedby', describedBy); - } - }; - - /** - * @func FormStep - * @constructor - * @param {object} element - * @return {object} - The FormStep instance. - * @desc Creates a form step. - */ - function FormStep(element) { - const self = this; - - this.$el = element instanceof $ ? element : $(element); - - const userform = this.$el.closest('.userform').data('inst'); - - // Find button for this step - this.$elButton = $(`.step-button-wrapper[data-for='${this.$el.prop('id')}']`); - - // Has the step been viewed by the user? - this.viewed = false; - - // Is the form step valid? - // This value is used on form submission, which fails, if any of the steps are invalid. - this.valid = false; - - // The internal id of the step. Used for getting the step from the UserForm.steps array. - this.id = null; - - this.hide(); - - if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) { - this.errorContainer = new ErrorContainer(this.$el.find('.error-container')); - - // Listen for errors on the UserForm. - userform.$el.on('userform.form.error', (e, validator) => { - // The step only cares about errors if it's currently visible. - if (!self.$el.is(':visible')) { - return; - } - - // Add or update each error in the list. - $.each(validator.errorList, (i, error) => { - self.errorContainer.updateErrorMessage($(error.element), error.message); - }); - }); - - // Listen for fields becoming valid - userform.$el.on('userform.form.valid', (e, fieldId) => { - self.errorContainer.removeErrorMessage(fieldId); - }); - } - - // Ensure that page visibilty updates the step navigation - this - .$elButton - .on('userform.field.hide userform.field.show', () => { - userform.$el.trigger('userform.form.conditionalstep'); - }); - - return this; - } - - /** - * Determine if this step is conditionally disabled - * - * @returns {Boolean} - */ - // Because the element itself could be visible but 0 height, so check visibility of button - FormStep.prototype.conditionallyHidden = function conditionallyHidden() { - return !this.$elButton.find('button').is(':visible'); - }; - - /** - * @func ProgressBar - * @constructor - * @param {object} element - * @return {object} - The Progress bar instance. - * @desc Creates a progress bar. - */ - function ProgressBar(element) { - const self = this; - - this.$el = element instanceof $ ? element : $(element); - this.$buttons = this.$el.find('.step-button-jump'); - this.$jsAlign = this.$el.find('.js-align'); - const userform = this.$el.closest('.userform').data('inst'); - - // Update the progress bar when 'step' buttons are clicked. - this.$buttons.each((i, stepButton) => { - $(stepButton).on('click', (e) => { + init() { + this.dom.style.display = 'initial'; + const buttons = this.buttons; + buttons.forEach((button) => { + button.addEventListener('click', (e) => { e.preventDefault(); - const stepNumber = parseInt($(e.target).data('step'), 10); - self.$el.trigger('userform.progress.changestep', stepNumber); + const stepNumber = parseInt(button.getAttribute('data-step'), 10); + this.userForm.jumpToStep(stepNumber - 1); + return false; }); }); - - // Update the progress bar when 'prev' and 'next' buttons are clicked. - userform.$el.on('userform.form.changestep', (e, stepID) => { - self.update(stepID); + this.userForm.dom.addEventListener('userform.form.changestep', (e) => { + this.update(e.detail.stepId); }); - - // Listen for steps being conditionally shown / hidden by display rules. - // We need to update step related UI like the number of step buttons - // and any text that shows the total number of steps. - userform.$el.on('userform.form.conditionalstep', () => { - // Update the step numbers on the buttons. - const $visibleButtons = self.$buttons.filter(':visible'); - - $visibleButtons.each((i, button) => { - $(button).text(i + 1); - }); - - // Update the actual progress bar. - self.$el.find('.progress-bar').attr('aria-valuemax', $visibleButtons.length); - - // Update any text that uses the total number of steps. - self.$el.find('.total-step-number').text($visibleButtons.length); - }); - - // Spaces out the steps below progress bar evenly - this.$jsAlign.each((index, button) => { - const $button = $(button); - const leftPercent = (100 / (self.$jsAlign.length - 1)) * index; - const leftPercentCssValue = `${leftPercent}%`; - const buttonOffset = -1 * ($button.innerWidth() / 2); - - $button.css({ - left: leftPercentCssValue, - marginLeft: buttonOffset, - }); - - // First and last buttons are kept within userform-progress container - if (index === self.$jsAlign.length - 1) { - $button.css({ marginLeft: buttonOffset * 2 }); - } else if (index === 0) { - $button.css({ marginLeft: 0 }); - } - }); - - return this; + this.update(0); } - /** - * @func ProgressBar.update - * @param {number} stepID - Zero based index of the new step. - * @desc Update the progress element to show a new step. - */ - ProgressBar.prototype.update = function update(stepID) { - const $newStepElement = $(this.$el.parent('.userform').find('.form-step')[stepID]); - let stepNumber = 0; - let barWidth = (stepID / (this.$buttons.length - 1)) * 100; + update(stepId) { + const stepNumber = this.userForm.getCurrentStepID() + 1; + const newStep = this.userForm.getStep(stepId); + const newStepElement = newStep.step; + let barWidth = (stepId / (this.buttons.length - 1)) * 100; - // Set the current step number. - this.$buttons.each((i, button) => { - if (i > stepID) { - // Break the loop - return false; + this.currentStepNumber.innerText = stepNumber; + + this.dom.querySelectorAll('[aria-valuenow]').forEach((e) => { + e.setAttribute('aria-valuenow', stepNumber); + }); + + this.buttons.forEach((button) => { + const btn = button; + const parent = btn.parentNode; + if (parseInt(btn.getAttribute('data-step'), 10) === stepNumber + && isVisible(btn)) { + parent.classList.add('current'); + parent.classList.add('viewed'); + + btn.disabled = false; } - - if ($(button).is(':visible')) { - stepNumber += 1; - } - return true; + parent.classList.remove('current'); }); - // Update elements that contain the current step number. - this.$el.find('.current-step-number').each((i, element) => { - $(element).text(stepNumber); - }); - - // Update aria attributes. - this.$el.find('[aria-valuenow]').each((i, element) => { - $(element).attr('aria-valuenow', stepNumber); - }); - - // Update the CSS classes on step buttons. - this.$buttons.each((i, element) => { - const $element = $(element); - const $item = $element.parent(); - - if (parseInt($element.data('step'), 10) === stepNumber && $element.is(':visible')) { - $item.addClass('current viewed'); - $element.removeAttr('disabled'); - - return; - } - - $item.removeClass('current'); - }); - - // Update the progress bar's title with the new step's title. - this.$el.siblings('.progress-title').text($newStepElement.data('title')); + this.progressTitle.innerText = newStepElement.getAttribute('data-title'); // Update the width of the progress bar. barWidth = barWidth ? `${barWidth}%` : ''; - this.$el.find('.progress-bar').width(barWidth); - }; + this.dom.querySelector('.progress-bar').style.width = barWidth; + } +} - /** - * @func FormActions - * @constructor - * @param {object} element - * @desc Creates the navigation and actions (Prev, Next, Submit buttons). - */ - function FormActions(element) { - const self = this; +class FormStep { + constructor(step, userForm) { + this.step = step; + this.userForm = userForm; + this.viewed = false; + this.buttonHolder = null; + this.id = 0; - this.$el = element instanceof $ ? element : $(element); - const $elFormItself = this.$el.closest('.userform'); - - this.userformInstance = $elFormItself.data('inst'); - - this.$prevButton = this.$el.find('.step-button-prev'); - this.$nextButton = this.$el.find('.step-button-next'); - - // Show the buttons. - this.$prevButton.parent().attr('aria-hidden', false).show(); - this.$nextButton.parent().attr('aria-hidden', false).show(); - - // Scroll up to the next page... - const scrollUpFx = function () { - const scrollTop = $elFormItself.offset(); - $('html, body').animate({ scrollTop: scrollTop.top }, 'slow'); - }; - - // Bind the step navigation event listeners. - this.$prevButton.on('click', (e) => { - e.preventDefault(); - scrollUpFx(); - self.$el.trigger('userform.action.prev'); - }); - this.$nextButton.on('click', (e) => { - e.preventDefault(); - scrollUpFx(); - self.$el.trigger('userform.action.next'); - }); - - // Listen for changes to the current form step, or conditional pages, - // so we can show hide buttons appropriately. - this.userformInstance.$el.on('userform.form.changestep userform.form.conditionalstep', () => { - self.update(); - }); - - return this; + this.init(); } - /** - * @func FormActions.update - * @param {number} stepID - Zero based ID of the current step. - * @desc Updates the form actions element to reflect the current state of the page. - */ - FormActions.prototype.update = function update() { - const numberOfSteps = this.userformInstance.steps.length; - const stepID = this.userformInstance.currentStep ? this.userformInstance.currentStep.id : 0; + init() { + const id = this.getHTMLId(); + this.buttonHolder = document.querySelector(`.step-button-wrapper[data-for='${id}']`); + if (this.buttonHolder) { + ['userform.field.hide', 'userform.field.show'].forEach((action) => { + this.buttonHolder.addEventListener(action, () => { + this.userForm.dom.trigger('userform.form.conditionalstep'); + }); + }); + } + } + + setId(id) { + this.id = id; + } + + getHTMLId() { + return this.step.getAttribute('id'); + } + + show() { + this.step.setAttribute('aria-hidden', false); + this.step.classList.remove('hide'); + this.step.classList.add('viewed'); + this.viewed = true; + } + + hide() { + this.step.setAttribute('aria-hidden', true); + this.step.classList.add('hide'); + } + + conditionallyHidden() { + const button = this.buttonHolder.querySelector('button'); + return !(button.style.display !== 'none' && button.visibility !== 'hidden' && !button.classList.contains('hide')); + } + + + getValidatorType(input) { + if (input.getAttribute('type') === 'email') { + return 'email'; + } + if (input.getAttribute('type') === 'date') { + return 'date'; + } + if (input.getAttribute('type') === 'file') { + return 'object'; + } + if (input.classList.contains('numeric') || input.getAttribute('type') === 'numeric') { + return 'number'; + } + return 'string'; + } + + getValidatorMessage(input) { + if (input.getAttribute('data-msg-required')) { + return input.getAttribute('data-msg-required'); + } + return `${this.getFieldLabel(input)} is required`; + } + + getHolderForField(input) { + return window.closest(input, '.field'); + } + + getFieldLabel(input) { + const holder = this.getHolderForField(input); + if (holder) { + const label = holder.querySelector('label.left, legend.left'); + if (label) { + return label.innerText; + } + } + return input.getAttribute('name'); + } + + isInputNumeric(input) { + return this.getValidatorType(input) === 'number'; + } + + isInputFile(input) { + return input.getAttribute('type') === 'file'; + } + + getInputByName(name) { + return this.step.querySelector(`input[name="${name}"]`); + } + + getValidationsDescriptors(onlyDirty) { + const descriptors = {}; + const fields = this.step.querySelectorAll('input, textarea, select'); + + fields.forEach((field) => { + if (isVisible(field) + && (!onlyDirty || (onlyDirty && field.classList.contains(FOCUSED_CLASS))) + ) { + const label = this.getFieldLabel(field); + const holder = this.getHolderForField(field); + + descriptors[field.getAttribute('name')] = { + title: label, + type: this.getValidatorType(field), + required: holder.classList.contains('requiredField'), + message: this.getValidatorMessage(field) + }; + + const min = field.getAttribute('data-rule-min'); + const max = field.getAttribute('data-rule-max'); + if (min !== null || max !== null) { + descriptors[field.getAttribute('name')].asyncValidator = function numericValidator(rule, value) { + return new Promise((resolve, reject) => { + if (min !== null && value < min) { + reject(`${label} cannot be less than ${min}`); + } else if (max !== null && value > max) { + reject(`${label} cannot be greater than ${max}`); + } else { + resolve(); + } + }); + }; + } + + const minL = field.getAttribute('data-rule-minlength'); + const maxL = field.getAttribute('data-rule-maxlength'); + if (minL !== null || maxL !== null) { + descriptors[field.getAttribute('name')].asyncValidator = function lengthValidator(rule, value) { + return new Promise((resolve, reject) => { + if (minL !== null && value.length < minL) { + reject(`${label} cannot be shorter than ${minL}`); + } else if (maxL !== null && value.length > maxL) { + reject(`${label} cannot be longer than ${maxL}`); + } else { + resolve(); + } + }); + }; + } + } + }); + return descriptors; + } + + validate(onlyDirty) { + const descriptors = this.getValidationsDescriptors(onlyDirty); + if (Object.keys(descriptors).length) { + const validator = new Schema(descriptors); + + const formData = new FormData(this.userForm.dom); + const data = {}; + formData.forEach((value, key) => { + let sanitised = value; + const input = this.getInputByName(key); + if (sanitised && input && this.isInputNumeric(input)) { + sanitised = parseFloat(sanitised); // because FormData reads all the values as strings + } + data[key] = sanitised; + }); + + // now check for unselected checkboxes and radio buttons + const selectableFields = this.step.querySelectorAll('input[type="radio"],input[type="checkbox"]'); + selectableFields.forEach((selectableField) => { + const fieldName = selectableField.getAttribute('name'); + if (typeof data[fieldName] === 'undefined') { + data[fieldName] = ''; + } + }); + + const promise = new Promise((resolve, reject) => { + validator.validate(data, (errors) => { + if (errors && errors.length) { + this.displayErrorMessages(errors); + reject(errors); + } else { + this.displayErrorMessages([]); + resolve(); + } + }); + }); + return promise; + } + + const promise = new Promise((resolve) => { + resolve(); + }); + return promise; + } + + enableLiveValidation() { + const fields = this.step.querySelectorAll('input, textarea, select'); + fields.forEach((field) => { + field.addEventListener('focusin', () => { + field.classList.add(FOCUSED_CLASS); + }); + + field.addEventListener('change', () => { + field.classList.add(DIRTY_CLASS); + }); + + field.addEventListener('focusout', () => { + this.validate(true).then(() => { + }).catch(() => { + }); + }); + }); + } + + displayErrorMessages(errors) { + const errorIds = []; + + errors.forEach((error) => { + const fieldHolder = this.userForm.dom.querySelector(`#${error.field}`); + if (fieldHolder) { + let errorLabel = fieldHolder.querySelector('span.error'); + if (!errorLabel) { + errorLabel = document.createElement('span'); + errorLabel.classList.add('error'); + errorLabel.setAttribute('data-id', error.field); + } + errorIds.push(error.field); + errorLabel.innerHTML = error.message; + fieldHolder.append(errorLabel); + } + }); + + // remove any thats not required + const messages = this.step.querySelectorAll('span.error'); + + messages.forEach((mesasge) => { + const id = mesasge.getAttribute('data-id'); + if (errorIds.indexOf(id) === -1) { + mesasge.remove(); + } + }); + } +} + +class FormActions { + constructor(dom, userForm) { + this.dom = dom; + this.userForm = userForm; + this.prevButton = dom.querySelector('.step-button-prev'); + this.nextButton = dom.querySelector('.step-button-next'); + + this.init(); + } + + init() { + this.prevButton.addEventListener('click', (e) => { + e.preventDefault(); + // scrollUpFx(); + window.triggerDispatchEvent(this.userForm.dom, 'userform.action.prev'); + }); + this.nextButton.addEventListener('click', (e) => { + e.preventDefault(); + // scrollUpFx(); + window.triggerDispatchEvent(this.userForm.dom, 'userform.action.next'); + }); + + this.update(); + + this.userForm.dom.addEventListener('userform.form.changestep', () => { + this.update(); + }); + + this.userForm.dom.addEventListener('userform.form.conditionalstep', () => { + this.update(); + }); + } + + update() { + const numberOfSteps = this.userForm.getNumberOfSteps(); + const stepId = this.userForm.getCurrentStepID(); let i = null; let lastStep = null; - - // Update the "Prev" button. - this.$el.find('.step-button-prev')[stepID === 0 ? 'hide' : 'show'](); - - // Find last step, skipping hidden ones for (i = numberOfSteps - 1; i >= 0; i--) { - lastStep = this.userformInstance.steps[i]; - - // Skip if step is hidden + lastStep = this.userForm.getStep(i); if (!lastStep.conditionallyHidden()) { - // Update the "Next" button. - this.$el.find('.step-button-next')[stepID >= i ? 'hide' : 'show'](); + if (stepId >= i) { + this.nextButton.parentNode.classList.add('hide'); + } else { + this.nextButton.parentNode.classList.remove('hide'); + } - // Update the "Actions". - this.$el.find('.btn-toolbar')[stepID >= i ? 'show' : 'hide'](); + if (stepId > 0 && stepId <= i) { + this.prevButton.parentNode.classList.remove('hide'); + } else { + this.prevButton.parentNode.classList.add('hide'); + } + + if (stepId >= i) { + this.dom.querySelector('.btn-toolbar').classList.remove('hide'); + } else { + this.dom.querySelector('.btn-toolbar').classList.add('hide'); + } - // Stop processing last step break; } } - }; + } +} - /** - * @func UserForm - * @constructor - * @param {object} element - * @return {object} - The UserForm instance. - * @desc The form - */ - function UserForm(element) { - const self = this; - - this.$el = element instanceof $ ? element : $(element); +class UserForm { + constructor(form) { + this.dom = form; + this.CONSTANTS = {}; // Settings that come from the CMS. this.steps = []; + this.progressBar = null; + this.actions = null; + this.currentStep = null; - // Add an error container which displays a list of invalid steps on form submission. - this.errorContainer = new ErrorContainer(this.$el.children('.error-container')); - - // Listen for events triggered by form steps. - this.$el.on('userform.action.prev', () => { - self.prevStep(); - }); - this.$el.on('userform.action.next', () => { - self.nextStep(); - }); - - // Listen for events triggered by the progress bar. - this.$el.find('.userform-progress').on('userform.progress.changestep', (e, stepNumber) => { - self.jumpToStep(stepNumber - 1); - }); - - // When a field becomes valid, remove errors from the error container. - this.$el.on('userform.form.valid', (e, fieldId) => { - self.errorContainer.removeStepLink(fieldId); - }); - - this.$el.validate(this.validationOptions); - - // Ensure checkbox groups are validated correctly - this.$el.find('.optionset.requiredField input').each((a, field) => { - $(field).rules('add', { - required: true, - }); - }); - - return this; + this.CONSTANTS.ENABLE_LIVE_VALIDATION = this.dom.getAttribute('livevalidation') !== undefined; + this.CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP = this.dom.getAttribute('toperrors') !== undefined; + this.CONSTANTS.ENABLE_ARE_YOU_SURE = this.dom.getAttribute('enableareyousure') !== undefined; } - /* - * Default options for step validation. These get extended in main(). - */ - UserForm.prototype.validationOptions = { - ignore: ':hidden,ul', - errorClass: 'error', - errorElement: 'span', - errorPlacement: (error, element) => { - error.addClass('message'); + init() { + this.initialiseFormSteps(); - if (element.is(':radio') || element.parents('.checkboxset').length > 0) { - error.appendTo(element.closest('.middleColumn, .field')); - } else if (element.parents('.checkbox').length > 0) { - error.appendTo(element.closest('.field')); - } else { - error.insertAfter(element); + if (this.CONSTANTS.ENABLE_ARE_YOU_SURE) { + this.initAreYouSure(); + } + } + + initialiseFormSteps() { + const steps = this.dom.querySelectorAll('.form-step'); + + steps.forEach((stepDom) => { + const step = new FormStep(stepDom, this); + step.hide(); + this.addStep(step); + if (this.CONSTANTS.ENABLE_LIVE_VALIDATION) { + step.enableLiveValidation(); } - }, - invalidHandler: (event, validator) => { - // setTimeout 0 so it runs after errorPlacement - setTimeout(() => { - validator.currentElements.filter('.error').first().focus(); - }, 0); - }, - // Callback for handling the actual submit when the form is valid. - // Submission in the jQuery.validate sence is handled at step level. - // So when the final step is submitted we have to also check all previous steps are valid. - submitHandler: (form) => { - let isValid = true; - const userform = $(form).closest('.userform').data('inst'); + }); - // Validate the current step - if (userform.currentStep) { - userform.currentStep.valid = $(form).valid(); - } + this.setCurrentStep(this.steps[0]); - // Check for invalid previous steps. - $.each(userform.steps, (i, step) => { - if (!step.valid && !step.conditionallyHidden()) { - isValid = false; - userform.errorContainer.addStepLink(step); + const progressBarDom = this.dom.querySelector('.userform-progress'); + if (progressBarDom) { + this.progressBar = new ProgressBar(progressBarDom, this); + } + + const stepNavigation = this.dom.querySelector('.step-navigation'); + if (stepNavigation) { + this.formActions = new FormActions(stepNavigation, this); + this.formActions.update(); + } + + this.setUpPing(); + + this.dom.addEventListener('userform.action.next', () => { + this.nextStep(); + }); + + this.dom.addEventListener('userform.action.prev', () => { + this.prevStep(); + }); + + this.dom.addEventListener('submit', (e) => { + this.validateForm(e); + }); + } + + + validateForm(e) { + e.preventDefault(); + this.currentStep.validate() + .then((errors) => { + if (!errors) { + this.dom.submit(); } - }); + }) + .catch(() => {}); + } - if (isValid) { - // Remove required attributes on hidden fields - const hiddenInputs = $(form).find('.field.requiredField.hide input'); - if (hiddenInputs.length > 0) { - hiddenInputs.removeAttr('required aria-required data-rule-required').valid(); - } - - // When using the "are you sure?" plugin, ensure the form immediately submits. - $(form).removeClass('dirty'); - - form.submit(); - userform.$el.trigger('userform.form.submit'); - } else { - userform.errorContainer.show(); - } - }, - // When a field becomes valid. - success: (error) => { - const userform = $(error).closest('.userform').data('inst'); - const errorId = $(error).attr('id'); - const fieldId = errorId.substr(0, errorId.indexOf('-error')).replace(/[\\[\\]]/, ''); - - // Remove square brackets since jQuery.validate.js uses idOrName, - // which breaks further on when using a selector that end with - // square brackets. - - error.remove(); - - // Pass the field's ID with the event - userform.$el.trigger('userform.form.valid', [fieldId]); - }, - }; - - /** - * @func UserForm.addStep - * @param {object} step - An instance of FormStep. - * @desc Adds a step to the UserForm. - */ - UserForm.prototype.addStep = function addStep(step) { + setCurrentStep(step) { // Make sure we're dealing with a form step. if (!(step instanceof FormStep)) { return; } - - // eslint-disable-next-line no-param-reassign - step.id = this.steps.length; - - this.steps.push(step); - }; - - /** - * @func UserForm.setCurrentStep - * @param {object} step - An instance of FormStep. - * @desc Sets the step the user is currently on. - */ - UserForm.prototype.setCurrentStep = function setCurrentStep(step) { - // Make sure we're dealing with a form step. - if (!(step instanceof FormStep)) { - return; - } - this.currentStep = step; this.currentStep.show(); + } - // Record the user has viewed the step. - this.currentStep.viewed = true; - this.currentStep.$el.addClass('viewed'); - }; + addStep(step) { + if (!(step instanceof FormStep)) { + return; + } + step.setId(this.steps.length); + this.steps.push(step); + } - /** - * @func UserForm.jumpToStep - * @param {number} stepNumber - * @param {boolean} [direction] - Defaults to forward (true). - * @desc Jumps to a specific form step. - */ - UserForm.prototype.jumpToStep = function jumpToStep(stepNumber, direction) { + getNumberOfSteps() { + return this.steps.length; + } + + getCurrentStepID() { + return this.currentStep.id ? this.currentStep.id : 0; + } + + getStep(index) { + return this.steps[index]; + } + + nextStep() { + this.currentStep.validate().then(() => { + this.jumpToStep(this.steps.indexOf(this.currentStep) + 1, true); + }).catch(() => {}); + } + + prevStep() { + this.jumpToStep(this.steps.indexOf(this.currentStep) - 1, true); + } + + jumpToStep(stepNumber, direction) { const targetStep = this.steps[stepNumber]; - let isValid = false; const forward = direction === undefined ? true : direction; - // Make sure the target step exists. if (targetStep === undefined) { return; } - // Make sure the step we're trying to set as current is not - // hidden by custom display rules. If it is then jump to the next step. if (targetStep.conditionallyHidden()) { if (forward) { this.jumpToStep(stepNumber + 1, direction); } else { this.jumpToStep(stepNumber - 1, direction); } - return; } - // Validate the form. - // This well effectivly validate the current step and not the entire form. - // This is because hidden fields are excluded from validation, and all fields - // on all other steps, are currently hidden. - isValid = this.$el.valid(); - - // Set the 'valid' property on the current step. - this.currentStep.valid = isValid; - - // Users can navigate to step's they've already viewed even if the current step is invalid. - if (isValid === false && targetStep.viewed === false) { - return; + if (this.currentStep) { + this.currentStep.hide(); } - this.currentStep.hide(); this.setCurrentStep(targetStep); - this.$el.trigger('userform.form.changestep', [targetStep.id]); - }; - - /** - * @func UserForm.nextStep - * @desc Advances the form to the next step. - */ - UserForm.prototype.nextStep = function nextStep() { - this.jumpToStep(this.steps.indexOf(this.currentStep) + 1, true); - }; - - /** - * @func UserForm.prevStep - * @desc Goes back one step (not bound to browser history). - */ - UserForm.prototype.prevStep = function prevStep() { - this.jumpToStep(this.steps.indexOf(this.currentStep) - 1, false); - }; - - /** - * @func main - * @desc Bootstraps the front-end. - */ - function main(index, userformElement) { - const $userform = $(userformElement); - - // If there's no userform, do nothing. - if ($userform.length === 0) { - return; - } - - CONSTANTS.ENABLE_LIVE_VALIDATION = $userform.data('livevalidation') !== undefined; - CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP = $userform.data('toperrors') !== undefined; - - // Extend the default validation options with conditional options - // that are set by the user in the CMS. - if (CONSTANTS.ENABLE_LIVE_VALIDATION === false) { - $.extend(UserForm.prototype.validationOptions, { - onfocusout: false, - }); - } - - if (CONSTANTS.DISPLAY_ERROR_MESSAGES_AT_TOP) { - $.extend(UserForm.prototype.validationOptions, { - // Callback for custom code when an invalid form / step is submitted. - invalidHandler: (event, validator) => { - $userform.trigger('userform.form.error', [validator]); - }, - onfocusout: false, - }); - } - - // Display all the things that are hidden when JavaScript is disabled. - $userform.find('.userform-progress, .step-navigation').attr('aria-hidden', false).show(); - - // Extend classes with common functionality. - $.extend(FormStep.prototype, commonMixin); - $.extend(ErrorContainer.prototype, commonMixin); - - const userform = new UserForm($userform); - $userform.data('inst', userform); - - // Conditionally hide field labels and use HTML5 placeholder instead. - if (CONSTANTS.HIDE_FIELD_LABELS) { - $userform.find('label.left').each(() => { - const $label = $(this); - - $(`[name="${$label.attr('for')}"]`).attr('placeholder', $label.text()); - $label.remove(); - }); - } - - // Initialise the form steps. - userform.$el.find('.form-step').each((i, element) => { - const step = new FormStep(element); - - userform.addStep(step); + window.triggerDispatchEvent(this.dom, 'userform.form.changestep', { + stepId: targetStep.id }); - - userform.setCurrentStep(userform.steps[0]); - - // Initialise actions and progressbar - const $progressEl = $userform.find('.userform-progress'); - if ($progressEl.length) { - const progressBar = new ProgressBar($progressEl); - progressBar.update(0); - } - - const $formActionsEl = $userform.find('.step-navigation'); - if ($formActionsEl.length) { - const formActions = new FormActions($formActionsEl); - formActions.update(); - } - - // Enable jQuery UI datepickers - $(document).on('click', 'input.text[data-showcalendar]', () => { - const $element = $(this); - - $element.ssDatepicker(); - - if ($element.data('datepicker')) { - $element.datepicker('show'); - } - }); - - // Make sure the form doesn't expire on the user. Pings every 3 mins. - setInterval(() => { - $.ajax({ url: 'UserDefinedFormController/ping' }); - }, 180 * 1000); - - // Bind a confirmation message when navigating away from a partially completed form. - if (typeof $userform.areYouSure !== 'undefined') { - $userform.areYouSure({ - message: i18n._t('UserForms.LEAVE_CONFIRMATION', 'You have unsaved changes!'), - }); - } } - $('.userform').each(main); + setUpPing() { + // Make sure the form doesn't expire on the user. Pings every 3 mins. + window.setInterval(() => { + fetch('UserDefinedFormController/ping'); + }, 180 * 1000); + } + + doConfirm(e) { + const dirtyFields = this.dom.querySelectorAll(`.${DIRTY_CLASS}`); + if (dirtyFields.length === 0) { + return true; + } + if (navigator.userAgent.toLowerCase().match(/msie|chrome/)) { + if (window.hasUserFormsPropted) { + return true; + } + window.hasUserFormsPropted = true; + window.setTimeout( + () => { + window.hasUserFormsPropted = false; + }, + 900 + ); + } + e.preventDefault(); + if (typeof window.i18n !== 'undefined') { + event.returnValue = window.i18n._t('UserForms.LEAVE_CONFIRMATION', 'You have unsaved changes!'); + } else { + event.returnValue = 'You have unsaved changes!'; + } + return true; + } + + initAreYouSure() { + const confirmFunction = this.doConfirm.bind(this); + this.dom.addEventListener('submit', (e) => { + window.removeEventListener('beforeunload', confirmFunction); + }) + window.addEventListener('beforeunload', confirmFunction); + } +} + + +document.addEventListener('DOMContentLoaded', () => { + const forms = document.querySelectorAll('form.userform'); + forms.forEach((form) => { + const userForm = new UserForm(form); + userForm.init(); + }); }); diff --git a/client/src/styles/userforms.scss b/client/src/styles/userforms.scss index e612cd4..8047335 100644 --- a/client/src/styles/userforms.scss +++ b/client/src/styles/userforms.scss @@ -15,6 +15,10 @@ .step-buttons { margin-left: 0; + padding-left: 0; + width: 100%; + display: flex; + justify-content: space-between; position: relative; } @@ -28,8 +32,6 @@ } .step-button-jump { - position: absolute; - top: 0; opacity: .7; } } diff --git a/code/Control/UserDefinedFormController.php b/code/Control/UserDefinedFormController.php index 8d838dc..a891025 100644 --- a/code/Control/UserDefinedFormController.php +++ b/code/Control/UserDefinedFormController.php @@ -79,22 +79,11 @@ class UserDefinedFormController extends PageController // load the jquery if (!$page->config()->get('block_default_userforms_js')) { - Requirements::javascript('silverstripe/userforms:client/dist/js/jquery.min.js'); - Requirements::javascript( - 'silverstripe/userforms:client/dist/js/jquery-validation/jquery.validate.min.js' - ); Requirements::javascript('silverstripe/admin:client/dist/js/i18n.js'); Requirements::add_i18n_javascript('silverstripe/userforms:client/lang'); Requirements::javascript('silverstripe/userforms:client/dist/js/userforms.js'); $this->addUserFormsValidatei18n(); - - // Bind a confirmation message when navigating away from a partially completed form. - if ($page::config()->get('enable_are_you_sure')) { - Requirements::javascript( - 'silverstripe/userforms:client/dist/js/jquery.are-you-sure/jquery.are-you-sure.js' - ); - } } } @@ -177,10 +166,16 @@ class UserDefinedFormController extends PageController */ public function Form() { + $page = $this->data(); $form = UserForm::create($this, 'Form_' . $this->ID); /** @skipUpgrade */ $form->setFormAction(Controller::join_links($this->Link(), 'Form')); $this->generateConditionalJavascript(); + + if ($page::config()->get('enable_are_you_sure')) { + $form->setAttribute('enableareyousure', 1); + } + return $form; } @@ -212,14 +207,35 @@ class UserDefinedFormController extends PageController $rules .= $this->buildWatchJS($watch); } + // add the custom scripts thats used by the steps. + Requirements::customScript(<<ID); } @@ -626,10 +642,11 @@ JS */ protected function buildWatchJS($watch) { + $result = ''; foreach ($watch as $key => $rule) { - $events = implode(' ', $rule['events']); - $selectors = implode(', ', $rule['selectors']); + $events = implode(',', $rule['events']); + $selectors = implode(',', $rule['selectors']); $conjunction = $rule['conjunction']; $operations = implode(" {$conjunction} ", $rule['operations']); $target = $rule['targetFieldID']; @@ -638,16 +655,21 @@ JS $result .= <<isCheckBoxField(); $radioField = $formFieldWatch->isRadioField(); - $target = sprintf('$("%s")', $formFieldWatch->getSelectorFieldOnly()); + $target = $formFieldWatch->getSelectorFieldOnly(); $fieldValue = Convert::raw2js($this->FieldValue); $conditionOptions = [ @@ -174,7 +174,9 @@ class EditableCustomRule extends DataObject switch ($this->ConditionOption) { case 'IsNotBlank': case 'IsBlank': - $expression = ($checkboxField || $radioField) ? "!{$target}.is(\":checked\")" : "{$target}.val() == ''"; + $expression = ($checkboxField || $radioField) + ? sprintf("document.querySelector(\"%s:checked\") !== null", $target) + : "document.querySelector(\"{$target}\").value == ''"; if ((string) $this->ConditionOption === 'IsNotBlank') { //Negate $expression = "!({$expression})"; @@ -185,9 +187,9 @@ class EditableCustomRule extends DataObject if ($checkboxField) { if ($formFieldWatch->isCheckBoxGroupField()) { $expression = sprintf( - "$.inArray('%s', %s.filter(':checked').map(function(){ return $(this).val();}).get()) > -1", + '[...document.querySelectorAll("%s:checked")].map(function(i) { return i ? i.getAttribute("value") : null; }).indexOf(\'%s\') > -1', + $target, $fieldValue, - $target ); } else { $expression = "{$target}.prop('checked')"; @@ -195,12 +197,12 @@ class EditableCustomRule extends DataObject } elseif ($radioField) { // We cannot simply get the value of the radio group, we need to find the checked option first. $expression = sprintf( - '%s.closest(".field, .control-group").find("input:checked").val() == "%s"', + 'closest(document.querySelector("%s"), ".field, .control-group").querySelector("input:checked").value == "%s"', $target, $fieldValue ); } else { - $expression = sprintf('%s.val() == "%s"', $target, $fieldValue); + $expression = sprintf('document.querySelector("%s").value == "%s"', $target, $fieldValue); } if ((string) $this->ConditionOption === 'ValueNot') { @@ -213,7 +215,7 @@ class EditableCustomRule extends DataObject case 'ValueGreaterThan': case 'ValueGreaterThanEqual': $expression = sprintf( - '%s.val() %s parseFloat("%s")', + 'document.querySelector("%s").value %s parseFloat("%s")', $target, $conditionOptions[$this->ConditionOption], $fieldValue @@ -299,11 +301,11 @@ class EditableCustomRule extends DataObject */ public function toggleDisplayText($initialState, $invert = false) { - $action = strtolower($initialState ?? '') === 'hide' ? 'removeClass' : 'addClass'; + $action = strtolower($initialState ?? '') === 'hide' ? 'remove' : 'add'; if ($invert) { - $action = $action === 'removeClass' ? 'addClass' : 'removeClass'; + $action = $action === 'remove' ? 'add' : 'remove'; } - return sprintf('%s("hide")', $action); + return sprintf('classList.%s("hide")', $action); } /** diff --git a/code/Model/EditableFormField.php b/code/Model/EditableFormField.php index 4f60370..70ebe04 100755 --- a/code/Model/EditableFormField.php +++ b/code/Model/EditableFormField.php @@ -868,7 +868,7 @@ class EditableFormField extends DataObject */ public function getSelectorHolder() { - return sprintf('$("%s")', $this->getSelectorOnly()); + return sprintf('document.querySelector("%s")', $this->getSelectorOnly()); } /** @@ -891,7 +891,7 @@ class EditableFormField extends DataObject */ public function getSelectorField(EditableCustomRule $rule, $forOnLoad = false) { - return sprintf("$(%s)", $this->getSelectorFieldOnly()); + return sprintf("document.querySelector(%s)", $this->getSelectorFieldOnly()); } /** diff --git a/code/Model/EditableFormField/EditableFieldGroup.php b/code/Model/EditableFormField/EditableFieldGroup.php index 93d46c3..a505c5d 100644 --- a/code/Model/EditableFormField/EditableFieldGroup.php +++ b/code/Model/EditableFormField/EditableFieldGroup.php @@ -48,7 +48,7 @@ class EditableFieldGroup extends EditableFormField public function getCMSFields() { $this->beforeUpdateCMSFields(function (FieldList $fields) { - $fields->removeByName(['MergeField', 'Default', 'Validation', 'DisplayRules']); + $fields->removeByName(['MergeField', 'Default', 'Validation']); }); return parent::getCMSFields(); @@ -98,5 +98,10 @@ class EditableFieldGroup extends EditableFormField if ($this->ExtraClass) { $field->addExtraClass($this->ExtraClass); } + + // if ShowOnLoad is false hide the field + if (!$this->ShowOnLoad) { + $field->addExtraClass($this->ShowOnLoadNice()); + } } } diff --git a/package.json b/package.json index 050be44..b8b40ea 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,10 @@ "node-dir": "^0.1.17" }, "dependencies": { - "babel-preset-es2016": "^6.24.1", "jquery": "^3.5.0", - "jquery-validation": "^1.19.5", - "jquery.are-you-sure": "^1.9.0", + "babel-preset-es2016": "^6.24.1", + "validator": "13.9.0", + "async-validator": "4.2.5", "mime": "^1.4.1", "qs": "^6.9.4", "react": "^16.13.1", diff --git a/templates/SilverStripe/UserForms/Form/Includes/UserFormProgress.ss b/templates/SilverStripe/UserForms/Form/Includes/UserFormProgress.ss index 5bfb843..96210a7 100644 --- a/templates/SilverStripe/UserForms/Form/Includes/UserFormProgress.ss +++ b/templates/SilverStripe/UserForms/Form/Includes/UserFormProgress.ss @@ -1,19 +1,21 @@ <% if $Steps.Count > 1 %>

    <% end_if %> diff --git a/templates/SilverStripe/UserForms/Form/Includes/UserFormStepNav.ss b/templates/SilverStripe/UserForms/Form/Includes/UserFormStepNav.ss index f7c9440..0b4fd6b 100644 --- a/templates/SilverStripe/UserForms/Form/Includes/UserFormStepNav.ss +++ b/templates/SilverStripe/UserForms/Form/Includes/UserFormStepNav.ss @@ -4,12 +4,12 @@ If JavaScript is disabled multi-step forms are displayed as a single page so the 'prev' and 'next' button are not used. These buttons are made visible via JavaScript. --%> - -