@uportal/form-builder 2.0.0 → 2.1.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.
- package/README.md +77 -12
- package/dist/form-builder.js +995 -161
- package/dist/form-builder.js.map +1 -1
- package/dist/form-builder.min.js +286 -73
- package/dist/form-builder.min.js.map +1 -1
- package/package.json +36 -8
- package/src/form-builder.js +937 -159
- package/src/index.js +0 -4
package/dist/form-builder.js
CHANGED
|
@@ -3,39 +3,91 @@
|
|
|
3
3
|
* Copyright 2019 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
5
|
*/
|
|
6
|
-
const t$
|
|
6
|
+
const t$1=globalThis,e$2=t$1.ShadowRoot&&(void 0===t$1.ShadyCSS||t$1.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$2=Symbol(),o$3=new WeakMap;let n$2 = class n{constructor(t,e,o){if(this._$cssResult$=true,o!==s$2)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$2&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o$3.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o$3.set(s,t));}return t}toString(){return this.cssText}};const r$2=t=>new n$2("string"==typeof t?t:t+"",void 0,s$2),i$3=(t,...e)=>{const o=1===t.length?t[0]:e.reduce(((e,s,o)=>e+(t=>{if(true===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[o+1]),t[0]);return new n$2(o,t,s$2)},S$1=(s,o)=>{if(e$2)s.adoptedStyleSheets=o.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet));else for(const e of o){const o=document.createElement("style"),n=t$1.litNonce;void 0!==n&&o.setAttribute("nonce",n),o.textContent=e.cssText,s.appendChild(o);}},c$2=e$2?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$2(e)})(t):t;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @license
|
|
10
10
|
* Copyright 2017 Google LLC
|
|
11
11
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
12
|
-
*/const{is:i$2,defineProperty:e$
|
|
12
|
+
*/const{is:i$2,defineProperty:e$1,getOwnPropertyDescriptor:h$1,getOwnPropertyNames:r$1,getOwnPropertySymbols:o$2,getPrototypeOf:n$1}=Object,a$1=globalThis,c$1=a$1.trustedTypes,l$1=c$1?c$1.emptyScript:"",p$1=a$1.reactiveElementPolyfillSupport,d$1=(t,s)=>t,u$1={toAttribute(t,s){switch(s){case Boolean:t=t?l$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t);}catch(t){i=null;}}return i}},f$1=(t,s)=>!i$2(t,s),b={attribute:true,type:String,converter:u$1,reflect:false,useDefault:false,hasChanged:f$1};Symbol.metadata??=Symbol("metadata"),a$1.litPropertyMetadata??=new WeakMap;let y$1 = class y extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t);}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b){if(s.state&&(s.attribute=false),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=true),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e$1(this.prototype,t,h);}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h$1(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t;}};return {get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i);},configurable:true,enumerable:true}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(d$1("elementProperties")))return;const t=n$1(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties);}static finalize(){if(this.hasOwnProperty(d$1("finalized")))return;if(this.finalized=true,this._$Ei(),this.hasOwnProperty(d$1("properties"))){const t=this.properties,s=[...r$1(t),...o$2(t)];for(const i of s)this.createProperty(i,t[i]);}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i);}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t);}this.elementStyles=this.finalizeStyles(this.styles);}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(c$2(s));}else void 0!==s&&i.push(c$2(s));return i}static _$Eu(t,s){const i=s.attribute;return false===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=false,this.hasUpdated=false,this._$Em=null,this._$Ev();}_$Ev(){this._$ES=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((t=>t(this)));}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.();}removeController(t){this._$EO?.delete(t);}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t);}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return S$1(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(true),this._$EO?.forEach((t=>t.hostConnected?.()));}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach((t=>t.hostDisconnected?.()));}attributeChangedCallback(t,s,i){this._$AK(t,i);}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&true===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u$1).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null;}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u$1;this._$Em=e;const r=h.fromAttribute(s,t.type);this[e]=r??this._$Ej?.get(e)??r,this._$Em=null;}}requestUpdate(t,s,i){if(void 0!==t){const e=this.constructor,h=this[t];if(i??=e.getPropertyOptions(t),!((i.hasChanged??f$1)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(e._$Eu(t,i))))return;this.C(t,s,i);} false===this.isUpdatePending&&(this._$ES=this._$EP());}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),true!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),true===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t));}async _$EP(){this.isUpdatePending=true;try{await this._$ES;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0;}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];true!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e);}}let t=false;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach((t=>t.hostUpdate?.())),this.update(s)):this._$EM();}catch(s){throw t=false,this._$EM(),s}t&&this._$AE(s);}willUpdate(t){}_$AE(t){this._$EO?.forEach((t=>t.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=true,this.firstUpdated(t)),this.updated(t);}_$EM(){this._$AL=new Map,this.isUpdatePending=false;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return true}update(t){this._$Eq&&=this._$Eq.forEach((t=>this._$ET(t,this[t]))),this._$EM();}updated(t){}firstUpdated(t){}};y$1.elementStyles=[],y$1.shadowRootOptions={mode:"open"},y$1[d$1("elementProperties")]=new Map,y$1[d$1("finalized")]=new Map,p$1?.({ReactiveElement:y$1}),(a$1.reactiveElementVersions??=[]).push("2.1.1");
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @license
|
|
16
16
|
* Copyright 2017 Google LLC
|
|
17
17
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
18
18
|
*/
|
|
19
|
-
const t
|
|
19
|
+
const t=globalThis,i$1=t.trustedTypes,s$1=i$1?i$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,e="$lit$",h=`lit$${Math.random().toFixed(9).slice(2)}$`,o$1="?"+h,n=`<${o$1}>`,r=document,l=()=>r.createComment(""),c=t=>null===t||"object"!=typeof t&&"function"!=typeof t,a=Array.isArray,u=t=>a(t)||"function"==typeof t?.[Symbol.iterator],d="[ \t\n\f\r]",f=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,v=/-->/g,_=/>/g,m=RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),p=/'/g,g=/"/g,$=/^(?:script|style|textarea|title)$/i,y=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=y(1),T=Symbol.for("lit-noChange"),E=Symbol.for("lit-nothing"),A=new WeakMap,C=r.createTreeWalker(r,129);function P(t,i){if(!a(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==s$1?s$1.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,o=[];let r,l=2===i?"<svg>":3===i?"<math>":"",c=f;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,y=0;for(;y<s.length&&(c.lastIndex=y,u=c.exec(s),null!==u);)y=c.lastIndex,c===f?"!--"===u[1]?c=v:void 0!==u[1]?c=_:void 0!==u[2]?($.test(u[2])&&(r=RegExp("</"+u[2],"g")),c=m):void 0!==u[3]&&(c=m):c===m?">"===u[0]?(c=r??f,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?m:'"'===u[3]?g:p):c===g||c===p?c=m:c===v||c===_?c=f:(c=m,r=void 0);const x=c===m&&t[i+1].startsWith("/>")?" ":"";l+=c===f?s+n:d>=0?(o.push(a),s.slice(0,d)+e+s.slice(d)+h+x):s+h+(-2===d?i:x);}return [P(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),o]};class N{constructor({strings:t,_$litType$:s},n){let r;this.parts=[];let c=0,a=0;const u=t.length-1,d=this.parts,[f,v]=V(t,s);if(this.el=N.createElement(f,n),C.currentNode=this.el.content,2===s||3===s){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=C.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(e)){const i=v[a++],s=r.getAttribute(t).split(h),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:c,name:e[2],strings:s,ctor:"."===e[1]?H:"?"===e[1]?I:"@"===e[1]?L:k}),r.removeAttribute(t);}else t.startsWith(h)&&(d.push({type:6,index:c}),r.removeAttribute(t));if($.test(r.tagName)){const t=r.textContent.split(h),s=t.length-1;if(s>0){r.textContent=i$1?i$1.emptyScript:"";for(let i=0;i<s;i++)r.append(t[i],l()),C.nextNode(),d.push({type:2,index:++c});r.append(t[s],l());}}}else if(8===r.nodeType)if(r.data===o$1)d.push({type:2,index:c});else {let t=-1;for(;-1!==(t=r.data.indexOf(h,t+1));)d.push({type:7,index:c}),t+=h.length-1;}c++;}}static createElement(t,i){const s=r.createElement("template");return s.innerHTML=t,s}}function S(t,i,s=t,e){if(i===T)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=c(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(false),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=S(t,h._$AS(t,i.values),h,e)),i}class M{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??r).importNode(i,true);C.currentNode=e;let h=C.nextNode(),o=0,n=0,l=s[0];for(;void 0!==l;){if(o===l.index){let i;2===l.type?i=new R(h,h.nextSibling,this,t):1===l.type?i=new l.ctor(h,l.name,l.strings,this,t):6===l.type&&(i=new z(h,this,t)),this._$AV.push(i),l=s[++n];}o!==l?.index&&(h=C.nextNode(),o++);}return C.currentNode=r,e}p(t){let i=0;for(const s of this._$AV) void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class R{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=E,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??true;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=S(this,t,i),c(t)?t===E||null==t||""===t?(this._$AH!==E&&this._$AR(),this._$AH=E):t!==this._$AH&&t!==T&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):u(t)?this.k(t):this._(t);}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t));}_(t){this._$AH!==E&&c(this._$AH)?this._$AA.nextSibling.data=t:this.T(r.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=N.createElement(P(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new M(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=A.get(t.strings);return void 0===i&&A.set(t.strings,i=new N(t)),i}k(t){a(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new R(this.O(l()),this.O(l()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,i){for(this._$AP?.(false,true,i);t!==this._$AB;){const i=t.nextSibling;t.remove(),t=i;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class k{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=E,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=E;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=S(this,t,i,0),o=!c(t)||t!==this._$AH&&t!==T,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=S(this,e[s+n],i,n),r===T&&(r=this._$AH[n]),o||=!c(r)||r!==this._$AH[n],r===E?t=E:t!==E&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===E?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class H extends k{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===E?void 0:t;}}class I extends k{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==E);}}class L extends k{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=S(this,t,i,0)??E)===T)return;const s=this._$AH,e=t===E&&s!==E||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==E&&(s===E||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){S(this,t);}}const j=t.litHtmlPolyfillSupport;j?.(N,R),(t.litHtmlVersions??=[]).push("3.3.1");const B=(t,i,s)=>{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new R(i.insertBefore(l(),t),t,void 0,s??{});}return h._$AI(t),h};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* @license
|
|
23
23
|
* Copyright 2017 Google LLC
|
|
24
24
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
25
|
-
*/const s=globalThis;class i extends y$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=B(r,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return T}}i._$litElement$=true,i["finalized"]=true,s.litElementHydrateSupport?.({LitElement:i});const o
|
|
25
|
+
*/const s=globalThis;class i extends y$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=B(r,this.renderRoot,this.renderOptions);}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(true);}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(false);}render(){return T}}i._$litElement$=true,i["finalized"]=true,s.litElementHydrateSupport?.({LitElement:i});const o=s.litElementPolyfillSupport;o?.({LitElement:i});(s.litElementVersions??=[]).push("4.2.1");
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
class InvalidTokenError extends Error {
|
|
28
|
+
}
|
|
29
|
+
InvalidTokenError.prototype.name = "InvalidTokenError";
|
|
30
|
+
function b64DecodeUnicode(str) {
|
|
31
|
+
return decodeURIComponent(atob(str).replace(/(.)/g, (m, p) => {
|
|
32
|
+
let code = p.charCodeAt(0).toString(16).toUpperCase();
|
|
33
|
+
if (code.length < 2) {
|
|
34
|
+
code = "0" + code;
|
|
35
|
+
}
|
|
36
|
+
return "%" + code;
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function base64UrlDecode(str) {
|
|
40
|
+
let output = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
41
|
+
switch (output.length % 4) {
|
|
42
|
+
case 0:
|
|
43
|
+
break;
|
|
44
|
+
case 2:
|
|
45
|
+
output += "==";
|
|
46
|
+
break;
|
|
47
|
+
case 3:
|
|
48
|
+
output += "=";
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
throw new Error("base64 string is not of the correct length");
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return b64DecodeUnicode(output);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return atob(output);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function jwtDecode(token, options) {
|
|
61
|
+
if (typeof token !== "string") {
|
|
62
|
+
throw new InvalidTokenError("Invalid token specified: must be a string");
|
|
63
|
+
}
|
|
64
|
+
options || (options = {});
|
|
65
|
+
const pos = options.header === true ? 0 : 1;
|
|
66
|
+
const part = token.split(".")[pos];
|
|
67
|
+
if (typeof part !== "string") {
|
|
68
|
+
throw new InvalidTokenError(`Invalid token specified: missing part #${pos + 1}`);
|
|
69
|
+
}
|
|
70
|
+
let decoded;
|
|
71
|
+
try {
|
|
72
|
+
decoded = base64UrlDecode(part);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
throw new InvalidTokenError(`Invalid token specified: invalid base64 for part #${pos + 1} (${e.message})`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(decoded);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
throw new InvalidTokenError(`Invalid token specified: invalid json for part #${pos + 1} (${e.message})`);
|
|
82
|
+
}
|
|
31
83
|
}
|
|
32
84
|
|
|
33
85
|
/**
|
|
34
86
|
* Dynamic Form Builder Web Component
|
|
35
87
|
* Fetches JSON schema and form data, then renders a dynamic form
|
|
36
|
-
*
|
|
88
|
+
*
|
|
37
89
|
* @element form-builder
|
|
38
|
-
*
|
|
90
|
+
*
|
|
39
91
|
* @attr {string} fbms-base-url - Base URL of the form builder microservice
|
|
40
92
|
* @attr {string} fbms-form-fname - Form name to fetch
|
|
41
93
|
* @attr {string} oidc-url - OpenID Connect URL for authentication
|
|
@@ -50,7 +102,7 @@ class FormBuilder extends i {
|
|
|
50
102
|
|
|
51
103
|
// Internal state
|
|
52
104
|
schema: { type: Object, state: true },
|
|
53
|
-
|
|
105
|
+
_formData: { type: Object, state: true },
|
|
54
106
|
uiSchema: { type: Object, state: true },
|
|
55
107
|
fbmsFormVersion: { type: String, state: true },
|
|
56
108
|
loading: { type: Boolean, state: true },
|
|
@@ -58,12 +110,20 @@ class FormBuilder extends i {
|
|
|
58
110
|
error: { type: String, state: true },
|
|
59
111
|
token: { type: String, state: true },
|
|
60
112
|
decoded: { type: Object, state: true },
|
|
113
|
+
submitSuccess: { type: Boolean, state: true },
|
|
114
|
+
validationFailed: { type: Boolean, state: true },
|
|
115
|
+
initialFormData: { type: Object, state: true },
|
|
116
|
+
hasChanges: { type: Boolean, state: true },
|
|
117
|
+
submissionStatus: { type: Object, state: true },
|
|
118
|
+
formCompleted: { type: Boolean, state: true },
|
|
119
|
+
submissionError: { type: String, state: true },
|
|
61
120
|
};
|
|
62
121
|
|
|
63
122
|
static styles = i$3`
|
|
64
123
|
:host {
|
|
65
124
|
display: block;
|
|
66
|
-
font-family:
|
|
125
|
+
font-family:
|
|
126
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
67
127
|
}
|
|
68
128
|
|
|
69
129
|
.container {
|
|
@@ -96,7 +156,27 @@ class FormBuilder extends i {
|
|
|
96
156
|
.form-group {
|
|
97
157
|
display: flex;
|
|
98
158
|
flex-direction: column;
|
|
99
|
-
gap:
|
|
159
|
+
gap: 4px;
|
|
160
|
+
margin: 14px 0px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nested-object {
|
|
164
|
+
margin-left: 20px;
|
|
165
|
+
padding-left: 20px;
|
|
166
|
+
border-left: 2px solid #e0e0e0;
|
|
167
|
+
margin-top: 10px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.nested-object-title {
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
color: #333;
|
|
173
|
+
margin-bottom: 10px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.nested-object-description {
|
|
177
|
+
font-size: 0.875rem;
|
|
178
|
+
color: #666;
|
|
179
|
+
margin-bottom: 15px;
|
|
100
180
|
}
|
|
101
181
|
|
|
102
182
|
label {
|
|
@@ -115,11 +195,11 @@ class FormBuilder extends i {
|
|
|
115
195
|
margin-top: 4px;
|
|
116
196
|
}
|
|
117
197
|
|
|
118
|
-
input[type=
|
|
119
|
-
input[type=
|
|
120
|
-
input[type=
|
|
121
|
-
input[type=
|
|
122
|
-
input[type=
|
|
198
|
+
input[type='text'],
|
|
199
|
+
input[type='email'],
|
|
200
|
+
input[type='number'],
|
|
201
|
+
input[type='date'],
|
|
202
|
+
input[type='tel'],
|
|
123
203
|
textarea,
|
|
124
204
|
select {
|
|
125
205
|
padding: 8px 12px;
|
|
@@ -139,16 +219,40 @@ class FormBuilder extends i {
|
|
|
139
219
|
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
|
140
220
|
}
|
|
141
221
|
|
|
222
|
+
select[multiple] {
|
|
223
|
+
min-height: 120px;
|
|
224
|
+
padding: 4px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
select[multiple] option {
|
|
228
|
+
padding: 4px 8px;
|
|
229
|
+
}
|
|
230
|
+
|
|
142
231
|
textarea {
|
|
143
232
|
min-height: 100px;
|
|
144
233
|
resize: vertical;
|
|
145
234
|
}
|
|
146
235
|
|
|
147
|
-
input[type=
|
|
148
|
-
input[type=
|
|
236
|
+
input[type='checkbox'],
|
|
237
|
+
input[type='radio'] {
|
|
149
238
|
margin-right: 8px;
|
|
150
239
|
}
|
|
151
240
|
|
|
241
|
+
fieldset {
|
|
242
|
+
border: none;
|
|
243
|
+
padding: 0;
|
|
244
|
+
margin: 0;
|
|
245
|
+
min-width: 0; /* Fix for some browsers */
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
legend {
|
|
249
|
+
font-weight: 700;
|
|
250
|
+
color: #333;
|
|
251
|
+
padding: 0;
|
|
252
|
+
margin-bottom: 8px;
|
|
253
|
+
font-size: 1rem;
|
|
254
|
+
}
|
|
255
|
+
|
|
152
256
|
.checkbox-group,
|
|
153
257
|
.radio-group {
|
|
154
258
|
display: flex;
|
|
@@ -162,6 +266,18 @@ class FormBuilder extends i {
|
|
|
162
266
|
align-items: center;
|
|
163
267
|
}
|
|
164
268
|
|
|
269
|
+
.checkbox-group.inline,
|
|
270
|
+
.radio-group.inline {
|
|
271
|
+
flex-direction: row;
|
|
272
|
+
flex-wrap: wrap;
|
|
273
|
+
gap: 16px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.checkbox-group.inline .checkbox-item,
|
|
277
|
+
.radio-group.inline .radio-item {
|
|
278
|
+
margin-right: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
165
281
|
.error-message {
|
|
166
282
|
color: #c00;
|
|
167
283
|
font-size: 0.875rem;
|
|
@@ -184,21 +300,21 @@ class FormBuilder extends i {
|
|
|
184
300
|
transition: background-color 0.2s;
|
|
185
301
|
}
|
|
186
302
|
|
|
187
|
-
button[type=
|
|
303
|
+
button[type='submit'] {
|
|
188
304
|
background-color: #0066cc;
|
|
189
305
|
color: white;
|
|
190
306
|
}
|
|
191
307
|
|
|
192
|
-
button[type=
|
|
308
|
+
button[type='submit']:hover {
|
|
193
309
|
background-color: #0052a3;
|
|
194
310
|
}
|
|
195
311
|
|
|
196
|
-
button[type=
|
|
312
|
+
button[type='button'] {
|
|
197
313
|
background-color: #c0c0c0;
|
|
198
314
|
color: #333;
|
|
199
315
|
}
|
|
200
316
|
|
|
201
|
-
button[type=
|
|
317
|
+
button[type='button']:hover {
|
|
202
318
|
background-color: #e0e0e0;
|
|
203
319
|
}
|
|
204
320
|
|
|
@@ -220,7 +336,9 @@ class FormBuilder extends i {
|
|
|
220
336
|
}
|
|
221
337
|
|
|
222
338
|
@keyframes spin {
|
|
223
|
-
to {
|
|
339
|
+
to {
|
|
340
|
+
transform: rotate(360deg);
|
|
341
|
+
}
|
|
224
342
|
}
|
|
225
343
|
|
|
226
344
|
.button-content {
|
|
@@ -228,20 +346,117 @@ class FormBuilder extends i {
|
|
|
228
346
|
align-items: center;
|
|
229
347
|
justify-content: center;
|
|
230
348
|
}
|
|
349
|
+
|
|
350
|
+
.status-message {
|
|
351
|
+
padding: 12px 16px;
|
|
352
|
+
border-radius: 4px;
|
|
353
|
+
margin-bottom: 20px;
|
|
354
|
+
font-weight: 500;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.status-message.success {
|
|
358
|
+
background-color: #d4edda;
|
|
359
|
+
border: 1px solid #c3e6cb;
|
|
360
|
+
color: #155724;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.status-message.validation-error {
|
|
364
|
+
background-color: #fff3cd;
|
|
365
|
+
border: 1px solid #ffeaa7;
|
|
366
|
+
color: #856404;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.status-message.error {
|
|
370
|
+
background-color: #f8d7da;
|
|
371
|
+
border: 1px solid #f5c6cb;
|
|
372
|
+
color: #721c24;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.status-message ul {
|
|
376
|
+
margin: 8px 0 0 0;
|
|
377
|
+
padding-left: 20px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.status-message li {
|
|
381
|
+
margin: 4px 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
form.submitting input,
|
|
385
|
+
form.submitting textarea,
|
|
386
|
+
form.submitting select,
|
|
387
|
+
form.submitting button:not([type='submit']) {
|
|
388
|
+
opacity: 0.6;
|
|
389
|
+
pointer-events: none;
|
|
390
|
+
cursor: not-allowed;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.info-only {
|
|
394
|
+
padding: 20px 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.info-only p {
|
|
398
|
+
line-height: 1.6;
|
|
399
|
+
color: #333;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.info-label {
|
|
403
|
+
font-weight: 500;
|
|
404
|
+
color: #333;
|
|
405
|
+
display: block;
|
|
406
|
+
}
|
|
231
407
|
`;
|
|
232
408
|
|
|
409
|
+
// Getter and setter for formData
|
|
410
|
+
get formData() {
|
|
411
|
+
return this._formData;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
set formData(value) {
|
|
415
|
+
const oldValue = this._formData;
|
|
416
|
+
this._formData = value;
|
|
417
|
+
this.requestUpdate('formData', oldValue);
|
|
418
|
+
this.updateStateFlags();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get custom error message from schema if available
|
|
423
|
+
* Follows the pattern: schema.properties.fieldName.messages.ruleName
|
|
424
|
+
* For nested fields: schema.properties.parent.properties.child.messages.ruleName
|
|
425
|
+
* Returns null if field, messages, or rule doesn't exist
|
|
426
|
+
*/
|
|
427
|
+
getCustomErrorMessage(fieldPath, ruleName) {
|
|
428
|
+
const pathParts = fieldPath.split('.');
|
|
429
|
+
let current = this.schema;
|
|
430
|
+
|
|
431
|
+
// Navigate to the field schema
|
|
432
|
+
for (const part of pathParts) {
|
|
433
|
+
current = current?.properties?.[part];
|
|
434
|
+
if (!current) return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Check for custom message
|
|
438
|
+
return current?.messages?.[ruleName] ?? null;
|
|
439
|
+
}
|
|
440
|
+
|
|
233
441
|
constructor() {
|
|
234
442
|
super();
|
|
235
443
|
this.loading = true;
|
|
236
444
|
this.submitting = false;
|
|
237
445
|
this.error = null;
|
|
238
446
|
this.schema = null;
|
|
239
|
-
this.
|
|
447
|
+
this._formData = {};
|
|
240
448
|
this.uiSchema = null;
|
|
241
449
|
this.fbmsFormVersion = null;
|
|
242
450
|
this.token = null;
|
|
243
|
-
this.decoded = {sub: 'unknown'};
|
|
451
|
+
this.decoded = { sub: 'unknown' };
|
|
244
452
|
this.fieldErrors = {};
|
|
453
|
+
this.submitSuccess = false;
|
|
454
|
+
this.validationFailed = false;
|
|
455
|
+
this.initialFormData = {};
|
|
456
|
+
this.hasChanges = false;
|
|
457
|
+
this.submissionStatus = null;
|
|
458
|
+
this.formCompleted = false;
|
|
459
|
+
this.submissionError = null;
|
|
245
460
|
}
|
|
246
461
|
|
|
247
462
|
async connectedCallback() {
|
|
@@ -260,10 +475,7 @@ class FormBuilder extends i {
|
|
|
260
475
|
}
|
|
261
476
|
|
|
262
477
|
// Fetch form schema and data
|
|
263
|
-
await Promise.all([
|
|
264
|
-
this.fetchSchema(),
|
|
265
|
-
this.fetchFormData(),
|
|
266
|
-
]);
|
|
478
|
+
await Promise.all([this.fetchSchema(), this.fetchFormData()]);
|
|
267
479
|
|
|
268
480
|
this.loading = false;
|
|
269
481
|
} catch (err) {
|
|
@@ -272,6 +484,80 @@ class FormBuilder extends i {
|
|
|
272
484
|
}
|
|
273
485
|
}
|
|
274
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Deep clone an object, handling Dates and other types
|
|
489
|
+
* Uses structuredClone if available, otherwise falls back to manual recursive
|
|
490
|
+
*/
|
|
491
|
+
deepClone(obj) {
|
|
492
|
+
if (obj === null || obj === undefined) return obj;
|
|
493
|
+
|
|
494
|
+
// Use structuredClone if available (modern browsers)
|
|
495
|
+
if (typeof structuredClone === 'function') {
|
|
496
|
+
try {
|
|
497
|
+
return structuredClone(obj);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
console.warn('structuredClone failed, falling back to manual clone:', err);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Fallback: manual deep clone handling common types
|
|
504
|
+
if (obj instanceof Date) {
|
|
505
|
+
return new Date(obj.getTime());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (Array.isArray(obj)) {
|
|
509
|
+
return obj.map((item) => this.deepClone(item));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (typeof obj === 'object') {
|
|
513
|
+
const cloned = {};
|
|
514
|
+
for (const key in obj) {
|
|
515
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
516
|
+
cloned[key] = this.deepClone(obj[key]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return cloned;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Primitives
|
|
523
|
+
return obj;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Deep equality check, handling Dates and other types
|
|
528
|
+
*/
|
|
529
|
+
deepEqual(obj1, obj2) {
|
|
530
|
+
if (obj1 === obj2) return true;
|
|
531
|
+
|
|
532
|
+
if (obj1 === null || obj2 === null) return false;
|
|
533
|
+
if (obj1 === undefined || obj2 === undefined) return false;
|
|
534
|
+
|
|
535
|
+
if (obj1 instanceof Date && obj2 instanceof Date) {
|
|
536
|
+
return obj1.getTime() === obj2.getTime();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// If only one is a Date, they are not equal
|
|
540
|
+
if (obj1 instanceof Date || obj2 instanceof Date) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
|
545
|
+
if (obj1.length !== obj2.length) return false;
|
|
546
|
+
return obj1.every((item, index) => this.deepEqual(item, obj2[index]));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (typeof obj1 === 'object' && typeof obj2 === 'object') {
|
|
550
|
+
const keys1 = Object.keys(obj1);
|
|
551
|
+
const keys2 = Object.keys(obj2);
|
|
552
|
+
|
|
553
|
+
if (keys1.length !== keys2.length) return false;
|
|
554
|
+
|
|
555
|
+
return keys1.every((key) => this.deepEqual(obj1[key], obj2[key]));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
275
561
|
async fetchToken() {
|
|
276
562
|
try {
|
|
277
563
|
const response = await fetch(this.oidcUrl, {
|
|
@@ -284,7 +570,13 @@ class FormBuilder extends i {
|
|
|
284
570
|
|
|
285
571
|
const data = await response.text();
|
|
286
572
|
this.token = data;
|
|
287
|
-
|
|
573
|
+
try {
|
|
574
|
+
this.decoded = jwtDecode(this.token);
|
|
575
|
+
} catch (_err) {
|
|
576
|
+
// Only need this to get the name, so warn
|
|
577
|
+
console.warn('Security Token failed to decode -- setting user to unknown');
|
|
578
|
+
this.decoded = { sub: 'unknown' };
|
|
579
|
+
}
|
|
288
580
|
} catch (err) {
|
|
289
581
|
console.error('Token fetch error:', err);
|
|
290
582
|
throw new Error('Authentication failed');
|
|
@@ -334,66 +626,250 @@ class FormBuilder extends i {
|
|
|
334
626
|
|
|
335
627
|
if (response.ok) {
|
|
336
628
|
const payload = await response.json();
|
|
337
|
-
this.
|
|
629
|
+
this._formData = payload?.answers ?? {}; // Use private property
|
|
630
|
+
this.initialFormData = this.deepClone(this._formData); // Use deepClone
|
|
338
631
|
} else {
|
|
339
|
-
|
|
340
|
-
this.
|
|
632
|
+
this._formData = {};
|
|
633
|
+
this.initialFormData = {};
|
|
341
634
|
}
|
|
635
|
+
this.hasChanges = false;
|
|
636
|
+
this.requestUpdate();
|
|
342
637
|
} catch (err) {
|
|
343
638
|
// Non-critical error
|
|
344
639
|
console.warn('Could not fetch form data:', err);
|
|
345
|
-
this.
|
|
640
|
+
this._formData = {};
|
|
641
|
+
this.initialFormData = {};
|
|
642
|
+
this.hasChanges = false;
|
|
643
|
+
this.requestUpdate();
|
|
346
644
|
}
|
|
347
645
|
}
|
|
348
646
|
|
|
349
|
-
|
|
350
|
-
|
|
647
|
+
updateStateFlags() {
|
|
648
|
+
// Clear status messages when user makes changes
|
|
649
|
+
this.submitSuccess = false;
|
|
650
|
+
this.validationFailed = false;
|
|
651
|
+
this.submissionError = null;
|
|
351
652
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
653
|
+
// Check if form data has changed from initial state
|
|
654
|
+
this.hasChanges = !this.deepEqual(this.formData, this.initialFormData);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Get nested value from formData using dot notation path
|
|
659
|
+
* e.g., "contact_information.email" => formData.contact_information.email
|
|
660
|
+
*/
|
|
661
|
+
getNestedValue(path) {
|
|
662
|
+
if (!path || typeof path !== 'string') return undefined;
|
|
663
|
+
|
|
664
|
+
const parts = path.split('.').filter((part) => part.length > 0);
|
|
665
|
+
if (parts.length === 0) return undefined;
|
|
666
|
+
|
|
667
|
+
let value = this.formData;
|
|
668
|
+
for (const part of parts) {
|
|
669
|
+
value = value?.[part];
|
|
670
|
+
}
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Sanitize a string for use as an HTML ID
|
|
676
|
+
* Replaces spaces and special characters with hyphens and collapses consecutive hyphens
|
|
677
|
+
* Ensures the ID starts with a letter by adding a prefix if necessary
|
|
678
|
+
*/
|
|
679
|
+
sanitizeId(str) {
|
|
680
|
+
if (typeof str !== 'string') {
|
|
681
|
+
str = String(str ?? '');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Replace invalid characters and collapse multiple hyphens
|
|
685
|
+
let sanitized = str.replace(/[^a-zA-Z0-9-_.]/g, '-').replace(/-+/g, '-');
|
|
686
|
+
|
|
687
|
+
// Trim leading/trailing hyphens that may have been introduced
|
|
688
|
+
sanitized = sanitized.replace(/^-+/, '').replace(/-+$/, '');
|
|
689
|
+
|
|
690
|
+
// Ensure we have some content
|
|
691
|
+
if (!sanitized) {
|
|
692
|
+
sanitized = 'id';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Ensure the ID starts with a letter
|
|
696
|
+
if (!/^[A-Za-z]/.test(sanitized)) {
|
|
697
|
+
sanitized = 'id-' + sanitized;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return sanitized;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Set nested value in formData using dot notation path
|
|
705
|
+
*/
|
|
706
|
+
setNestedValue(path, value) {
|
|
707
|
+
if (!path || typeof path !== 'string') return;
|
|
708
|
+
|
|
709
|
+
const parts = path.split('.').filter((part) => part.length > 0);
|
|
710
|
+
if (parts.length === 0) return;
|
|
711
|
+
|
|
712
|
+
const newData = { ...this.formData };
|
|
713
|
+
let current = newData;
|
|
714
|
+
|
|
715
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
716
|
+
const part = parts[i];
|
|
717
|
+
const existing = current[part];
|
|
718
|
+
// Note: Arrays are not currently supported in schemas, but we preserve them
|
|
719
|
+
// to maintain data integrity. Setting properties on arrays may produce unexpected results.
|
|
720
|
+
if (Array.isArray(existing)) {
|
|
721
|
+
current[part] = [...existing];
|
|
722
|
+
} else if (!existing || typeof existing !== 'object') {
|
|
723
|
+
current[part] = {};
|
|
724
|
+
} else {
|
|
725
|
+
current[part] = { ...existing };
|
|
726
|
+
}
|
|
727
|
+
current = current[part];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
current[parts[parts.length - 1]] = value;
|
|
731
|
+
this.formData = newData;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Get the schema object at a given path
|
|
736
|
+
* e.g., "contact_information" => schema.properties.contact_information
|
|
737
|
+
*/
|
|
738
|
+
getSchemaAtPath(path) {
|
|
739
|
+
if (!path) return this.schema; // Handle empty string/null/undefined
|
|
740
|
+
|
|
741
|
+
const parts = path.split('.').filter((part) => part.length > 0);
|
|
742
|
+
if (parts.length === 0) return this.schema; // All segments were empty
|
|
743
|
+
|
|
744
|
+
let schema = this.schema;
|
|
745
|
+
|
|
746
|
+
for (const part of parts) {
|
|
747
|
+
schema = schema.properties?.[part];
|
|
748
|
+
if (!schema) return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return schema;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
handleInputChange(fieldPath, event) {
|
|
755
|
+
const { type, value, checked } = event.target;
|
|
756
|
+
this.setNestedValue(fieldPath, type === 'checkbox' ? checked : value);
|
|
356
757
|
|
|
357
758
|
// Clear field error on change
|
|
358
|
-
if (this.fieldErrors[
|
|
759
|
+
if (this.fieldErrors[fieldPath]) {
|
|
359
760
|
this.fieldErrors = { ...this.fieldErrors };
|
|
360
|
-
delete this.fieldErrors[
|
|
761
|
+
delete this.fieldErrors[fieldPath];
|
|
361
762
|
}
|
|
763
|
+
|
|
764
|
+
this.updateStateFlags();
|
|
362
765
|
}
|
|
363
766
|
|
|
364
|
-
handleArrayChange(
|
|
365
|
-
const currentArray = this.
|
|
767
|
+
handleArrayChange(fieldPath, index, event) {
|
|
768
|
+
const currentArray = this.getNestedValue(fieldPath) || [];
|
|
366
769
|
const newArray = [...currentArray];
|
|
367
770
|
newArray[index] = event.target.value;
|
|
771
|
+
this.setNestedValue(fieldPath, newArray);
|
|
368
772
|
|
|
369
|
-
this.
|
|
370
|
-
...this.formData,
|
|
371
|
-
[fieldName]: newArray,
|
|
372
|
-
};
|
|
773
|
+
this.updateStateFlags();
|
|
373
774
|
}
|
|
374
775
|
|
|
375
|
-
|
|
776
|
+
handleMultiSelectChange(fieldPath, event) {
|
|
777
|
+
const selectedOptions = Array.from(event.target.selectedOptions);
|
|
778
|
+
const values = selectedOptions.map((option) => option.value);
|
|
779
|
+
|
|
780
|
+
this.setNestedValue(fieldPath, values);
|
|
781
|
+
|
|
782
|
+
// Clear field error on change
|
|
783
|
+
if (this.fieldErrors[fieldPath]) {
|
|
784
|
+
this.fieldErrors = { ...this.fieldErrors };
|
|
785
|
+
delete this.fieldErrors[fieldPath];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this.updateStateFlags();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
handleCheckboxArrayChange(fieldPath, optionValue, event) {
|
|
792
|
+
const { checked } = event.target;
|
|
793
|
+
const currentArray = this.getNestedValue(fieldPath) || [];
|
|
794
|
+
|
|
795
|
+
let newArray;
|
|
796
|
+
if (checked) {
|
|
797
|
+
// Add to array if not already present
|
|
798
|
+
newArray = currentArray.includes(optionValue) ? currentArray : [...currentArray, optionValue];
|
|
799
|
+
} else {
|
|
800
|
+
// Remove from array
|
|
801
|
+
newArray = currentArray.filter((v) => v !== optionValue);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
this.setNestedValue(fieldPath, newArray);
|
|
805
|
+
|
|
806
|
+
// Clear field error on change
|
|
807
|
+
if (this.fieldErrors[fieldPath]) {
|
|
808
|
+
this.fieldErrors = { ...this.fieldErrors };
|
|
809
|
+
delete this.fieldErrors[fieldPath];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
this.updateStateFlags();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Recursively validate form fields including nested objects
|
|
817
|
+
*/
|
|
818
|
+
validateFormFields(properties, required = [], basePath = '', depth = 0) {
|
|
819
|
+
const MAX_DEPTH = 10;
|
|
820
|
+
if (depth > MAX_DEPTH) {
|
|
821
|
+
console.warn(`Schema nesting exceeds maximum depth of ${MAX_DEPTH} at path: ${basePath}`);
|
|
822
|
+
return {};
|
|
823
|
+
}
|
|
824
|
+
|
|
376
825
|
const errors = {};
|
|
377
|
-
const { properties = {}, required = [] } = this.schema;
|
|
378
826
|
|
|
379
827
|
// Check required fields
|
|
380
|
-
required.forEach(fieldName => {
|
|
381
|
-
const
|
|
828
|
+
required.forEach((fieldName) => {
|
|
829
|
+
const fieldPath = basePath ? `${basePath}.${fieldName}` : fieldName;
|
|
830
|
+
const value = this.getNestedValue(fieldPath);
|
|
382
831
|
if (value === undefined || value === null || value === '') {
|
|
383
|
-
|
|
832
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'required');
|
|
833
|
+
errors[fieldPath] = customMsg || 'This field is required';
|
|
384
834
|
}
|
|
385
835
|
});
|
|
386
836
|
|
|
387
837
|
// Type validation
|
|
388
838
|
Object.entries(properties).forEach(([fieldName, fieldSchema]) => {
|
|
389
|
-
const
|
|
839
|
+
const fieldPath = basePath ? `${basePath}.${fieldName}` : fieldName;
|
|
840
|
+
const value = this.getNestedValue(fieldPath);
|
|
841
|
+
|
|
842
|
+
// Handle nested objects recursively
|
|
843
|
+
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
|
844
|
+
const nestedErrors = this.validateFormFields(
|
|
845
|
+
fieldSchema.properties,
|
|
846
|
+
fieldSchema.required || [],
|
|
847
|
+
fieldPath,
|
|
848
|
+
depth + 1
|
|
849
|
+
);
|
|
850
|
+
Object.assign(errors, nestedErrors);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
390
853
|
|
|
391
854
|
if (value !== undefined && value !== null && value !== '') {
|
|
392
855
|
// Email validation
|
|
393
856
|
if (fieldSchema.format === 'email') {
|
|
394
857
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
395
858
|
if (!emailRegex.test(value)) {
|
|
396
|
-
|
|
859
|
+
// Support both 'format' (generic) and 'email' (specific) custom message keys
|
|
860
|
+
const customMsg =
|
|
861
|
+
this.getCustomErrorMessage(fieldPath, 'format') ||
|
|
862
|
+
this.getCustomErrorMessage(fieldPath, 'email');
|
|
863
|
+
errors[fieldPath] = customMsg || 'Invalid email address';
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Pattern validation
|
|
868
|
+
if (fieldSchema.pattern) {
|
|
869
|
+
const regex = new RegExp(fieldSchema.pattern);
|
|
870
|
+
if (!regex.test(value)) {
|
|
871
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'pattern');
|
|
872
|
+
errors[fieldPath] = customMsg || 'Invalid format';
|
|
397
873
|
}
|
|
398
874
|
}
|
|
399
875
|
|
|
@@ -401,13 +877,16 @@ class FormBuilder extends i {
|
|
|
401
877
|
if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
|
|
402
878
|
const num = Number(value);
|
|
403
879
|
if (isNaN(num)) {
|
|
404
|
-
|
|
880
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'type');
|
|
881
|
+
errors[fieldPath] = customMsg || 'Must be a number';
|
|
405
882
|
} else {
|
|
406
883
|
if (fieldSchema.minimum !== undefined && num < fieldSchema.minimum) {
|
|
407
|
-
|
|
884
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'minimum');
|
|
885
|
+
errors[fieldPath] = customMsg || `Must be at least ${fieldSchema.minimum}`;
|
|
408
886
|
}
|
|
409
887
|
if (fieldSchema.maximum !== undefined && num > fieldSchema.maximum) {
|
|
410
|
-
|
|
888
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'maximum');
|
|
889
|
+
errors[fieldPath] = customMsg || `Must be at most ${fieldSchema.maximum}`;
|
|
411
890
|
}
|
|
412
891
|
}
|
|
413
892
|
}
|
|
@@ -415,24 +894,41 @@ class FormBuilder extends i {
|
|
|
415
894
|
// String length validation
|
|
416
895
|
if (fieldSchema.type === 'string') {
|
|
417
896
|
if (fieldSchema.minLength && value.length < fieldSchema.minLength) {
|
|
418
|
-
|
|
897
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'minLength');
|
|
898
|
+
errors[fieldPath] = customMsg || `Must be at least ${fieldSchema.minLength} characters`;
|
|
419
899
|
}
|
|
420
900
|
if (fieldSchema.maxLength && value.length > fieldSchema.maxLength) {
|
|
421
|
-
|
|
901
|
+
const customMsg = this.getCustomErrorMessage(fieldPath, 'maxLength');
|
|
902
|
+
errors[fieldPath] = customMsg || `Must be at most ${fieldSchema.maxLength} characters`;
|
|
422
903
|
}
|
|
423
904
|
}
|
|
424
905
|
}
|
|
425
906
|
});
|
|
426
907
|
|
|
427
|
-
|
|
428
|
-
|
|
908
|
+
return errors;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
validateForm() {
|
|
912
|
+
const { properties = {}, required = [] } = this.schema;
|
|
913
|
+
this.fieldErrors = this.validateFormFields(properties, required);
|
|
914
|
+
return Object.keys(this.fieldErrors).length === 0;
|
|
429
915
|
}
|
|
430
916
|
|
|
431
917
|
async handleSubmit(event) {
|
|
432
918
|
event.preventDefault();
|
|
433
919
|
|
|
920
|
+
// Clear previous status messages
|
|
921
|
+
this.submitSuccess = false;
|
|
922
|
+
this.validationFailed = false;
|
|
434
923
|
if (!this.validateForm()) {
|
|
435
|
-
this.
|
|
924
|
+
this.validationFailed = true;
|
|
925
|
+
await this.updateComplete; // Wait for render to complete
|
|
926
|
+
|
|
927
|
+
// Scroll to validation warning banner at top of form
|
|
928
|
+
const validationWarning = this.shadowRoot.querySelector('.status-message.validation-error');
|
|
929
|
+
if (validationWarning) {
|
|
930
|
+
validationWarning.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
931
|
+
}
|
|
436
932
|
return;
|
|
437
933
|
}
|
|
438
934
|
|
|
@@ -441,17 +937,25 @@ class FormBuilder extends i {
|
|
|
441
937
|
return;
|
|
442
938
|
}
|
|
443
939
|
|
|
940
|
+
await this.submitWithRetry(false);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async submitWithRetry(isRetry = false) {
|
|
444
944
|
try {
|
|
445
|
-
|
|
446
|
-
|
|
945
|
+
// Only set submitting on the first call, not on retry
|
|
946
|
+
if (!isRetry) {
|
|
947
|
+
this.submitting = true;
|
|
948
|
+
}
|
|
949
|
+
this.submissionError = null;
|
|
950
|
+
this.submissionStatus = null; // Clear previous messages
|
|
951
|
+
|
|
447
952
|
const body = {
|
|
448
953
|
username: this.decoded.sub,
|
|
449
954
|
formFname: this.fbmsFormFname,
|
|
450
955
|
formVersion: this.fbmsFormVersion,
|
|
451
956
|
timestamp: Date.now(),
|
|
452
|
-
answers: this.formData
|
|
957
|
+
answers: this.formData,
|
|
453
958
|
};
|
|
454
|
-
await delay(300);
|
|
455
959
|
|
|
456
960
|
const url = `${this.fbmsBaseUrl}/api/v1/submissions/${this.fbmsFormFname}`;
|
|
457
961
|
const headers = {
|
|
@@ -469,27 +973,121 @@ class FormBuilder extends i {
|
|
|
469
973
|
body: JSON.stringify(body),
|
|
470
974
|
});
|
|
471
975
|
|
|
976
|
+
// Try to parse response body for messages (even on error)
|
|
977
|
+
let responseData = null;
|
|
978
|
+
try {
|
|
979
|
+
responseData = await response.json();
|
|
980
|
+
this.submissionStatus = responseData;
|
|
981
|
+
} catch (jsonErr) {
|
|
982
|
+
// Response might not be JSON
|
|
983
|
+
console.warn('Could not parse response as JSON:', jsonErr);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Handle 403 - token may be stale
|
|
987
|
+
if (response.status === 403 && !isRetry) {
|
|
988
|
+
console.warn('Received 403, attempting to refresh token and retry...');
|
|
989
|
+
|
|
990
|
+
// Re-fetch token if OIDC URL is configured
|
|
991
|
+
if (this.oidcUrl) {
|
|
992
|
+
try {
|
|
993
|
+
await this.fetchToken();
|
|
994
|
+
console.warn('Token refreshed successfully, retrying submission...');
|
|
995
|
+
|
|
996
|
+
// Retry once with new token (submitting flag stays true)
|
|
997
|
+
return await this.submitWithRetry(true);
|
|
998
|
+
} catch (tokenError) {
|
|
999
|
+
console.error('Failed to refresh token:', tokenError);
|
|
1000
|
+
// Fall through to handle the original 403 error
|
|
1001
|
+
throw new Error('Authentication failed: Unable to refresh token');
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
console.warn('OIDC URL is not configured; cannot refresh token. Skipping retry.');
|
|
1005
|
+
// Fall through to handle the 403 error normally
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
472
1009
|
if (!response.ok) {
|
|
473
|
-
|
|
1010
|
+
// Provide specific error for 403 after retry
|
|
1011
|
+
if (response.status === 403 && isRetry) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
'Authorization failed: Access denied even after token refresh. You may not have permission to submit this form.'
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Use server error message if available
|
|
1018
|
+
const errorMessage =
|
|
1019
|
+
responseData?.messageHeader ||
|
|
1020
|
+
responseData?.message ||
|
|
1021
|
+
`Failed to submit form: ${response.statusText}`;
|
|
1022
|
+
throw new Error(errorMessage);
|
|
474
1023
|
}
|
|
475
1024
|
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
1025
|
+
// Check for form forwarding header (safely handle missing headers object)
|
|
1026
|
+
const formForward = response.headers?.get ? response.headers.get('x-fbms-formforward') : null;
|
|
1027
|
+
if (formForward) {
|
|
1028
|
+
// eslint-disable-next-line no-console
|
|
1029
|
+
console.info(`Form submitted successfully. Forwarding to next form: ${formForward}`);
|
|
1030
|
+
this.fbmsFormFname = formForward;
|
|
1031
|
+
|
|
1032
|
+
// Keep success state and messages visible for the forwarded form
|
|
1033
|
+
this.submitSuccess = true;
|
|
1034
|
+
// Note: submissionStatus is preserved to show server messages on the next form
|
|
1035
|
+
this.formCompleted = false;
|
|
1036
|
+
|
|
1037
|
+
// Re-initialize with the new form
|
|
1038
|
+
this.loading = true;
|
|
1039
|
+
try {
|
|
1040
|
+
await this.initialize();
|
|
1041
|
+
return; // Exit early, don't show success message for intermediate form
|
|
1042
|
+
// Note: finally block will set submitting = false
|
|
1043
|
+
} catch (forwardingError) {
|
|
1044
|
+
console.error('Failed to load forwarded form:', forwardingError);
|
|
1045
|
+
this.loading = false;
|
|
1046
|
+
this.submissionError =
|
|
1047
|
+
forwardingError?.message || 'Form was submitted, but loading the next form failed.';
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
482
1050
|
|
|
483
|
-
//
|
|
484
|
-
this.
|
|
1051
|
+
// Dispatch success event
|
|
1052
|
+
this.dispatchEvent(
|
|
1053
|
+
new CustomEvent('form-submit-success', {
|
|
1054
|
+
detail: { data: body },
|
|
1055
|
+
bubbles: true,
|
|
1056
|
+
composed: true,
|
|
1057
|
+
})
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
// No form forward - this is the final form completion
|
|
1061
|
+
this.formCompleted = true;
|
|
1062
|
+
this.submitSuccess = true;
|
|
1063
|
+
this.submissionError = null;
|
|
1064
|
+
this.initialFormData = this.deepClone(this.formData);
|
|
1065
|
+
this.hasChanges = false;
|
|
1066
|
+
|
|
1067
|
+
await this.updateComplete;
|
|
1068
|
+
|
|
1069
|
+
// Scroll to success message
|
|
1070
|
+
const successMsg = this.shadowRoot.querySelector('.status-message.success');
|
|
1071
|
+
if (successMsg) {
|
|
1072
|
+
successMsg.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1073
|
+
}
|
|
485
1074
|
} catch (err) {
|
|
486
|
-
this.
|
|
487
|
-
|
|
488
|
-
this.dispatchEvent(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
1075
|
+
this.submissionError = err.message || 'Failed to submit form';
|
|
1076
|
+
|
|
1077
|
+
this.dispatchEvent(
|
|
1078
|
+
new CustomEvent('form-submit-error', {
|
|
1079
|
+
detail: { error: err.message },
|
|
1080
|
+
bubbles: true,
|
|
1081
|
+
composed: true,
|
|
1082
|
+
})
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
// Scroll to error message at top of form
|
|
1086
|
+
await this.updateComplete;
|
|
1087
|
+
const errorMsg = this.shadowRoot.querySelector('.status-message.error');
|
|
1088
|
+
if (errorMsg) {
|
|
1089
|
+
errorMsg.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1090
|
+
}
|
|
493
1091
|
} finally {
|
|
494
1092
|
this.submitting = false;
|
|
495
1093
|
}
|
|
@@ -501,49 +1099,211 @@ class FormBuilder extends i {
|
|
|
501
1099
|
this.requestUpdate();
|
|
502
1100
|
}
|
|
503
1101
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
1102
|
+
/**
|
|
1103
|
+
* Render a field - can be a simple input or a nested object
|
|
1104
|
+
*/
|
|
1105
|
+
renderField(fieldName, fieldSchema, basePath = '', depth = 0) {
|
|
1106
|
+
const MAX_DEPTH = 10;
|
|
1107
|
+
if (depth > MAX_DEPTH) {
|
|
1108
|
+
console.warn(`Schema nesting exceeds maximum depth of ${MAX_DEPTH}`);
|
|
1109
|
+
return x`<div class="error">Schema too deeply nested</div>`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const fieldPath = basePath ? `${basePath}.${fieldName}` : fieldName;
|
|
1113
|
+
|
|
1114
|
+
// Handle nested objects with properties
|
|
1115
|
+
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
|
1116
|
+
return this.renderNestedObject(fieldName, fieldSchema, basePath, depth);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Single-value enum - render as informational text only (title serves as the message)
|
|
1120
|
+
if (fieldSchema.enum && fieldSchema.enum.length === 1) {
|
|
1121
|
+
return x`
|
|
1122
|
+
<div class="form-group">
|
|
1123
|
+
<span class="info-label">${fieldSchema.title || fieldName}</span>
|
|
1124
|
+
${fieldSchema.description
|
|
1125
|
+
? x`<span class="description">${fieldSchema.description}</span>`
|
|
1126
|
+
: ''}
|
|
1127
|
+
</div>
|
|
1128
|
+
`;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Regular field
|
|
1132
|
+
const value = this.getNestedValue(fieldPath);
|
|
1133
|
+
const error = this.fieldErrors[fieldPath];
|
|
1134
|
+
// For nested fields, check the parent schema's required array
|
|
1135
|
+
const parentSchema = basePath ? this.getSchemaAtPath(basePath) : this.schema;
|
|
1136
|
+
const required = parentSchema?.required?.includes(fieldName) ?? false;
|
|
1137
|
+
const uiSchemaPath = fieldPath.split('.');
|
|
1138
|
+
let uiOptions = this.uiSchema;
|
|
1139
|
+
for (const part of uiSchemaPath) {
|
|
1140
|
+
uiOptions = uiOptions?.[part];
|
|
1141
|
+
}
|
|
1142
|
+
uiOptions = uiOptions || {};
|
|
1143
|
+
|
|
1144
|
+
const widget = uiOptions['ui:widget'];
|
|
1145
|
+
const isGroupedInput = widget === 'radio' || widget === 'checkboxes';
|
|
509
1146
|
|
|
510
1147
|
return x`
|
|
511
1148
|
<div class="form-group">
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1149
|
+
${!isGroupedInput
|
|
1150
|
+
? x`
|
|
1151
|
+
<label class="${required ? 'required' : ''}" for="${fieldPath}">
|
|
1152
|
+
${fieldSchema.title || fieldName}
|
|
1153
|
+
</label>
|
|
1154
|
+
`
|
|
1155
|
+
: ''}
|
|
1156
|
+
${fieldSchema.description && !isGroupedInput
|
|
1157
|
+
? x` <span class="description">${fieldSchema.description}</span> `
|
|
1158
|
+
: ''}
|
|
1159
|
+
${this.renderInput(fieldPath, fieldSchema, value, uiOptions)}
|
|
1160
|
+
${error ? x` <span class="error-message">${error}</span> ` : ''}
|
|
1161
|
+
</div>
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
519
1164
|
|
|
520
|
-
|
|
1165
|
+
/**
|
|
1166
|
+
* Render a nested object with its own properties
|
|
1167
|
+
*/
|
|
1168
|
+
renderNestedObject(fieldName, fieldSchema, basePath = '', depth = 0) {
|
|
1169
|
+
const fieldPath = basePath ? `${basePath}.${fieldName}` : fieldName;
|
|
521
1170
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1171
|
+
return x`
|
|
1172
|
+
<div class="nested-object">
|
|
1173
|
+
${fieldSchema.title
|
|
1174
|
+
? x`<div class="nested-object-title">${fieldSchema.title}</div>`
|
|
1175
|
+
: ''}
|
|
1176
|
+
${fieldSchema.description
|
|
1177
|
+
? x`<div class="nested-object-description">${fieldSchema.description}</div>`
|
|
1178
|
+
: ''}
|
|
1179
|
+
${Object.entries(fieldSchema.properties).map(([nestedFieldName, nestedFieldSchema]) =>
|
|
1180
|
+
this.renderField(nestedFieldName, nestedFieldSchema, fieldPath, depth + 1)
|
|
1181
|
+
)}
|
|
525
1182
|
</div>
|
|
526
1183
|
`;
|
|
527
1184
|
}
|
|
528
1185
|
|
|
529
|
-
renderInput(
|
|
530
|
-
const { type, enum: enumValues, format } = fieldSchema;
|
|
1186
|
+
renderInput(fieldPath, fieldSchema, value, uiOptions) {
|
|
1187
|
+
const { type, enum: enumValues, format, items } = fieldSchema;
|
|
1188
|
+
const widget = uiOptions['ui:widget'];
|
|
1189
|
+
const isInline = uiOptions['ui:options']?.inline;
|
|
1190
|
+
|
|
1191
|
+
// Single-value enum - no input needed, title/label already displays the message
|
|
1192
|
+
if (enumValues && enumValues.length === 1) {
|
|
1193
|
+
return x``;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Array of enums with checkboxes widget - render as checkboxes
|
|
1197
|
+
if (type === 'array' && items?.enum && widget === 'checkboxes') {
|
|
1198
|
+
const selectedValues = Array.isArray(value) ? value : [];
|
|
1199
|
+
const containerClass = isInline ? 'checkbox-group inline' : 'checkbox-group';
|
|
1200
|
+
|
|
1201
|
+
// Extract basePath and fieldName from fieldPath
|
|
1202
|
+
const pathParts = fieldPath.split('.');
|
|
1203
|
+
const fieldName = pathParts[pathParts.length - 1];
|
|
1204
|
+
const basePath = pathParts.slice(0, -1).join('.');
|
|
1205
|
+
|
|
1206
|
+
const parentSchema = basePath ? this.getSchemaAtPath(basePath) : this.schema;
|
|
1207
|
+
const isRequired = parentSchema?.required?.includes(fieldName) ?? false;
|
|
1208
|
+
|
|
1209
|
+
return x`
|
|
1210
|
+
<fieldset class="${containerClass}">
|
|
1211
|
+
<legend class="${isRequired ? 'required' : ''}">${fieldSchema.title || fieldName}</legend>
|
|
1212
|
+
${fieldSchema.description
|
|
1213
|
+
? x`<span class="description">${fieldSchema.description}</span>`
|
|
1214
|
+
: ''}
|
|
1215
|
+
${items.enum.map((opt) => {
|
|
1216
|
+
const sanitizedId = this.sanitizeId(`${fieldPath}-${opt}`);
|
|
1217
|
+
return x`
|
|
1218
|
+
<div class="checkbox-item">
|
|
1219
|
+
<input
|
|
1220
|
+
type="checkbox"
|
|
1221
|
+
id="${sanitizedId}"
|
|
1222
|
+
name="${fieldPath}"
|
|
1223
|
+
value="${opt}"
|
|
1224
|
+
.checked="${selectedValues.includes(opt)}"
|
|
1225
|
+
@change="${(e) => this.handleCheckboxArrayChange(fieldPath, opt, e)}"
|
|
1226
|
+
/>
|
|
1227
|
+
<label for="${sanitizedId}">${opt}</label>
|
|
1228
|
+
</div>
|
|
1229
|
+
`;
|
|
1230
|
+
})}
|
|
1231
|
+
</fieldset>
|
|
1232
|
+
`;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Array of enums without widget - render as multi-select dropdown (default)
|
|
1236
|
+
if (type === 'array' && items?.enum) {
|
|
1237
|
+
const selectedValues = Array.isArray(value) ? value : [];
|
|
531
1238
|
|
|
532
|
-
|
|
1239
|
+
return x`
|
|
1240
|
+
<select
|
|
1241
|
+
id="${fieldPath}"
|
|
1242
|
+
name="${fieldPath}"
|
|
1243
|
+
multiple
|
|
1244
|
+
size="5"
|
|
1245
|
+
@change="${(e) => this.handleMultiSelectChange(fieldPath, e)}"
|
|
1246
|
+
>
|
|
1247
|
+
${items.enum.map(
|
|
1248
|
+
(opt) => x`
|
|
1249
|
+
<option value="${opt}" ?selected="${selectedValues.includes(opt)}">${opt}</option>
|
|
1250
|
+
`
|
|
1251
|
+
)}
|
|
1252
|
+
</select>
|
|
1253
|
+
`;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Enum with radio widget - render as radio buttons
|
|
1257
|
+
if (enumValues && widget === 'radio') {
|
|
1258
|
+
const containerClass = isInline ? 'radio-group inline' : 'radio-group';
|
|
1259
|
+
|
|
1260
|
+
// Extract basePath and fieldName from fieldPath
|
|
1261
|
+
const pathParts = fieldPath.split('.');
|
|
1262
|
+
const fieldName = pathParts[pathParts.length - 1];
|
|
1263
|
+
const basePath = pathParts.slice(0, -1).join('.');
|
|
1264
|
+
|
|
1265
|
+
const parentSchema = basePath ? this.getSchemaAtPath(basePath) : this.schema;
|
|
1266
|
+
const isRequired = parentSchema?.required?.includes(fieldName) ?? false;
|
|
1267
|
+
|
|
1268
|
+
return x`
|
|
1269
|
+
<fieldset class="${containerClass}">
|
|
1270
|
+
<legend class="${isRequired ? 'required' : ''}">${fieldSchema.title || fieldName}</legend>
|
|
1271
|
+
${fieldSchema.description
|
|
1272
|
+
? x`<span class="description">${fieldSchema.description}</span>`
|
|
1273
|
+
: ''}
|
|
1274
|
+
${enumValues.map((opt) => {
|
|
1275
|
+
const sanitizedId = this.sanitizeId(`${fieldPath}-${opt}`);
|
|
1276
|
+
return x`
|
|
1277
|
+
<div class="radio-item">
|
|
1278
|
+
<input
|
|
1279
|
+
type="radio"
|
|
1280
|
+
id="${sanitizedId}"
|
|
1281
|
+
name="${fieldPath}"
|
|
1282
|
+
value="${opt}"
|
|
1283
|
+
.checked="${value === opt}"
|
|
1284
|
+
@change="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
1285
|
+
/>
|
|
1286
|
+
<label for="${sanitizedId}">${opt}</label>
|
|
1287
|
+
</div>
|
|
1288
|
+
`;
|
|
1289
|
+
})}
|
|
1290
|
+
</fieldset>
|
|
1291
|
+
`;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Enum - render as select (default)
|
|
533
1295
|
if (enumValues) {
|
|
534
1296
|
return x`
|
|
535
1297
|
<select
|
|
536
|
-
id="${
|
|
537
|
-
name="${
|
|
1298
|
+
id="${fieldPath}"
|
|
1299
|
+
name="${fieldPath}"
|
|
538
1300
|
.value="${value || ''}"
|
|
539
|
-
@change="${(e) => this.handleInputChange(
|
|
1301
|
+
@change="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
540
1302
|
>
|
|
541
1303
|
<option value="">-- Select --</option>
|
|
542
|
-
${enumValues.map(
|
|
543
|
-
<option value="${opt}" ?selected="${value === opt}">
|
|
544
|
-
|
|
545
|
-
</option>
|
|
546
|
-
`)}
|
|
1304
|
+
${enumValues.map(
|
|
1305
|
+
(opt) => x` <option value="${opt}" ?selected="${value === opt}">${opt}</option> `
|
|
1306
|
+
)}
|
|
547
1307
|
</select>
|
|
548
1308
|
`;
|
|
549
1309
|
}
|
|
@@ -554,12 +1314,12 @@ class FormBuilder extends i {
|
|
|
554
1314
|
<div class="checkbox-item">
|
|
555
1315
|
<input
|
|
556
1316
|
type="checkbox"
|
|
557
|
-
id="${
|
|
558
|
-
name="${
|
|
1317
|
+
id="${fieldPath}"
|
|
1318
|
+
name="${fieldPath}"
|
|
559
1319
|
.checked="${!!value}"
|
|
560
|
-
@change="${(e) => this.handleInputChange(
|
|
1320
|
+
@change="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
561
1321
|
/>
|
|
562
|
-
<label for="${
|
|
1322
|
+
<label for="${fieldPath}">${fieldSchema.title || fieldPath.split('.').pop()}</label>
|
|
563
1323
|
</div>
|
|
564
1324
|
`;
|
|
565
1325
|
}
|
|
@@ -570,10 +1330,10 @@ class FormBuilder extends i {
|
|
|
570
1330
|
return x`
|
|
571
1331
|
<input
|
|
572
1332
|
type="email"
|
|
573
|
-
id="${
|
|
574
|
-
name="${
|
|
1333
|
+
id="${fieldPath}"
|
|
1334
|
+
name="${fieldPath}"
|
|
575
1335
|
.value="${value || ''}"
|
|
576
|
-
@input="${(e) => this.handleInputChange(
|
|
1336
|
+
@input="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
577
1337
|
/>
|
|
578
1338
|
`;
|
|
579
1339
|
}
|
|
@@ -582,10 +1342,10 @@ class FormBuilder extends i {
|
|
|
582
1342
|
return x`
|
|
583
1343
|
<input
|
|
584
1344
|
type="date"
|
|
585
|
-
id="${
|
|
586
|
-
name="${
|
|
1345
|
+
id="${fieldPath}"
|
|
1346
|
+
name="${fieldPath}"
|
|
587
1347
|
.value="${value || ''}"
|
|
588
|
-
@input="${(e) => this.handleInputChange(
|
|
1348
|
+
@input="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
589
1349
|
/>
|
|
590
1350
|
`;
|
|
591
1351
|
}
|
|
@@ -593,10 +1353,10 @@ class FormBuilder extends i {
|
|
|
593
1353
|
if (uiOptions['ui:widget'] === 'textarea') {
|
|
594
1354
|
return x`
|
|
595
1355
|
<textarea
|
|
596
|
-
id="${
|
|
597
|
-
name="${
|
|
1356
|
+
id="${fieldPath}"
|
|
1357
|
+
name="${fieldPath}"
|
|
598
1358
|
.value="${value || ''}"
|
|
599
|
-
@input="${(e) => this.handleInputChange(
|
|
1359
|
+
@input="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
600
1360
|
></textarea>
|
|
601
1361
|
`;
|
|
602
1362
|
}
|
|
@@ -605,10 +1365,10 @@ class FormBuilder extends i {
|
|
|
605
1365
|
return x`
|
|
606
1366
|
<input
|
|
607
1367
|
type="text"
|
|
608
|
-
id="${
|
|
609
|
-
name="${
|
|
1368
|
+
id="${fieldPath}"
|
|
1369
|
+
name="${fieldPath}"
|
|
610
1370
|
.value="${value || ''}"
|
|
611
|
-
@input="${(e) => this.handleInputChange(
|
|
1371
|
+
@input="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
612
1372
|
/>
|
|
613
1373
|
`;
|
|
614
1374
|
}
|
|
@@ -618,11 +1378,11 @@ class FormBuilder extends i {
|
|
|
618
1378
|
return x`
|
|
619
1379
|
<input
|
|
620
1380
|
type="number"
|
|
621
|
-
id="${
|
|
622
|
-
name="${
|
|
1381
|
+
id="${fieldPath}"
|
|
1382
|
+
name="${fieldPath}"
|
|
623
1383
|
.value="${value || ''}"
|
|
624
1384
|
step="${type === 'integer' ? '1' : 'any'}"
|
|
625
|
-
@input="${(e) => this.handleInputChange(
|
|
1385
|
+
@input="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
626
1386
|
/>
|
|
627
1387
|
`;
|
|
628
1388
|
}
|
|
@@ -631,29 +1391,27 @@ class FormBuilder extends i {
|
|
|
631
1391
|
return x`
|
|
632
1392
|
<input
|
|
633
1393
|
type="text"
|
|
634
|
-
id="${
|
|
635
|
-
name="${
|
|
1394
|
+
id="${fieldPath}"
|
|
1395
|
+
name="${fieldPath}"
|
|
636
1396
|
.value="${value || ''}"
|
|
637
|
-
@input="${(e) => this.handleInputChange(
|
|
1397
|
+
@input="${(e) => this.handleInputChange(fieldPath, e)}"
|
|
638
1398
|
/>
|
|
639
1399
|
`;
|
|
640
1400
|
}
|
|
641
1401
|
|
|
642
1402
|
render() {
|
|
643
|
-
if (this.
|
|
1403
|
+
if (this.error) {
|
|
644
1404
|
return x`
|
|
645
1405
|
<div class="container">
|
|
646
|
-
<div class="
|
|
1406
|
+
<div class="error"><strong>Error:</strong> ${this.error}</div>
|
|
647
1407
|
</div>
|
|
648
1408
|
`;
|
|
649
1409
|
}
|
|
650
1410
|
|
|
651
|
-
if (this.
|
|
1411
|
+
if (this.loading) {
|
|
652
1412
|
return x`
|
|
653
1413
|
<div class="container">
|
|
654
|
-
<div class="
|
|
655
|
-
<strong>Error:</strong> ${this.error}
|
|
656
|
-
</div>
|
|
1414
|
+
<div class="loading">Loading form...</div>
|
|
657
1415
|
</div>
|
|
658
1416
|
`;
|
|
659
1417
|
}
|
|
@@ -666,30 +1424,106 @@ class FormBuilder extends i {
|
|
|
666
1424
|
`;
|
|
667
1425
|
}
|
|
668
1426
|
|
|
669
|
-
|
|
670
|
-
|
|
1427
|
+
// NEW: Success-only view when form is completed
|
|
1428
|
+
if (this.formCompleted) {
|
|
1429
|
+
return x`
|
|
1430
|
+
${this.customStyles
|
|
1431
|
+
? x`<style>
|
|
1432
|
+
${this.customStyles}
|
|
1433
|
+
</style>`
|
|
1434
|
+
: ''}
|
|
671
1435
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1436
|
+
<div class="container">
|
|
1437
|
+
<div class="status-message success">
|
|
1438
|
+
<h2>✓ Form submitted successfully!</h2>
|
|
1439
|
+
${this.submissionStatus?.messages?.length > 0
|
|
1440
|
+
? x`
|
|
1441
|
+
<ul>
|
|
1442
|
+
${this.submissionStatus.messages.map((msg) => x`<li>${msg}</li>`)}
|
|
1443
|
+
</ul>
|
|
1444
|
+
`
|
|
1445
|
+
: ''}
|
|
1446
|
+
</div>
|
|
1447
|
+
</div>
|
|
1448
|
+
`;
|
|
1449
|
+
}
|
|
676
1450
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
)}
|
|
1451
|
+
// Regular form view (rest of existing render code)
|
|
1452
|
+
const hasFields = Object.keys(this.schema.properties).length > 0;
|
|
680
1453
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1454
|
+
return x`
|
|
1455
|
+
${this.customStyles
|
|
1456
|
+
? x`<style>
|
|
1457
|
+
${this.customStyles}
|
|
1458
|
+
</style>`
|
|
1459
|
+
: ''}
|
|
1460
|
+
|
|
1461
|
+
<div class="container">
|
|
1462
|
+
${this.submitSuccess
|
|
1463
|
+
? x`
|
|
1464
|
+
<div class="status-message success">
|
|
1465
|
+
✓ Your form was successfully submitted.
|
|
1466
|
+
${this.submissionStatus?.messages?.length > 0
|
|
1467
|
+
? x`
|
|
1468
|
+
<ul>
|
|
1469
|
+
${this.submissionStatus.messages.map((msg) => x`<li>${msg}</li>`)}
|
|
1470
|
+
</ul>
|
|
1471
|
+
`
|
|
1472
|
+
: ''}
|
|
1473
|
+
</div>
|
|
1474
|
+
`
|
|
1475
|
+
: ''}
|
|
1476
|
+
${hasFields
|
|
1477
|
+
? x`
|
|
1478
|
+
<form @submit="${this.handleSubmit}" class="${this.submitting ? 'submitting' : ''}">
|
|
1479
|
+
${this.schema.title ? x`<h2>${this.schema.title}</h2>` : ''}
|
|
1480
|
+
${this.schema.description ? x`<p>${this.schema.description}</p>` : ''}
|
|
1481
|
+
${this.validationFailed
|
|
1482
|
+
? x`
|
|
1483
|
+
<div class="status-message validation-error">
|
|
1484
|
+
⚠ Please correct the errors below before submitting.
|
|
1485
|
+
</div>
|
|
1486
|
+
`
|
|
1487
|
+
: ''}
|
|
1488
|
+
${this.submissionError
|
|
1489
|
+
? x`
|
|
1490
|
+
<div class="status-message error">
|
|
1491
|
+
<strong>Error:</strong> ${this.submissionError}
|
|
1492
|
+
${this.submissionStatus?.messages?.length > 0
|
|
1493
|
+
? x`
|
|
1494
|
+
<ul>
|
|
1495
|
+
${this.submissionStatus.messages.map(
|
|
1496
|
+
(msg) => x`<li>${msg}</li>`
|
|
1497
|
+
)}
|
|
1498
|
+
</ul>
|
|
1499
|
+
`
|
|
1500
|
+
: ''}
|
|
1501
|
+
</div>
|
|
1502
|
+
`
|
|
1503
|
+
: ''}
|
|
1504
|
+
${Object.entries(this.schema.properties).map(([fieldName, fieldSchema]) =>
|
|
1505
|
+
this.renderField(fieldName, fieldSchema)
|
|
1506
|
+
)}
|
|
1507
|
+
|
|
1508
|
+
<div class="buttons">
|
|
1509
|
+
<button type="submit" ?disabled="${this.submitting || !this.hasChanges}">
|
|
1510
|
+
<span class="button-content">
|
|
1511
|
+
${this.submitting ? x`<span class="spinner"></span>` : ''}
|
|
1512
|
+
${this.submitting ? 'Submitting...' : 'Submit'}
|
|
1513
|
+
</span>
|
|
1514
|
+
</button>
|
|
1515
|
+
<button type="button" @click="${this.handleReset}" ?disabled="${this.submitting}">
|
|
1516
|
+
Reset
|
|
1517
|
+
</button>
|
|
1518
|
+
</div>
|
|
1519
|
+
</form>
|
|
1520
|
+
`
|
|
1521
|
+
: x`
|
|
1522
|
+
<div class="info-only">
|
|
1523
|
+
${this.schema.title ? x`<h2>${this.schema.title}</h2>` : ''}
|
|
1524
|
+
${this.schema.description ? x`<p>${this.schema.description}</p>` : ''}
|
|
1525
|
+
</div>
|
|
1526
|
+
`}
|
|
693
1527
|
</div>
|
|
694
1528
|
`;
|
|
695
1529
|
}
|