@uportal/form-builder 1.3.2 → 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.
@@ -0,0 +1,1533 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2019 Google LLC
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ */
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
+
8
+ /**
9
+ * @license
10
+ * Copyright 2017 Google LLC
11
+ * SPDX-License-Identifier: BSD-3-Clause
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
+
14
+ /**
15
+ * @license
16
+ * Copyright 2017 Google LLC
17
+ * SPDX-License-Identifier: BSD-3-Clause
18
+ */
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
+
21
+ /**
22
+ * @license
23
+ * Copyright 2017 Google LLC
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=s.litElementPolyfillSupport;o?.({LitElement:i});(s.litElementVersions??=[]).push("4.2.1");
26
+
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
+ }
83
+ }
84
+
85
+ /**
86
+ * Dynamic Form Builder Web Component
87
+ * Fetches JSON schema and form data, then renders a dynamic form
88
+ *
89
+ * @element form-builder
90
+ *
91
+ * @attr {string} fbms-base-url - Base URL of the form builder microservice
92
+ * @attr {string} fbms-form-fname - Form name to fetch
93
+ * @attr {string} oidc-url - OpenID Connect URL for authentication
94
+ * @attr {string} styles - Optional custom CSS styles
95
+ */
96
+ class FormBuilder extends i {
97
+ static properties = {
98
+ fbmsBaseUrl: { type: String, attribute: 'fbms-base-url' },
99
+ fbmsFormFname: { type: String, attribute: 'fbms-form-fname' },
100
+ oidcUrl: { type: String, attribute: 'oidc-url' },
101
+ customStyles: { type: String, attribute: 'styles' },
102
+
103
+ // Internal state
104
+ schema: { type: Object, state: true },
105
+ _formData: { type: Object, state: true },
106
+ uiSchema: { type: Object, state: true },
107
+ fbmsFormVersion: { type: String, state: true },
108
+ loading: { type: Boolean, state: true },
109
+ submitting: { type: Boolean, state: true },
110
+ error: { type: String, state: true },
111
+ token: { type: String, state: true },
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 },
120
+ };
121
+
122
+ static styles = i$3`
123
+ :host {
124
+ display: block;
125
+ font-family:
126
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
127
+ }
128
+
129
+ .container {
130
+ max-width: 800px;
131
+ margin: 0 auto;
132
+ padding: 20px;
133
+ }
134
+
135
+ .loading {
136
+ text-align: center;
137
+ padding: 40px;
138
+ color: #666;
139
+ }
140
+
141
+ .error {
142
+ background-color: #fee;
143
+ border: 1px solid #fcc;
144
+ border-radius: 4px;
145
+ padding: 15px;
146
+ margin: 20px 0;
147
+ color: #c00;
148
+ }
149
+
150
+ form {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 20px;
154
+ }
155
+
156
+ .form-group {
157
+ display: flex;
158
+ flex-direction: column;
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;
180
+ }
181
+
182
+ label {
183
+ font-weight: 500;
184
+ color: #333;
185
+ }
186
+
187
+ .required::after {
188
+ content: ' *';
189
+ color: #c00;
190
+ }
191
+
192
+ .description {
193
+ font-size: 0.875rem;
194
+ color: #666;
195
+ margin-top: 4px;
196
+ }
197
+
198
+ input[type='text'],
199
+ input[type='email'],
200
+ input[type='number'],
201
+ input[type='date'],
202
+ input[type='tel'],
203
+ textarea,
204
+ select {
205
+ padding: 8px 12px;
206
+ border: 1px solid #ccc;
207
+ border-radius: 4px;
208
+ font-size: 1rem;
209
+ font-family: inherit;
210
+ width: 100%;
211
+ box-sizing: border-box;
212
+ }
213
+
214
+ input:focus,
215
+ textarea:focus,
216
+ select:focus {
217
+ outline: none;
218
+ border-color: #0066cc;
219
+ box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
220
+ }
221
+
222
+ select[multiple] {
223
+ min-height: 120px;
224
+ padding: 4px;
225
+ }
226
+
227
+ select[multiple] option {
228
+ padding: 4px 8px;
229
+ }
230
+
231
+ textarea {
232
+ min-height: 100px;
233
+ resize: vertical;
234
+ }
235
+
236
+ input[type='checkbox'],
237
+ input[type='radio'] {
238
+ margin-right: 8px;
239
+ }
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
+
256
+ .checkbox-group,
257
+ .radio-group {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 8px;
261
+ }
262
+
263
+ .checkbox-item,
264
+ .radio-item {
265
+ display: flex;
266
+ align-items: center;
267
+ }
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
+
281
+ .error-message {
282
+ color: #c00;
283
+ font-size: 0.875rem;
284
+ margin-top: 4px;
285
+ }
286
+
287
+ .buttons {
288
+ display: flex;
289
+ gap: 12px;
290
+ margin-top: 20px;
291
+ }
292
+
293
+ button {
294
+ padding: 10px 20px;
295
+ border: none;
296
+ border-radius: 4px;
297
+ font-size: 1rem;
298
+ cursor: pointer;
299
+ font-family: inherit;
300
+ transition: background-color 0.2s;
301
+ }
302
+
303
+ button[type='submit'] {
304
+ background-color: #0066cc;
305
+ color: white;
306
+ }
307
+
308
+ button[type='submit']:hover {
309
+ background-color: #0052a3;
310
+ }
311
+
312
+ button[type='button'] {
313
+ background-color: #c0c0c0;
314
+ color: #333;
315
+ }
316
+
317
+ button[type='button']:hover {
318
+ background-color: #e0e0e0;
319
+ }
320
+
321
+ button:disabled {
322
+ opacity: 0.5;
323
+ cursor: not-allowed;
324
+ }
325
+
326
+ .spinner {
327
+ display: inline-block;
328
+ width: 1em;
329
+ height: 1em;
330
+ border: 2px solid rgba(255, 255, 255, 0.3);
331
+ border-radius: 50%;
332
+ border-top-color: white;
333
+ animation: spin 0.8s linear infinite;
334
+ margin-right: 8px;
335
+ vertical-align: middle;
336
+ }
337
+
338
+ @keyframes spin {
339
+ to {
340
+ transform: rotate(360deg);
341
+ }
342
+ }
343
+
344
+ .button-content {
345
+ display: inline-flex;
346
+ align-items: center;
347
+ justify-content: center;
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
+ }
407
+ `;
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
+
441
+ constructor() {
442
+ super();
443
+ this.loading = true;
444
+ this.submitting = false;
445
+ this.error = null;
446
+ this.schema = null;
447
+ this._formData = {};
448
+ this.uiSchema = null;
449
+ this.fbmsFormVersion = null;
450
+ this.token = null;
451
+ this.decoded = { sub: 'unknown' };
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;
460
+ }
461
+
462
+ async connectedCallback() {
463
+ super.connectedCallback();
464
+ await this.initialize();
465
+ }
466
+
467
+ async initialize() {
468
+ try {
469
+ this.loading = true;
470
+ this.error = null;
471
+
472
+ // Fetch OIDC token if URL provided
473
+ if (this.oidcUrl) {
474
+ await this.fetchToken();
475
+ }
476
+
477
+ // Fetch form schema and data
478
+ await Promise.all([this.fetchSchema(), this.fetchFormData()]);
479
+
480
+ this.loading = false;
481
+ } catch (err) {
482
+ this.error = err.message || 'Failed to initialize form';
483
+ this.loading = false;
484
+ }
485
+ }
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
+
561
+ async fetchToken() {
562
+ try {
563
+ const response = await fetch(this.oidcUrl, {
564
+ credentials: 'include',
565
+ });
566
+
567
+ if (!response.ok) {
568
+ throw new Error('Failed to authenticate');
569
+ }
570
+
571
+ const data = await response.text();
572
+ this.token = data;
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
+ }
580
+ } catch (err) {
581
+ console.error('Token fetch error:', err);
582
+ throw new Error('Authentication failed');
583
+ }
584
+ }
585
+
586
+ async fetchSchema() {
587
+ const url = `${this.fbmsBaseUrl}/api/v1/forms/${this.fbmsFormFname}`;
588
+ const headers = {
589
+ 'content-type': 'application/jwt',
590
+ };
591
+
592
+ if (this.token) {
593
+ headers['Authorization'] = `Bearer ${this.token}`;
594
+ }
595
+
596
+ const response = await fetch(url, {
597
+ credentials: 'same-origin',
598
+ headers,
599
+ });
600
+
601
+ if (!response.ok) {
602
+ throw new Error(`Failed to fetch schema: ${response.statusText}`);
603
+ }
604
+
605
+ const data = await response.json();
606
+ this.fbmsFormVersion = data.version;
607
+ this.schema = data.schema || data;
608
+ this.uiSchema = data.metadata;
609
+ }
610
+
611
+ async fetchFormData() {
612
+ const url = `${this.fbmsBaseUrl}/api/v1/submissions/${this.fbmsFormFname}?safarifix=${Math.random()}`;
613
+ const headers = {
614
+ 'content-type': 'application/jwt',
615
+ };
616
+
617
+ if (this.token) {
618
+ headers['Authorization'] = `Bearer ${this.token}`;
619
+ }
620
+
621
+ try {
622
+ const response = await fetch(url, {
623
+ credentials: 'same-origin',
624
+ headers,
625
+ });
626
+
627
+ if (response.ok) {
628
+ const payload = await response.json();
629
+ this._formData = payload?.answers ?? {}; // Use private property
630
+ this.initialFormData = this.deepClone(this._formData); // Use deepClone
631
+ } else {
632
+ this._formData = {};
633
+ this.initialFormData = {};
634
+ }
635
+ this.hasChanges = false;
636
+ this.requestUpdate();
637
+ } catch (err) {
638
+ // Non-critical error
639
+ console.warn('Could not fetch form data:', err);
640
+ this._formData = {};
641
+ this.initialFormData = {};
642
+ this.hasChanges = false;
643
+ this.requestUpdate();
644
+ }
645
+ }
646
+
647
+ updateStateFlags() {
648
+ // Clear status messages when user makes changes
649
+ this.submitSuccess = false;
650
+ this.validationFailed = false;
651
+ this.submissionError = null;
652
+
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);
757
+
758
+ // Clear field error on change
759
+ if (this.fieldErrors[fieldPath]) {
760
+ this.fieldErrors = { ...this.fieldErrors };
761
+ delete this.fieldErrors[fieldPath];
762
+ }
763
+
764
+ this.updateStateFlags();
765
+ }
766
+
767
+ handleArrayChange(fieldPath, index, event) {
768
+ const currentArray = this.getNestedValue(fieldPath) || [];
769
+ const newArray = [...currentArray];
770
+ newArray[index] = event.target.value;
771
+ this.setNestedValue(fieldPath, newArray);
772
+
773
+ this.updateStateFlags();
774
+ }
775
+
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
+
825
+ const errors = {};
826
+
827
+ // Check required fields
828
+ required.forEach((fieldName) => {
829
+ const fieldPath = basePath ? `${basePath}.${fieldName}` : fieldName;
830
+ const value = this.getNestedValue(fieldPath);
831
+ if (value === undefined || value === null || value === '') {
832
+ const customMsg = this.getCustomErrorMessage(fieldPath, 'required');
833
+ errors[fieldPath] = customMsg || 'This field is required';
834
+ }
835
+ });
836
+
837
+ // Type validation
838
+ Object.entries(properties).forEach(([fieldName, fieldSchema]) => {
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
+ }
853
+
854
+ if (value !== undefined && value !== null && value !== '') {
855
+ // Email validation
856
+ if (fieldSchema.format === 'email') {
857
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
858
+ if (!emailRegex.test(value)) {
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';
873
+ }
874
+ }
875
+
876
+ // Number validation
877
+ if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
878
+ const num = Number(value);
879
+ if (isNaN(num)) {
880
+ const customMsg = this.getCustomErrorMessage(fieldPath, 'type');
881
+ errors[fieldPath] = customMsg || 'Must be a number';
882
+ } else {
883
+ if (fieldSchema.minimum !== undefined && num < fieldSchema.minimum) {
884
+ const customMsg = this.getCustomErrorMessage(fieldPath, 'minimum');
885
+ errors[fieldPath] = customMsg || `Must be at least ${fieldSchema.minimum}`;
886
+ }
887
+ if (fieldSchema.maximum !== undefined && num > fieldSchema.maximum) {
888
+ const customMsg = this.getCustomErrorMessage(fieldPath, 'maximum');
889
+ errors[fieldPath] = customMsg || `Must be at most ${fieldSchema.maximum}`;
890
+ }
891
+ }
892
+ }
893
+
894
+ // String length validation
895
+ if (fieldSchema.type === 'string') {
896
+ if (fieldSchema.minLength && value.length < fieldSchema.minLength) {
897
+ const customMsg = this.getCustomErrorMessage(fieldPath, 'minLength');
898
+ errors[fieldPath] = customMsg || `Must be at least ${fieldSchema.minLength} characters`;
899
+ }
900
+ if (fieldSchema.maxLength && value.length > fieldSchema.maxLength) {
901
+ const customMsg = this.getCustomErrorMessage(fieldPath, 'maxLength');
902
+ errors[fieldPath] = customMsg || `Must be at most ${fieldSchema.maxLength} characters`;
903
+ }
904
+ }
905
+ }
906
+ });
907
+
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;
915
+ }
916
+
917
+ async handleSubmit(event) {
918
+ event.preventDefault();
919
+
920
+ // Clear previous status messages
921
+ this.submitSuccess = false;
922
+ this.validationFailed = false;
923
+ if (!this.validateForm()) {
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
+ }
932
+ return;
933
+ }
934
+
935
+ // Prevent double-submission
936
+ if (this.submitting) {
937
+ return;
938
+ }
939
+
940
+ await this.submitWithRetry(false);
941
+ }
942
+
943
+ async submitWithRetry(isRetry = false) {
944
+ try {
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
+
952
+ const body = {
953
+ username: this.decoded.sub,
954
+ formFname: this.fbmsFormFname,
955
+ formVersion: this.fbmsFormVersion,
956
+ timestamp: Date.now(),
957
+ answers: this.formData,
958
+ };
959
+
960
+ const url = `${this.fbmsBaseUrl}/api/v1/submissions/${this.fbmsFormFname}`;
961
+ const headers = {
962
+ 'content-type': 'application/json',
963
+ };
964
+
965
+ if (this.token) {
966
+ headers['Authorization'] = `Bearer ${this.token}`;
967
+ }
968
+
969
+ const response = await fetch(url, {
970
+ method: 'POST',
971
+ credentials: 'same-origin',
972
+ headers,
973
+ body: JSON.stringify(body),
974
+ });
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
+
1009
+ if (!response.ok) {
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);
1023
+ }
1024
+
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
+ }
1050
+
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
+ }
1074
+ } catch (err) {
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
+ }
1091
+ } finally {
1092
+ this.submitting = false;
1093
+ }
1094
+ }
1095
+
1096
+ handleReset() {
1097
+ this.formData = {};
1098
+ this.fieldErrors = {};
1099
+ this.requestUpdate();
1100
+ }
1101
+
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';
1146
+
1147
+ return x`
1148
+ <div class="form-group">
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
+ }
1164
+
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;
1170
+
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
+ )}
1182
+ </div>
1183
+ `;
1184
+ }
1185
+
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 : [];
1238
+
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)
1295
+ if (enumValues) {
1296
+ return x`
1297
+ <select
1298
+ id="${fieldPath}"
1299
+ name="${fieldPath}"
1300
+ .value="${value || ''}"
1301
+ @change="${(e) => this.handleInputChange(fieldPath, e)}"
1302
+ >
1303
+ <option value="">-- Select --</option>
1304
+ ${enumValues.map(
1305
+ (opt) => x` <option value="${opt}" ?selected="${value === opt}">${opt}</option> `
1306
+ )}
1307
+ </select>
1308
+ `;
1309
+ }
1310
+
1311
+ // Boolean - render as checkbox
1312
+ if (type === 'boolean') {
1313
+ return x`
1314
+ <div class="checkbox-item">
1315
+ <input
1316
+ type="checkbox"
1317
+ id="${fieldPath}"
1318
+ name="${fieldPath}"
1319
+ .checked="${!!value}"
1320
+ @change="${(e) => this.handleInputChange(fieldPath, e)}"
1321
+ />
1322
+ <label for="${fieldPath}">${fieldSchema.title || fieldPath.split('.').pop()}</label>
1323
+ </div>
1324
+ `;
1325
+ }
1326
+
1327
+ // String with format
1328
+ if (type === 'string') {
1329
+ if (format === 'email') {
1330
+ return x`
1331
+ <input
1332
+ type="email"
1333
+ id="${fieldPath}"
1334
+ name="${fieldPath}"
1335
+ .value="${value || ''}"
1336
+ @input="${(e) => this.handleInputChange(fieldPath, e)}"
1337
+ />
1338
+ `;
1339
+ }
1340
+
1341
+ if (format === 'date') {
1342
+ return x`
1343
+ <input
1344
+ type="date"
1345
+ id="${fieldPath}"
1346
+ name="${fieldPath}"
1347
+ .value="${value || ''}"
1348
+ @input="${(e) => this.handleInputChange(fieldPath, e)}"
1349
+ />
1350
+ `;
1351
+ }
1352
+
1353
+ if (uiOptions['ui:widget'] === 'textarea') {
1354
+ return x`
1355
+ <textarea
1356
+ id="${fieldPath}"
1357
+ name="${fieldPath}"
1358
+ .value="${value || ''}"
1359
+ @input="${(e) => this.handleInputChange(fieldPath, e)}"
1360
+ ></textarea>
1361
+ `;
1362
+ }
1363
+
1364
+ // Default text input
1365
+ return x`
1366
+ <input
1367
+ type="text"
1368
+ id="${fieldPath}"
1369
+ name="${fieldPath}"
1370
+ .value="${value || ''}"
1371
+ @input="${(e) => this.handleInputChange(fieldPath, e)}"
1372
+ />
1373
+ `;
1374
+ }
1375
+
1376
+ // Number
1377
+ if (type === 'number' || type === 'integer') {
1378
+ return x`
1379
+ <input
1380
+ type="number"
1381
+ id="${fieldPath}"
1382
+ name="${fieldPath}"
1383
+ .value="${value || ''}"
1384
+ step="${type === 'integer' ? '1' : 'any'}"
1385
+ @input="${(e) => this.handleInputChange(fieldPath, e)}"
1386
+ />
1387
+ `;
1388
+ }
1389
+
1390
+ // Fallback
1391
+ return x`
1392
+ <input
1393
+ type="text"
1394
+ id="${fieldPath}"
1395
+ name="${fieldPath}"
1396
+ .value="${value || ''}"
1397
+ @input="${(e) => this.handleInputChange(fieldPath, e)}"
1398
+ />
1399
+ `;
1400
+ }
1401
+
1402
+ render() {
1403
+ if (this.error) {
1404
+ return x`
1405
+ <div class="container">
1406
+ <div class="error"><strong>Error:</strong> ${this.error}</div>
1407
+ </div>
1408
+ `;
1409
+ }
1410
+
1411
+ if (this.loading) {
1412
+ return x`
1413
+ <div class="container">
1414
+ <div class="loading">Loading form...</div>
1415
+ </div>
1416
+ `;
1417
+ }
1418
+
1419
+ if (!this.schema || !this.schema.properties) {
1420
+ return x`
1421
+ <div class="container">
1422
+ <div class="error">Invalid form schema</div>
1423
+ </div>
1424
+ `;
1425
+ }
1426
+
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
+ : ''}
1435
+
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
+ }
1450
+
1451
+ // Regular form view (rest of existing render code)
1452
+ const hasFields = Object.keys(this.schema.properties).length > 0;
1453
+
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
+ `}
1527
+ </div>
1528
+ `;
1529
+ }
1530
+ }
1531
+
1532
+ customElements.define('form-builder', FormBuilder);
1533
+ //# sourceMappingURL=form-builder.js.map