From 0442e1186ae642c86947d03961fde7d461ba054d Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Feb 2026 21:23:47 +0000
Subject: [PATCH] =minor Form.js fixes
---
/dev/null | 1857 --------------------------------------------------------
inc/meta/Form.php | 12
assets/js/min/form.min.js | 2
inc/managers/SEO/_setup.php | 2
assets/js/concise/FormController.js | 50
5 files changed, 42 insertions(+), 1,881 deletions(-)
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index 8996cbf..5f26f7e 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -863,7 +863,8 @@
let data = {};
data.repeater = repeater;
repeater.append(this.templates.create(repeater.dataset.repeaterId, data));
- this.initializeFields(repeater, this.getField(repeater).config??{});
+ let form = this.getForm(repeater);
+ this.initializeFields(repeater, form);
this.a11y.announce('Row added');
}
removeRepeaterRow(row) {
@@ -919,11 +920,11 @@
}
addTagListListeners(el) {
el.addEventListener('click', this.tagListClick);
- el.addEventListener('keypress', this.tagListInput, {passive: true})
+ el.addEventListener('keypress', this.tagListInput);
}
removeTagListListeners(el) {
el.removeEventListener('click', this.tagListClick);
- el.removeEventListener('keypress', this.tagListInput)
+ el.removeEventListener('keypress', this.tagListInput);
}
handleTagListClick(e) {
@@ -1129,25 +1130,32 @@
}
});
}
- checkForCharacterLimits(form) {
- if (!form.querySelector(this.selectors.limits.hasLimit)) return;
- this.countUpdaters = this.updateCount.bind(this);
+ checkForCharacterLimits(form) {
+ if (!form.querySelector(this.selectors.limits.hasLimit)) return;
+ this.countUpdaters = this.updateCount.bind(this);
- form.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach(input => {
- let id = window.generateID('limit');
- input.dataset.charLimitId = id;
- let config = {
- element: input,
- form: form.dataset.formId,
- ui: window.uiFromSelectors(this.selectors.limits, input.closest('.field'))
- };
- config.ui.limit.textContent = input.dataset.limit;
- this.charLimits.set(id, config);
+ form.querySelectorAll(this.selectors.limits.hasLimit).forEach(field => {
+ const input = field.querySelector('input, textarea, select');
+ if (!input) return;
- this.addCharacterLimitListeners(input);
- });
+ let id = window.generateID('limit');
+ input.dataset.charLimitId = id;
+ input.dataset.limit = field.dataset.limit;
+ let config = {
+ element: input,
+ form: form.dataset.formId,
+ ui: window.uiFromSelectors(this.selectors.limits, field)
+ };
+
+ if (config.ui.limit) {
+ config.ui.limit.textContent = field.dataset.limit;
}
+
+ this.charLimits.set(id, config);
+ this.addCharacterLimitListeners(input);
+ });
+ }
addCharacterLimitListeners(input) {
input.addEventListener('input', this.countUpdaters, {passive: true});
}
@@ -1965,10 +1973,10 @@
});
this.tagLists.clear();
}
- if(this.charLimits.size > 0) {
+ if (this.charLimits.size > 0) {
Array.from(this.charLimits.values()).forEach(charLimit => {
- charLimit.removeEventListener('input', this.countUpdaters);
- })
+ charLimit.element.removeEventListener('input', this.countUpdaters);
+ });
}
this.inputs.clear();
this.forms.clear();
diff --git a/assets/js/min/form.min.js b/assets/js/min/form.min.js
index e36ea87..a23602d 100644
--- a/assets/js/min/form.min.js
+++ b/assets/js/min/form.min.js
@@ -1 +1 @@
-(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators()}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab.content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-limit]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe(((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter((e=>e.src===window.location.pathname));for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)}))}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);const a=document.createElement("div");a.className="pendingChanges",a.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n <button class="restore" type="button" data-form-id="${e}">Restore</button>\n <button class="discard" type="button" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0;let e={fields:t};this.populate.populate(i,e),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.delete(e),this.a11y.announce("Previous changes discarded"),a.remove()}))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some((e=>e.checked)))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).items.forEach((e=>{this.checkFieldDependency(e,t.dataset.field)}))}if("repeater"===t.dataset.fieldType||"tag-list"===t.dataset.fieldType)return void this.updateCollectionField(t);let s=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),s)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target),this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,(()=>this.validateField(i)),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);i.changes[e]=t,this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{this.changes.size>0&&await this.backup()}),2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then((()=>{})),this.changes.delete(e))}registerForm(e,t){if(Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:{autoUpload:t.autoUpload??!1,imageMeta:t.imageMeta??!0,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",showStatus:t.showStatus??!0,showSummary:t.showSummary??!1,cache:t.cache??!0,ignore:t.ignore??[]},ui:window.uiFromSelectors(this.selectors.forms,e)};return this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach(((t,s)=>{t.items=t.items.filter((t=>t.form!==e)),0===t.items.length&&this.dependencies.delete(s)})),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach((e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}})),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter((t=>t.form===e));s.length>0&&s.forEach((e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)}))}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find((e=>e.field?.dataset.field===i));if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach((e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),t.append(e)}})),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).map((e=>{this.getItem(e,t?.id)}))}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach((e=>{this.quillInstances.get(t.id).add(e)}))}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach((t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)}))}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e)}))}}),window.Sortable&&(s.sortable=new Sortable(t,{handle:this.selectors.repeater.header,animation:150,onEnd:()=>{this.reindexList(t)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)}))}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e,e.append(this.templates.create(e.dataset.repeaterId,t)),this.initializeFields(e,this.getField(e).config??{}),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove(),this.reindexList(t),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t)})),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)}))}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput,{passive:!0})}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const l=e.name.replace("new_","");i[l]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find((e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e)));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const l=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${l}:${s}`,e.id=`${t.fieldName}:${l}:${s}`,e.value=i[s]||""})),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach((t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";if(!this.dependencies.has(s)){let e=document.querySelector(`[field="${s}"]`);e&&this.dependencies.set(s,{element:e,items:[]})}let r=this.dependencies.get(s);r.items.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,r),this.checkFieldDependency(r,s)}))}checkFieldDependency(e,t){const s=this.dependencies.get(t);if(!s)return;const i=this.getFieldCheckedValue(s.element),a=this.evaluateCondition(i,e.requiredValue,e.operator);this.toggleFieldVisibility(e.field,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(`${this.selectors.limits.hasLimit}`).forEach((t=>{let s=window.generateID("limit");t.dataset.charLimitId=s;let i={element:t,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t.closest(".field"))};i.ui.limit.textContent=t.dataset.limit,this.charLimits.set(s,i),this.addCharacterLimitListeners(t)})))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta)}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,s)=>this.validateStep(e,t)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach((e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputs))})),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe(((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter((e=>e.dataset.tab===s.current))[0]??!1;if(!e)return;const i=e.dataset.step,a=t.ui.sections.length;window.showProgress(t.ui.tabs.progress,i,a)}})),this.forms.set(t.id,t))}validateStep(e,t){const s=e.closest("[data-form-id]")?.dataset.formId;if(!s)return!0;if(!this.forms.get(s))return!0;return Array.from(this.inputs.values()).filter((t=>t&&t.form===s&&t.section===e.dataset.tab&&!t.element.closest("[hidden]"))).every((e=>!0===this.validateField(e.element)))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){const t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;Array.from(e.children).forEach(((e,s)=>{e.dataset.index=`${s}`;e.querySelectorAll("input, select, textarea").forEach((i=>{if("file"===i.type)return;i.dataset.field||i.name.split(":").pop();window.prefixInput(i,`${t}:${s}:`,e)}))})),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t.querySelector("input, select, textarea"));this.updateItem(t.dataset.field,a,i)}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),i.ui.success&&(i.ui.success.hidden=!0),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.success&&(i.ui.success.hidden=!1),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout((()=>i.ui.status.status.hidden=!0),"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":break;case"location":return this.getLocationValue(e,s);case"selector":case"upload":return this.getHiddenInputValue(e,s,i);case"true-false":return"1"===e.value||"on"===e.value||"true"===e.value;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter((e=>e.checked)).map((e=>e.value))}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find((e=>e.checked));return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){t.container||(t.container=t.field?.querySelector(".repeater-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t={};e.querySelectorAll("[data-field]").forEach((e=>{t[e.dataset.field]=this.getFieldValue(e)})),s.push(t)})),s}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach((e=>{i[e.dataset.field]=e.value})),s.push(i)})),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach((e=>{s[e.dataset.locationField]=e.value})),s}getHiddenInputValue(e,t,s){return t.value||(t.value=t.field?.querySelector(`input[type=hidden][name="${s}"]`),this.saveItem(t)),t.value.value}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map((e=>this.getDisplayLabel(t,e))).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach(((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)})),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach((e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find((e=>!this.isEmptyValue(e)))||"",a=Object.entries(e).filter((([e,t])=>!this.isEmptyValue(t)));a.length>1?t.textContent=a.map((([e,t])=>t)).join(", "):t.textContent=s,i.appendChild(t)})),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if("upload"===s){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach((e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)})),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find((e=>e.value===t));if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach((e=>{this.removeFormListeners(e)})),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach((e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()})),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach((e=>{this.removeQuantityListeners(e.element)})),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach((e=>{this.removeTagListListeners(e.element)})),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach((e=>{e.removeEventListener("input",this.countUpdaters)})),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=new e)}))}))})();
\ No newline at end of file
+(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.queue=window.jvbQueue,this.populate=window.jvbPopulate,this.changes=new Map,this.forms=new Map,this.inputs=new Map,this.repeaters=new Map,this.tagLists=new Map,this.charLimits=new Map,this.quantityFields=new Map,this.quillInstances=new Map,this.dependencies=new Map,this.subscribers=new Set,this.isRestoring=!1,this.hasListeners=!1,this.summaryTemplate=!1,this.init()}init(){this.templates=window.jvbTemplates,this.defineSummaryTemplate(),this.initElements(),this.initListeners(),this.initStore(),this.initValidators()}initElements(){this.inputSelectors="input, textarea, select",this.selectors={tabs:{nav:"nav.tabs",sections:".tab.content",progress:{progress:".progress",fill:".progress .fill",details:".progress .details",icon:".progress .icon"},buttons:"nav.tabs button"},dependsOn:"[data-depends-on]",forms:{status:{status:".fstatus",message:".fstatus .message",icon:".fstatus .icon",actions:".fstatus .actions"}},inputs:this.inputSelectors,fields:{field:".field",label:"label",success:".success",error:".error",message:".validation-message"},repeater:{repeater:".repeater",header:".repeater-row-header",remove:".remove-row",add:".add-repeater-row",template:"template",items:".repeater-items",inputs:this.inputSelectors},tagList:{tagList:".field.tag-list",input:".row",add:".add-tag",remove:".remove-tag",label:".tag-label",items:".tag-items",item:".tag-item",inputs:this.inputSelectors,value:'input[type="hidden"]'},tag:{label:".tag-label"},number:{number:".field div.quantity",increase:"button.increase",decrease:"button.decrease",input:'input[type="number"]'},limits:{hasLimit:"[data-limit]",limit:".limit",current:".current"}}}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.blurHandler=this.handleBlur.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.quantityClick=this.handleQuantityClick.bind(this),this.repeaterClick=this.handleRepeaterClick.bind(this),this.tagListClick=this.handleTagListClick.bind(this),this.tagListInput=this.handleTagListInput.bind(this)}addFormListeners(e){e.addEventListener("click",this.clickHandler),e.addEventListener("change",this.changeHandler),e.addEventListener("input",this.inputHandler),e.addEventListener("blur",this.blurHandler),e.addEventListener("submit",this.submitHandler)}removeFormListeners(e){e.removeEventListener("click",this.clickHandler),e.removeEventListener("change",this.changeHandler),e.removeEventListener("input",this.inputHandler),e.removeEventListener("blur",this.blurHandler),e.removeEventListener("submit",this.submitHandler)}initStore(){const e=window.jvbStore.register("forms",{storeName:"forms",keyPath:"id",indexes:[{name:"src",keyPath:"src"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:1008e4});this.store=e.forms,this.store.subscribe(((e,t)=>{if("data-ready"===e){let e=this.store.getFiltered().filter((e=>e.src===window.location.pathname));for(let t of e)this.showPendingNotification(t.id,t.changes)}else"operation-status"===e&&"completed"===t.status&&t.config&&this.store.delete(t.config.id)}))}showPendingNotification(e,t){let s=this.forms.get(e);if(!s)return;let i=s.element;if(!i)return void console.warn(`Form element not found for: ${e}`);const a=document.createElement("div");a.className="pendingChanges",a.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n <button class="restore" type="button" data-form-id="${e}">Restore</button>\n <button class="discard" type="button" data-form-id="${e}">Discard</button>`,i.insertBefore(a,s.ui.status.status),a.querySelector(".restore").addEventListener("click",(async()=>{this.isRestoring=!0;let e={fields:t};this.populate.populate(i,e),this.a11y.announce("Previous changes restored"),this.isRestoring=!1,a.remove()})),a.querySelector(".discard").addEventListener("click",(async()=>{await this.store.delete(e),this.a11y.announce("Previous changes discarded"),a.remove()}))}initValidators(){this.validators={email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with https://"},phone:{pattern:/^[\d\s\-+().]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const s=parseFloat(e);if(isNaN(s))return"Please enter a valid number";const i=t.dataset.min,a=t.dataset.max;return void 0!==i&&s<parseFloat(i)?`Value must be at least ${i}`:!(void 0!==a&&s>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const s=t.dataset.minlength,i=t.dataset.maxlength;return s&&e.length<parseInt(s)?`Must be at least ${s} characters`:!(i&&e.length>parseInt(i))||`Must be no more than ${i} characters`}}}}validateField(e){const t=this.performValidation(e);return this.updateValidationUI(e,t),t.isValid}performValidation(e){const t=e.closest(".field"),s=this.getFieldCheckedValue(e);if(!s&&!e.required)return{isValid:!0,message:""};if(e.required)if("checkbox"===e.type){if(!e.checked)return{isValid:!1,message:"This field is required"}}else if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`);if(!Array.from(t).some((e=>e.checked)))return{isValid:!1,message:"Please select an option"}}else if(!s)return{isValid:!1,message:"This field is required"};if(e.checkValidity&&!e.checkValidity())return{isValid:!1,message:e.validationMessage};if(s&&Object.hasOwn(t.dataset,"pattern")){if(!new RegExp(t.dataset.pattern).test(s))return{isValid:!1,message:t.dataset.validationMessage||"Invalid format"}}if(Object.hasOwn(t.dataset,"validate")||e.type){const i=this.validators[t.dataset.validate||e.type];if(i&&i.pattern&&!i.pattern.test(s))return{isValid:!1,message:i.message};if(i&&i.test){const e=i.test(s,t);if(!0!==e)return{isValid:!1,message:e}}}return{isValid:!0,message:""}}updateValidationUI(e,t){t.isValid?this.showSuccess(e,t.message):this.showError(e,t.message)}handleClick(e){let t=this.getForm(e.target);if(!t)return;const s=window.targetCheck(e,"[data-action]");if(s){switch(s.dataset.action){case"clear-form":this.store.delete(t.id),t.element.reset(),t.ui.status.status.hidden=!0,this.a11y.announce("Form cleared, starting fresh");break;case"dismiss-restore":t.ui.status.status.hidden=!0}}}handleChange(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getField(e.target);if(this.dependencies.has(t.dataset.field)){this.dependencies.get(t.dataset.field).items.forEach((e=>{this.checkFieldDependency(e,t.dataset.field)}))}if("repeater"===t.dataset.fieldType||"tag-list"===t.dataset.fieldType)return void this.updateCollectionField(t);let s=this.getForm(e.target);this.updateItem(t.dataset.field,this.getFieldValue(e.target),s)}handleBlur(e){if(e.target.closest("[data-ignore]")||this.isRestoring)return;let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target).dataset.field;window.debouncer.cancel(`form:${t.id}:validate:${s}`),this.validateField(e.target),this.updateItem(s,this.getFieldValue(e.target),t)}handleInput(e){let t=this.getForm(e.target);if(!t)return;let s=this.getField(e.target);if(!s)return;const i=e.target,a=s.dataset.field;this.showFormStatus(t.id,"pending"),window.debouncer.schedule(`form:${t.id}:validate:${a}`,(()=>this.validateField(i)),500)}async handleSubmit(e){let t=this.getForm(e.target);if(t){if(this.subscribers.size>0)if(e.preventDefault(),t.options.cache){this.cancelBackup(),await this.backup();const e=await this.store.get(t.id);this.notify("form-submit",{config:t,data:e.changes})}else this.notify("form-submit",{config:t,data:this.changes.get(t.id)?.changes??{}});if(t.options.showSummary){const e=await this.store.get(t.id);this.showSummary({config:t,changes:e?.changes})}}}updateItem(e,t,s){this.changes.has(s.id)||this.changes.set(s.id,{id:s.id,timestamp:Date.now(),src:window.location.pathname,changes:{}});let i=this.changes.get(s.id);i.changes[e]=t,this.changes.set(s.id,i),s.options.cache&&this.scheduleBackup()}scheduleBackup(){window.debouncer.schedule("form_changes",(async()=>{this.changes.size>0&&await this.backup()}),2e3)}cancelBackup(){window.debouncer.cancel("form_changes")}async backup(){const e=new Map;for(let[t,s]of this.changes.entries()){const i=await this.store.get(t);i?e.set(t,{...i,...s,changes:{...i.changes,...s.changes},timestamp:Date.now()}):e.set(t,s)}await this.store.saveMany(e);for(let e of this.changes.keys())this.showFormStatus(e,"autosaved");this.changes.clear()}saveCache(e){if(!this.changes.has(e))return;let t=this.changes.get(e);0!==t.size&&(this.store.save(t).then((()=>{})),this.changes.delete(e))}registerForm(e,t){if(Object.hasOwn(e.dataset,"formId")&&this.forms.has(e.dataset.formId))return;Object.hasOwn(e.dataset,"formId")||(e.dataset.formId=window.generateID("form_"));const s=e.dataset.formId;this.addFormListeners(e);const i={element:e,id:s,status:"",options:{autoUpload:t.autoUpload??!1,imageMeta:t.imageMeta??!0,delay:t.delay??1500,endpoint:t.save??e.dataset.save??"",showStatus:t.showStatus??!0,showSummary:t.showSummary??!1,cache:t.cache??!0,ignore:t.ignore??[]},ui:window.uiFromSelectors(this.selectors.forms,e)};return this.initializeFields(e,i),this.forms.set(s,i),i}clearForm(e){const t=this.forms.get(e);if(!t)return;t.unsubscribeTabs&&t.unsubscribeTabs(),t.tabs&&window.jvbTabs.removeTab(t.element),t.cache&&this.changes.has(e)&&this.saveCache(e);for(let[t,s]of this.inputs.entries())s.form===e&&this.inputs.delete(t);if(this.dependencies.forEach(((t,s)=>{t.items=t.items.filter((t=>t.form!==e)),0===t.items.length&&this.dependencies.delete(s)})),Object.hasOwn(t,"hasQuill")&&this.quillInstances.has(e)){this.quillInstances.get(e).forEach((e=>{e.disable(),e.off("text-change"),e.off("selection-change");const t=e.container.parentElement,s=t?.querySelector(".ql-toolbar");if(s&&s.remove(),e.setText(""),t&&t.classList.contains("editor-container")){const e=t.nextElementSibling;"TEXTAREA"===e?.tagName&&(e.style.display=""),t.remove()}})),this.quillInstances.delete(e)}let s={repeater:this.repeaters,tagList:this.tagLists,charLimit:this.charLimits,quantity:this.quantityFields};for(let[t,i]of Object.entries(s)){if(0===i.size)continue;let s=Array.from(i.values()).filter((t=>t.form===e));s.length>0&&s.forEach((e=>{switch(t){case"repeater":this.removeRepeaterListeners(e.element);break;case"tagList":this.removeTagListListeners(e.element);break;case"charLimit":this.removeCharacterLimitListeners(e.element);break;case"quantity":this.removeQuantityListeners(e.element)}i.has(e.id)&&i.delete(e.id)}))}this.removeFormListeners(t.element),this.forms.delete(e),window.debouncer.cancel("form_changes")}defineSummaryTemplate(){this.summaryTemplate=!0;let e=this;this.templates.define("formSummary",{refs:{result:".result",h3:"h3",p:"p"},setup({el:t,refs:s,manyRefs:i,data:a}){const r=["sendAll",...a.config.options.ignore??[]];for(let[i,n]of Object.entries(a.changes)){if(r.includes(i)||e.isEmptyValue(n))continue;let a=Array.from(e.inputs.values()).find((e=>e.field?.dataset.field===i));if(!a)continue;let l=s.result.cloneNode(!0),o=l.querySelector("h3"),d=l.querySelector("p");const c=a.field?.querySelector("legend");o.textContent=c?c.textContent.replace("*","").trim():a.ui.label?.textContent.replace("*","").trim();const u=e.formatValueForSummary(n,a);u instanceof HTMLElement?d.replaceWith(u):d.textContent=u,t.append(l)}let n=a.config?.element?.querySelectorAll("[data-upload-field]");n&&n.forEach((e=>{let i=e.querySelector("h2")?.textContent??"Upload:",a=e.querySelectorAll(".item-grid.preview img"),r=s.result.cloneNode(!0);if(a){let e=s.result.cloneNode(!0),n=r.querySelector("h3"),l=r.querySelector("p");l?.remove(),n&&(n.textContent=i),a.forEach((t=>{t=t.cloneNode(!0),e.append(t)})),t.append(e)}})),s.result?.remove(),a.config.element.after(t),window.fade(a.config.element,!1)}})}initializeFields(e,t=null){const s={"[data-editor]":()=>this.checkForQuill(e,t),"div.quantity":()=>this.checkForQuantity(e),".repeater":()=>this.checkForRepeaters(e,t),".field.tag-list":()=>this.checkForTagLists(e),"[data-depends-on]":()=>this.checkForConditionalFields(e),"[data-limit]":()=>this.checkForCharacterLimits(e),"[data-uploader],[data-upload-field]":()=>this.checkForImageUploads(e,t),"nav.tabs":()=>this.checkForTabs(e,t),'[data-type="selector"]':()=>this.checkForSelectors(e)};for(const[t,i]of Object.entries(s))e.querySelector(t)&&i();Array.from(e.querySelectorAll(this.inputSelectors)).map((e=>{this.getItem(e,t?.id)}))}checkForQuill(e,t){if(!e.querySelector("[data-editor]"))return;t&&!Object.hasOwn(t,"hasQuill")&&(t.hasQuill=!0,this.forms.set(t.id,t)),this.quillInstances.has(t.id)||this.quillInstances.set(t.id,new Set);window.jvbQuill(e).forEach((e=>{this.quillInstances.get(t.id).add(e)}))}checkForQuantity(e){e.querySelector(this.selectors.number.number)&&e.querySelectorAll(this.selectors.number.number).forEach((t=>{let s={id:window.generateID("quant"),form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.number,t),element:t};t.dataset.numId=s.id,this.quantityFields.set(s.id,s),this.addQuantityListeners(t)}))}addQuantityListeners(e){e.addEventListener("click",this.quantityClick)}removeQuantityListeners(e){e.removeEventListener("click",this.quantityClick)}handleQuantityClick(e){let t=this.quantityFields.get(e.target.closest("[data-num-id]")?.dataset.numId);if(!t)return;let s=0;if(t.ui.increase.contains(e.target)?s++:t.ui.decrease.contains(e.target)&&s--,0===s)return;this.getField(e.target);let i=t.ui.input.step;i=Math.max(i,1),e.ctrlKey&&e.shiftKey?i*=50:e.ctrlKey?i*=5:e.shiftKey&&(i*=10);let a=""===t.ui.input.value?0:parseFloat(t.ui.input.value);t.ui.input.value=a+i*s,a=parseFloat(t.ui.input.value),t.ui.input.min&&a<t.ui.input.min?(t.ui.input.value=t.ui.input.min,t.ui.decrease.disabled=!0):t.ui.input.max&&a>t.ui.input.max?(t.ui.input.value=t.ui.input.max,t.ui.increase.disabled=!0):(t.ui.decrease.disabled&&(t.ui.decrease.disabled=!1),t.ui.increase.disabled&&(t.ui.increase.disabled=!1))}checkForRepeaters(e){e.querySelector(this.selectors.repeater.repeater)&&e.querySelectorAll(this.selectors.repeater.repeater).forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("repeater"),ui:window.uiFromSelectors(this.selectors.repeater,t),form:e.dataset.formId,element:t,field:this.getField(t),sortable:!1};if(!s.ui.add)return;let i=t.querySelector("template");this.templates.define(i.className,{manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((t=>{window.prefixInput(t,`${a.repeater.dataset.field}:${r}:`,e)}))}}),window.Sortable&&(s.sortable=new Sortable(t,{handle:this.selectors.repeater.header,animation:150,onEnd:()=>{this.reindexList(t)}})),t.dataset.repeaterId=s.id,this.addRepeaterListeners(t),this.repeaters.set(s.id,s)}))}addRepeaterListeners(e){e.addEventListener("click",this.repeaterClick)}removeRepeaterListeners(e){e.removeEventListener("click",this.repeaterClick)}handleRepeaterClick(e){e.target.matches(this.selectors.repeater.add)?this.addRepeaterRow(e.target.closest("[data-repeater-id]")):e.target.matches(this.selectors.repeater.remove)&&this.removeRepeaterRow(e.target.closest("[data-index]"))}addRepeaterRow(e){let t={};t.repeater=e,e.append(this.templates.create(e.dataset.repeaterId,t));let s=this.getForm(e);this.initializeFields(e,s),this.a11y.announce("Row added")}removeRepeaterRow(e){let t=e.closest("[data-repeater-id]");e.remove(),this.reindexList(t),this.a11y.announce("Row removed")}checkForTagLists(e){e.querySelectorAll(this.selectors.tagList.tagList)?.forEach((t=>{let s={id:t.querySelector("template").className??window.generateID("tagList"),ui:window.uiFromSelectors(this.selectors.tagList,t),element:t,form:e.dataset.formId,format:t.dataset.tagFormat??"first_field"};if(!s.ui.input||!s.ui.add||!s.ui.items)return;t.dataset.tagListId=s.id,s.fieldName=t.dataset.field;let i=t.querySelector("template");this.templates.define(i.className,{refs:{label:this.selectors.tagList.label},manyRefs:{inputs:this.inputSelectors},setup({el:e,refs:t,manyRefs:i,data:a}){let r=s.ui.items?.children?.length??0;e.dataset.index=r,i.inputs?.forEach((e=>{let t=e.closest(".tag-item");window.prefixInput(e,`${a.fieldName}:${r}:`,t)})),t.label&&(t.label.textContent=a.label)}}),s.ui.inputs=Array.from(t.querySelectorAll(this.selectors.tagList.inputs)),s.ui.value=Array.from(t.querySelectorAll(this.selectors.tagList.value)),this.tagLists.set(s.id,s),this.addTagListListeners(t)}))}addTagListListeners(e){e.addEventListener("click",this.tagListClick),e.addEventListener("keypress",this.tagListInput)}removeTagListListeners(e){e.removeEventListener("click",this.tagListClick),e.removeEventListener("keypress",this.tagListInput)}handleTagListClick(e){window.targetCheck(e,this.selectors.tagList.add)?this.addTagListItem(e.target.closest("[data-tag-list-id]")):window.targetCheck(e,this.selectors.tagList.remove)&&this.removeTagListItem(e.target.closest(this.selectors.tagList.item))}addTagListItem(e){let t=this.tagLists.get(e.dataset.tagListId);if(!t)return;let s,i={},a=!1,r=!0;for(let e of t.ui.inputs){const t=e.required||"true"===e.dataset.required,s=this.getFieldValue(e);s&&(a=!0);const n=this.validateField(e);t&&!s?(this.showError(e,"This field is required"),r=!1):n||(r=!1);const l=e.name.replace("new_","");i[l]=s}if(!r){this.a11y.announce("Please correct the errors before adding");const e=t.ui.inputs.find((e=>(e.required||"true"===e.dataset.required)&&!this.getFieldValue(e)));return void(e&&e.focus())}if(!a)return this.a11y.announce("Please fill in at least one field"),void t.ui.inputs[0].focus();switch(t.format){case"first_field":s=Object.values(i)[0];break;case"all_fields":s=Object.values(i).join(", ");break;default:if(t.format.includes("{")){s=t.format;for(const[e,t]of Object.entries(i))s=s.replace(`{${e}}`,t)}else s=i[t.format]??Object.values(i)[0]}let n=this.templates.create(e.dataset.tagListId,{label:s,fieldName:t.fieldName});const l=t.ui.items?.children?.length??0;n?.querySelectorAll("input[type=hidden]")?.forEach((e=>{const s=e.dataset.field;e.name=`${t.fieldName}:${l}:${s}`,e.id=`${t.fieldName}:${l}:${s}`,e.value=i[s]||""})),t.ui.items.append(n);for(let e of t.ui.inputs)["checkbox","radio"].includes(e.type)?e.checked=!1:e.value="",this.clearValidation(e);t.ui.inputs[0]?.focus(),this.updateCollectionField(e),this.a11y.announce("Item added")}removeTagListItem(e){let t=e.closest("[data-tag-list-id]");t&&(e.remove(),this.reindexList(t),this.updateCollectionField(t),this.a11y.announce("Item removed"))}handleTagListInput(e){let t=e.target,s=t.closest("[data-tag-list-id]");if(!s)return;let i=this.tagLists.get(s.dataset.tagListId);if(i&&"Enter"===e.key)if(t===i.ui.inputs[i.ui.inputs.length-1])e.preventDefault(),this.addTagListItem(t.closest("[data-tag-list-id]"));else{e.preventDefault();let s=i.ui.inputs.indexOf(t);i.ui.inputs[s+1].focus()}}checkForConditionalFields(e){e.querySelectorAll(this.selectors.dependsOn).forEach((t=>{const s=t.dataset.dependsOn,i=t.dataset.dependsValue,a=t.dataset.dependsOperatior??"==";if(!this.dependencies.has(s)){let e=document.querySelector(`[field="${s}"]`);e&&this.dependencies.set(s,{element:e,items:[]})}let r=this.dependencies.get(s);r.items.push({field:t,form:e.dataset.formId,requiredValue:i,operator:a}),this.dependencies.set(s,r),this.checkFieldDependency(r,s)}))}checkFieldDependency(e,t){const s=this.dependencies.get(t);if(!s)return;const i=this.getFieldCheckedValue(s.element),a=this.evaluateCondition(i,e.requiredValue,e.operator);this.toggleFieldVisibility(e.field,a)}evaluateCondition(e,t,s){const i=String(e||""),a=String(t||"");switch(s){case"==":default:return i===a;case"!=":return i!==a;case">":return parseFloat(i)>parseFloat(a);case"<":return parseFloat(i)<parseFloat(a);case">=":return parseFloat(i)>=parseFloat(a);case"<=":return parseFloat(i)<=parseFloat(a);case"contains":return i.includes(a);case"empty":return""===i;case"not_empty":return""!==i}}toggleFieldVisibility(e,t){const s=e.closest(".field, fieldset");s&&(s.hidden=!t,s.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}checkForCharacterLimits(e){e.querySelector(this.selectors.limits.hasLimit)&&(this.countUpdaters=this.updateCount.bind(this),e.querySelectorAll(this.selectors.limits.hasLimit).forEach((t=>{const s=t.querySelector("input, textarea, select");if(!s)return;let i=window.generateID("limit");s.dataset.charLimitId=i,s.dataset.limit=t.dataset.limit;let a={element:s,form:e.dataset.formId,ui:window.uiFromSelectors(this.selectors.limits,t)};a.ui.limit&&(a.ui.limit.textContent=t.dataset.limit),this.charLimits.set(i,a),this.addCharacterLimitListeners(s)})))}addCharacterLimitListeners(e){e.addEventListener("input",this.countUpdaters,{passive:!0})}removeCharacterLimitListeners(e){e.removeEventListener("input",this.countUpdaters,{passive:!0})}updateCount(e){let t=e.target,s=this.charLimits.get(t.dataset.charLimitId);if(!s)return;let i=t.value.length,a=t.dataset.limit;s.ui.current&&(s.ui.current.textContent=i,s.ui.current.classList.toggle("exceeded",i>=a)),i>a&&(t.value=t.value.slice(0,a))}checkForImageUploads(e,t){window.jvbUploads.scanFields(e,t.options.autoUpload,t.options.imageMeta)}checkForTabs(e,t){window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=window.jvbTabs.registerTab(e,{preCheck:(e,s)=>this.validateStep(e,t)}),t.ui.tabs=window.uiFromSelectors(this.selectors.tabs,e),t.ui.tabs.sections=Array.from(e.querySelectorAll(this.selectors.tabs.sections)),t.ui.tabs.inputs={},t.ui.tabs.sections.forEach((e=>{t.ui.tabs.inputs[e.dataset.tab]=Array.from(e.querySelectorAll(this.inputs))})),t.ui.tabs.buttons=Array.from(e.querySelectorAll(this.selectors.tabs.buttons)),t.unsubscribeTabs=window.jvbTabs.subscribe(((e,s)=>{if("tab-switched"===e&&t.ui.tabs.progress){const e=t.ui.tabs.sections.filter((e=>e.dataset.tab===s.current))[0]??!1;if(!e)return;const i=e.dataset.step,a=t.ui.sections.length;window.showProgress(t.ui.tabs.progress,i,a)}})),this.forms.set(t.id,t))}validateStep(e,t){const s=e.closest("[data-form-id]")?.dataset.formId;if(!s)return!0;if(!this.forms.get(s))return!0;return Array.from(this.inputs.values()).filter((t=>t&&t.form===s&&t.section===e.dataset.tab&&!t.element.closest("[hidden]"))).every((e=>!0===this.validateField(e.element)))}checkForSelectors(e){window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}reindexList(e){const t=e.dataset.field||e.dataset.repeaterId||e.dataset.tagListId;Array.from(e.children).forEach(((e,s)=>{e.dataset.index=`${s}`;e.querySelectorAll("input, select, textarea").forEach((i=>{if("file"===i.type)return;i.dataset.field||i.name.split(":").pop();window.prefixInput(i,`${t}:${s}:`,e)}))})),this.updateCollectionField(e)}updateCollectionField(e){const t=e.closest("[data-field]");if(!t)return;const s=t.dataset.fieldType;if(!["repeater","tag-list"].includes(s))return;const i=this.getForm(e);if(!i)return;const a=this.getFieldValue(t.querySelector("input, select, textarea"));this.updateItem(t.dataset.field,a,i)}clearValidation(e){let t=this.getField(e);if(!t)return;let s=this.getItem(e);s&&(t.classList.remove("has-error","has-success"),s.ui.success&&(s.ui.success.hidden=!0),s.ui.error&&(s.ui.error.hidden=!0),s.ui.message&&(s.ui.message.hidden=!0,s.ui.message.textContent=""))}showError(e,t="Invalid field"){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-success"),s.classList.add("has-error"),i.ui.success&&(i.ui.success.hidden=!0),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=!1,i.ui.message.textContent=t))}showSuccess(e,t=""){let s=this.getField(e);if(!s)return;let i=this.getItem(e);i&&(s.classList.remove("has-error"),s.classList.add("has-success"),i.ui.success&&(i.ui.success.hidden=!1),i.ui.error&&(i.ui.error.hidden=!0),i.ui.message&&(i.ui.message.hidden=""===t,i.ui.message.textContent=t))}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const s=document.createElement("div");s.className="form-success-message success-message",s.textContent=t.message,e.insertBefore(s,e.firstChild);const i=window.getIcon?.("check-circle");i&&(i.classList.add("success-icon"),s.prepend(i))}if(t.title||t.description){const s=document.createElement("div");if(s.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,s.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,s.appendChild(t)}))}e.insertBefore(s,e.firstChild)}if(e.dataset.formId){this.store.delete(e.dataset.formId).catch((e=>{console.warn("Failed to clear form cache:",e)}));const t=this.forms.get(e.dataset.formId);t&&(t.isDirty=!1,t.lastSaved=Date.now(),t.data={})}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully")}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const s=e.querySelector(`[data-field="${t.field}"]`);if(s){this.showError(s,t.message),s.scrollIntoView({behavior:"smooth",block:"center"});const e=s.querySelector("input, textarea, select");e&&e.focus()}}else{const s=document.createElement("div");s.className="form-error error-message",s.textContent=t.message;const i=window.getIcon?.("close-circle");i&&(i.classList.add("error-icon"),s.prepend(i)),e.insertBefore(s,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}showFormStatus(e,t,s=""){let i=this.forms.get(e);i&&i.options.showStatus&&i.ui?.status?.status&&i.status!==t&&(i.status=t,i.ui.status.status.hidden=!1,i.ui.status.status.classList.toggle("loading",["uploading","saving"].includes(t)),i.ui.status.message.textContent=""===s?this.getDefaultMessage(t):s,i.ui.status.icon.className="icon icon-"+this.getDefaultIcon(t),setTimeout((()=>i.ui.status.status.hidden=!0),"submitted"===t?3e3:1e4))}getDefaultMessage(e){return{saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",restored:"Welcome back! We've restored your previous entry.",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"}[e]??e}getDefaultIcon(e){return{autosaved:"check-circle",submitted:"check-circle",restored:"history",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[e]??""}showSummary(e){let t=this.templates.create("formSummary",e);e.config.element.after(t),window.fade(e.config.element,!1)}getForm(e){let t=e.closest("[data-form-id]");if(!t)return!1;let s=t.dataset.formId;if(!s)return!1;let i=this.forms.get(s);return i||!1}getField(e){return e.closest("[data-field]")}getFieldType(e){let t=this.getField(e);if(t)return t.dataset.fieldType}getFieldValue(e){let t=this.getFieldType(e),s=this.getItem(e),i=s.field?.dataset.field??!1;if(!i)return!1;switch(t){case"repeater":return this.getRepeaterValue(e,s);case"tag-list":return this.getTagListValue(e,s);case"group":break;case"location":return this.getLocationValue(e,s);case"selector":case"upload":return this.getHiddenInputValue(e,s,i);case"true-false":return"1"===e.value||"on"===e.value||"true"===e.value;case"checkbox":return e.name.endsWith("[]")?this.getCheckboxGroupValue(e,s):e.checked?e.value:"";default:return e.value}}getCheckboxGroupValue(e,t){return t.checkboxGroup||(t.checkboxGroup=t.field?.querySelectorAll(`input[type="checkbox"][name="${e.name}"]`),this.saveItem(t)),Array.from(t.checkboxGroup).filter((e=>e.checked)).map((e=>e.value))}getFieldCheckedValue(e){if("checkbox"===e.type){return"true-false"===this.getFieldType(e)?e.checked:e.checked?e.value:""}if("radio"===e.type){const t=document.querySelectorAll(`input[name="${e.name}"]`),s=Array.from(t).find((e=>e.checked));return s?s.value:""}return this.getFieldValue(e)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getRepeaterValue(e,t){t.container||(t.container=t.field?.querySelector(".repeater-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t={};e.querySelectorAll("[data-field]").forEach((e=>{t[e.dataset.field]=this.getFieldValue(e)})),s.push(t)})),s}getTagListValue(e,t){t.container||(t.container=t.field?.querySelector(".tag-items"),this.saveItem(t));let s=[];return Array.from(t.container.children).forEach((e=>{let t=e.querySelectorAll('input[type="hidden"]'),i={};t.forEach((e=>{i[e.dataset.field]=e.value})),s.push(i)})),s}getLocationValue(e,t){t.values||(t.values=Array.from(t.field?.querySelectorAll("[data-location-field]")),this.saveItem(t));let s={};return t.values.forEach((e=>{s[e.dataset.locationField]=e.value})),s}getHiddenInputValue(e,t,s){return t.value||(t.value=t.field?.querySelector(`input[type=hidden][name="${s}"]`),this.saveItem(t)),t.value.value}formatValueForSummary(e,t){const s=this.getFieldType(t.element);if(this.isEmptyValue(e))return"";switch(s){case"repeater":return this.formatRepeaterForSummary(e,t);case"tag-list":return this.formatTagListForSummary(e,t);case"location":return this.formatLocationForSummary(e);case"true-false":return e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatCheckboxGroupForSummary(e,t):this.getDisplayLabel(t,e);case"selector":case"upload":return this.formatHiddenFieldForSummary(e,t,s);default:return"string"==typeof e?this.getDisplayLabel(t,e):"string"==typeof e&&e.includes("\n")?this.convertLineBreaks(e):e}}formatCheckboxGroupForSummary(e,t){return e.map((e=>this.getDisplayLabel(t,e))).join(", ")}convertLineBreaks(e){const t=document.createElement("span");return t.innerHTML=e.split("\n").join("<br>"),t}formatRepeaterForSummary(e,t){const s=document.createElement("div");return s.className="summary-repeater",e.forEach(((e,i)=>{const a=document.createElement("div");a.className="summary-repeater-row";const r=document.createElement("strong");r.textContent=`Entry ${i+1}:`,a.appendChild(r);const n=document.createElement("ul");n.className="summary-repeater-fields";for(const[s,i]of Object.entries(e)){if(this.isEmptyValue(i))continue;const e=document.createElement("li"),a=t.field?.querySelector(`[data-field="${s}"]`),r=a?.closest(".field")?.querySelector("label")?.textContent.replace("*","").trim()||s;e.innerHTML=`<span class="field-label">${r}:</span> <span class="field-value">${i}</span>`,n.appendChild(e)}a.appendChild(n),s.appendChild(a)})),s}formatTagListForSummary(e,t){const s=document.createElement("div");s.className="summary-taglist";const i=document.createElement("ul");return i.className="summary-tags",e.forEach((e=>{const t=document.createElement("li");t.className="summary-tag";const s=Object.values(e).find((e=>!this.isEmptyValue(e)))||"",a=Object.entries(e).filter((([e,t])=>!this.isEmptyValue(t)));a.length>1?t.textContent=a.map((([e,t])=>t)).join(", "):t.textContent=s,i.appendChild(t)})),s.appendChild(i),s}formatLocationForSummary(e){const t=[];return e.street&&t.push(e.street),e.city&&t.push(e.city),e.province&&t.push(e.province),e.postal_code&&t.push(e.postal_code),e.country&&t.push(e.country),t.length>0?t.join(", "):e.address||""}formatHiddenFieldForSummary(e,t,s){if("upload"===s){const s=t.field?.querySelector("[data-upload-field]");if(s){const e=s.querySelectorAll(".item-grid.preview img");if(e.length>0){const t=document.createElement("div");return t.className="summary-uploads",e.forEach((e=>{const s=e.cloneNode(!0);s.style.maxWidth="100px",s.style.maxHeight="100px",t.appendChild(s)})),t}}return`${e.split(",").length} file(s) uploaded`}return e}getDisplayLabel(e,t){if(!e.element)return t;const s=e.element.type;if("radio"===s){const s=e.field.querySelectorAll(`input[type="radio"][name="${e.element.name}"]`),i=Array.from(s).find((e=>e.value===t));if(i){const t=i.closest("label")||e.field.querySelector(`label[for="${i.id}"]`);if(t)return t.textContent.replace("*","").trim()}}if("checkbox"===s&&"true-false"!==this.getFieldType(e.element)){const s=e.field.querySelector(`input[type="checkbox"][value="${t}"]`);if(s){const t=s.closest("label")||e.field.querySelector(`label[for="${s.id}"]`);if(t){const e=t.querySelector("span");return e?e.textContent.trim():t.textContent.replace("*","").trim()}}}return t}getItem(e,t=null){const s=Object.hasOwn(e.dataset,"ref");let i=s?e.dataset.ref:window.generateID("input");if(s||(e.dataset.ref=i),!this.inputs.has(i)){t||(t=e.closest("[data-form-id]")?.dataset.formId??!1);let s=this.getField(e);this.inputs.set(i,{id:i,element:e,form:t,field:s,section:e.closest("[data-tab]")?.dataset.tab??!1,ui:window.uiFromSelectors(this.selectors.fields,s)})}return this.inputs.get(i)}saveItem(e){this.inputs.set(e.id,e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>{try{s(e,t)}catch(e){console.error("HandleSelection subscriber error:",e)}}))}destroy(){this.forms.size>0&&(Array.from(this.forms.values()).forEach((e=>{this.removeFormListeners(e)})),this.forms.clear()),this.repeaters.size>0&&(Array.from(this.repeaters.values()).forEach((e=>{this.removeRepeaterListeners(e.element),e.sortable?.destroy()})),this.repeaters.clear()),this.quantityFields.size>0&&(Array.from(this.quantityFields.values()).forEach((e=>{this.removeQuantityListeners(e.element)})),this.quantityFields.clear()),this.tagLists.size>0&&(Array.from(this.tagLists.values()).forEach((e=>{this.removeTagListListeners(e.element)})),this.tagLists.clear()),this.charLimits.size>0&&Array.from(this.charLimits.values()).forEach((e=>{e.element.removeEventListener("input",this.countUpdaters)})),this.inputs.clear(),this.forms.clear(),this.charLimits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbForm=new e)}))}))})();
\ No newline at end of file
diff --git a/inc/managers/SEO/SchemaRegistry.php b/inc/managers/SEO/SchemaRegistry.php
deleted file mode 100644
index 175209f..0000000
--- a/inc/managers/SEO/SchemaRegistry.php
+++ /dev/null
@@ -1,1857 +0,0 @@
-<?php
-namespace JVBase\managers\SEO;
-
-if (!defined('ABSPATH')) {
- exit;
-}
-
-/**
- * Schema.org Registry - Centralized field and type definitions
- *
- * Field definitions use Meta.php field types and include transformer hints.
- * Types reference field names and support inheritance via 'extends'.
- */
-class SchemaRegistry
-{
- private static ?self $instance = null;
- private array $fieldDefinitions = [];
- private array $typeDefinitions = [];
- private array $typeGroups = [];
-
- private array $metaFields = ['metaTitle', 'metaDescription','socialPreviewImage', 'twitterImage'];
-
- private array $defaultMetaValues = [
- 'title' => '{{post_title}} | {{site_name}}',
- 'description' => '{{post_excerpt}}',
- 'image' => '{{featured_image}}',
- 'twitter_image' => ''
- ];
-
- public static function getInstance(): self
- {
- if (self::$instance === null) {
- self::$instance = new self();
- }
- return self::$instance;
- }
-
- public array $schemaTypes = [
- 'WebSite' => 'Web Site',
- 'Organization' => 'Organization',
- 'LocalBusiness' => ' - Local Business',
- 'TattooParlor' => ' - - Tattoo Shop',
- 'HealthBusiness' => ' - - Health Business',
- 'FoodEstablishment' => ' - - Restaurant',
- 'WebPage' => 'Web Page',
- 'CollectionPage' => ' - Collection Page',
- 'FAQPage' => ' - FAQ Page',
- 'Person' => 'Person',
- 'CreativeWork' => 'Creative Work',
- 'DefinedTerm' => ' - Defined Term',
- 'VisualArtwork' => ' - Visual Artwork',
- 'Tattoo' => ' - - Tattoo',
- 'BeforeAfter' => ' - Before & After',
- 'Product' => 'Product',
- 'Event' => 'Event',
- ];
-
- private function __construct()
- {
- $this->registerFieldDefinitions();
- $this->registerTypeDefinitions();
- $this->registerTypeGroups();
-
- do_action(BASE . 'schema_registry_loaded', $this);
- }
-
- /**
- * Get field definition for a specific field
- */
- public function getFieldDefinition(string $fieldName): ?array
- {
- $definitions = $this->getFieldDefinitions();
- return $definitions[$fieldName] ?? null;
- }
-
- /**
- * Get all field definitions
- */
- public function getFieldDefinitions(): array
- {
- return apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions);
- }
-
- public function getMetaFields(): array
- {
- return $this->metaFields;
- }
-
- public function getDefaultMetaValues(): array
- {
- return $this->defaultMetaValues;
- }
-
- /**
- * Get type definition
- */
- public function getTypeDefinition(string $type): ?array
- {
- $definitions = $this->getTypeDefinitions();
- return $definitions[$type] ?? null;
- }
-
- /**
- * Get all type definitions
- */
- public function getTypeDefinitions(): array
- {
- return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions);
- }
-
- /**
- * Get all fields for a specific type (with inheritance)
- */
- public function getFieldsForType(string $type): array
- {
- $fields = [];
-
- $typeDefinition = $this->getTypeDefinition($type);
- if (!$typeDefinition) {
- return $fields;
- }
-
- $fields = array_merge($fields, $typeDefinition['fields'] ?? []);
-
- // Handle inheritance
- if (!empty($typeDefinition['extends'])) {
- $parentFields = $this->getFieldsForType($typeDefinition['extends']);
- $fields = array_unique(array_merge($parentFields, $fields));
- }
-
- return $fields;
- }
-
- /**
- * Get Meta configuration for a schema type
- * This creates the form fields for the selected @type
- */
- public function getMetaConfigForType(string $type): array
- {
- $fields = $this->getFieldsForType($type);
- $config = [];
-
- foreach ($fields as $fieldName) {
- $fieldDef = $this->getFieldDefinition($fieldName);
- if ($fieldDef) {
- // Use the field name as the key (this IS the schema property)
- $config[$fieldName] = $fieldDef;
- }
- }
-
- return $config;
- }
-
- /**
- * Get types organized by group for UI display
- */
- public function getTypesByGroup(): array
- {
- $types = $this->getTypeDefinitions();
- $grouped = [];
-
- foreach ($types as $typeName => $config) {
- $group = $config['group'] ?? 'general';
-
- if (!isset($grouped[$group])) {
- $grouped[$group] = [
- 'label' => $this->typeGroups[$group] ?? ucfirst($group),
- 'types' => []
- ];
- }
-
- $grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName;
- }
-
- return $grouped;
- }
-
- /**
- * Register all field definitions
- * Array key = schema property name
- */
- private function registerFieldDefinitions(): void
- {
- $this->fieldDefinitions = [
- 'type' => [
- 'type' => 'select',
- 'label' => 'Type',
- 'options' => array_merge(['' => '-- Content Type'], $this->schemaTypes)
- ],
- /**************************************************************
- META FIELDS
- **************************************************************/
- 'metaTitle' => [
- 'type' => 'text',
- 'label' => 'Meta Title',
- 'hint' => 'Used in search results and when shared on social media. Leave blank to use default.',
- 'default' => '{{post_title}} | {{site_name}}'
- ],
- 'metaDescription' => [
- 'type' => 'textarea',
- 'label' => 'Meta Description',
- 'hint' => 'Brief description shown in search results and social previews.',
- 'default' => '{{post_excerpt}}',
- 'rows' => 3
- ],
- 'socialPreviewImage' => [
- 'type' => 'upload',
- 'label' => 'Social Preview Image',
- 'hint' => 'Image shown when shared on social media. Recommended: 1200x630px.',
- 'transformer' => 'image_url'
- ],
- 'twitterImage' => [
- 'type' => 'upload',
- 'label' => 'Twitter Card Image (Optional)',
- 'hint' => 'Separate image for Twitter. Falls back to main image if empty.',
- 'transformer' => 'image_url'
- ],
- /**************************************************************
- CORE IDENTITY FIELDS
- **************************************************************/
- 'name' => [
- 'type' => 'text',
- 'label' => 'Name',
- 'description' => 'The name of the item',
- 'transformer' => 'text',
- ],
-
- 'alternateName' => [
- 'type' => 'repeater',
- 'label' => 'Alternate Name(s)',
- 'description' => 'Alternative names or nicknames',
- 'transformer' => 'text_array',
- 'fields' => [
- 'name' => [
- 'type' => 'text',
- 'label' => 'Name',
- ]
- ]
- ],
-
- 'legalName' => [
- 'type' => 'text',
- 'label' => 'Legal Name',
- 'description' => 'The official legal name',
- 'transformer' => 'text',
- ],
-
- 'description' => [
- 'type' => 'textarea',
- 'label' => 'Description',
- 'description' => 'A description of the item',
- 'transformer' => 'text',
- ],
-
- 'disambiguatingDescription' => [
- 'type' => 'textarea',
- 'label' => 'Disambiguating Description',
- 'description' => 'Brief clarification to distinguish from similar items',
- 'transformer' => 'text',
- ],
-
- 'url' => [
- 'type' => 'url',
- 'label' => 'URL',
- 'description' => 'Website URL',
- 'transformer' => 'url',
- ],
-
- 'slogan' => [
- 'type' => 'text',
- 'label' => 'Slogan',
- 'description' => 'A slogan or tagline',
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- Before/After
- **************************************************************/
- 'about' => [
- 'type' => 'reference',
- 'label' => 'About (Service/Topic)',
- 'transformer' => 'reference',
- ],
-
- 'temporalCoverage' => [
- 'type' => 'text',
- 'label' => 'Time Period',
- 'description' => 'ISO 8601 format: 2024-01-10/2024-09-01',
- 'transformer' => 'text',
- ],
-
- 'associatedMedia' => [
- 'type' => 'repeater',
- 'label' => 'Associated Media',
- 'transformer' => 'image_object_array',
- 'fields' => [
- 'image' => ['type' => 'image', 'label' => 'Image'],
- 'caption' => ['type' => 'text', 'label' => 'Caption'],
- 'position' => ['type' => 'number', 'label' => 'Position'],
- ]
- ],
-
- 'additionalProperty' => [
- 'type' => 'repeater',
- 'label' => 'Additional Properties',
- 'transformer' => 'property_value_array',
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Property Name'],
- 'value' => ['type' => 'text', 'label' => 'Value'],
- ]
- ],
- /**************************************************************
- IMAGE FIELDS
- **************************************************************/
- 'image' => [
- 'type' => 'image',
- 'label' => 'Image',
- 'description' => 'Primary image',
- 'transformer' => 'image_object',
- ],
-
- 'logo' => [
- 'type' => 'upload',
- 'label' => 'Logo',
- 'transformer' => 'image_object',
- ],
-
- 'photo' => [
- 'type' => 'upload',
- 'label' => 'Photo of Location',
- 'transformer' => 'image_object',
- ],
-
- /**************************************************************
- LOCATION & CONTACT FIELDS
- **************************************************************/
- 'location' => [
- 'type' => 'location',
- 'label' => 'Location',
- 'description' => 'Physical location with address and coordinates',
- 'transformer' => 'location_complex', // Returns array with 'address' and 'geo'
- ],
-
- 'address' => [
- 'type' => 'location',
- 'label' => 'Address',
- 'description' => 'Postal address',
- 'transformer' => 'postal_address',
- ],
-
- 'geo' => [
- 'type' => 'group',
- 'label' => 'Geographic Coordinates',
- 'description' => 'Latitude and longitude',
- 'transformer' => 'geo_coordinates',
- 'fields' => [
- 'latitude' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Latitude',
- ],
- 'longitude' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Longitude',
- ]
- ]
- ],
-
- 'telephone' => [
- 'type' => 'text',
- 'subtype'=> 'tel',
- 'label' => 'Telephone',
- 'description' => 'Phone number',
- 'transformer' => 'text',
- ],
-
- 'faxNumber' => [
- 'type' => 'text',
- 'subtype'=> 'tel',
- 'label' => 'Fax Number',
- 'transformer' => 'text',
- ],
-
- 'email' => [
- 'type' => 'email',
- 'label' => 'Email',
- 'description' => 'Email address',
- 'transformer' => 'email',
- ],
-
- 'contactPoint' => [
- 'type' => 'repeater',
- 'label' => 'Contact Points',
- 'description' => 'Additional contact methods',
- 'transformer' => 'contact_point_array',
- 'fields' => [
- 'contactType' => [
- 'type' => 'text',
- 'label' => 'Contact Type',
- 'description' => 'e.g., customer service, sales',
- ],
- 'telephone' => [
- 'type' => 'text',
- 'label' => 'Phone',
- ],
- 'email' => [
- 'type' => 'email',
- 'label' => 'Email',
- ]
- ]
- ],
-
- 'potentialAction' => [
- 'type' => 'repeater',
- 'label' => 'Potential Actions',
- 'fields' => [
- 'action' => [
- 'type' => 'radio',
- 'label' => 'Action',
- 'options' => [
- 'searchAction' => 'Search Action',
- 'communicateAction' => 'Contact Action',
- 'scheduleAction' => 'Reserve Action',
- 'applyAction' => 'Estimate Action'
- ]
- ],
- 'name' => [
- 'type' => 'text',
- 'label' => 'Name',
- ],
- 'target' => [
- 'type' => 'url',
- 'label' => 'Action URL',
- ],
- 'description' => [
- 'type' => 'textarea',
- 'label' => 'Description'
- ]
- ],
- 'default' => [
- [
- 'action' => 'searchAction',
- 'target' => get_home_url(null,'/search/?s={query}')
- ]
- ],
- 'transformer' => 'potential_action_array'
- ],
-
- /**************************************************************
- HOURS & OPERATIONAL FIELDS
- **************************************************************/
- 'openingHours' => [
- 'type' => 'group',
- 'label' => 'Opening Hours',
- 'description' => 'Business hours specification',
- 'transformer' => 'opening_hours_specification',
- 'fields' => [
- 'monday' => [
- 'type' => 'group',
- 'label' => 'Monday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- 'tuesday' => [
- 'type' => 'group',
- 'label' => 'Tuesday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- 'wednesday' => [
- 'type' => 'group',
- 'label' => 'Wednesday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- 'thursday' => [
- 'type' => 'group',
- 'label' => 'Thursday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- 'friday' => [
- 'type' => 'group',
- 'label' => 'Friday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- 'saturday' => [
- 'type' => 'group',
- 'label' => 'Saturday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- 'sunday' => [
- 'type' => 'group',
- 'label' => 'Sunday',
- 'fields' => [
- 'opens' => [
- 'type' => 'time',
- 'label' => 'Opens'
- ],
- 'closes' => [
- 'type' => 'time',
- 'label' => 'Closes'
- ]
- ]
- ],
- ]
- ],
- 'hasPart' => [
- 'type' => 'repeater',
- 'label' => 'Site Navigation',
- 'description' => 'Main navigation menu items',
- 'transformer' => 'navigation_array',
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Link Text'],
- 'url' => ['type' => 'url', 'label' => 'URL'],
- 'description' => ['type' => 'textarea', 'label' => 'Description (optional)'],
- ]
- ],
-
- 'priceRange' => [
- 'type' => 'text',
- 'label' => 'Price Range',
- 'description' => 'e.g., $$, $100-$500',
- 'transformer' => 'text',
- ],
-
- 'currenciesAccepted' => [
- 'type' => 'checkbox',
- 'label' => 'Currencies Accepted',
- 'options' => [
- 'CAD' => 'CAD',
- 'USD' => 'USD',
- ],
- 'transformer' => 'text',
- ],
-
- 'paymentAccepted' => [
- 'type' => 'checkbox',
- 'label' => 'Payment Methods',
- 'options' => [
- 'Cash' => 'Cash',
- 'Credit Card' => 'Credit Card',
- 'Debit' => 'Debit',
- 'Google Pay' => 'Google Pay',
- 'Apple Pay' => 'Apple Pay',
- 'PayPal' => 'PayPal',
- 'Interac' => 'Interac',
- 'AMEX' => 'AMEX',
- ],
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- ORGANIZATION & BUSINESS FIELDS
- **************************************************************/
- 'foundingDate' => [
- 'type' => 'date',
- 'label' => 'Founding Date',
- 'description' => 'Date the organization was founded',
- 'transformer' => 'date',
- ],
-
- 'dissolutionDate' => [
- 'type' => 'date',
- 'label' => 'Dissolution Date',
- 'description' => 'Date the organization closed',
- 'transformer' => 'date',
- ],
-
- 'founders' => [
- 'type' => 'repeater',
- 'label' => 'Founders',
- 'description' => 'Name of founder(s)',
- 'fields' => [
- 'name' => [
- 'type' => 'text',
- 'label' => 'Name',
- ],
- 'url' => [
- 'type' => 'url',
- 'label' => 'URL',
- ]
- ],
- 'transformer' => 'founders',
- ],
-
- 'numberOfEmployees' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Number of Employees',
- 'transformer' => 'number',
- ],
-
- 'taxID' => [
- 'type' => 'text',
- 'label' => 'Tax ID',
- 'description' => 'Tax identification number',
- 'transformer' => 'text',
- ],
-
- 'vatID' => [
- 'type' => 'text',
- 'label' => 'VAT ID',
- 'description' => 'VAT registration number',
- 'transformer' => 'text',
- ],
-
- 'duns' => [
- 'type' => 'text',
- 'label' => 'D-U-N-S Number',
- 'description' => 'Dun & Bradstreet number',
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- SOCIAL & LINKS
- **************************************************************/
- 'sameAs' => [
- 'type' => 'repeater',
- 'label' => 'Social Media & Links',
- 'description' => 'URLs to social profiles and related pages',
- 'transformer' => 'url_array',
- 'fields' => [
- 'url' => [
- 'type' => 'url',
- 'label' => 'URL',
- ]
- ]
- ],
-
- /**************************************************************
- AREA & GEOGRAPHY
- **************************************************************/
- 'areaServed' => [
- 'type' => 'repeater',
- 'label' => 'Area Served',
- 'description' => 'Geographic areas served',
- 'transformer' => 'text_array',
- 'fields' => [
- 'name' => [
- 'type' => 'text',
- 'label' => 'Location Name',
- ],
- 'url' => [
- 'type' => 'url',
- 'label' => 'Wikipedia Page',
- ]
- ]
- ],
-
- 'hasMap' => [
- 'type' => 'url',
- 'label' => 'Map URL',
- 'description' => 'Link to a map (e.g., Google Maps)',
- 'transformer' => 'url',
- ],
-
- /**************************************************************
- AMENITIES & FEATURES
- **************************************************************/
- 'amenityFeature' => [
- 'type' => 'checkbox',
- 'label' => 'Amenity Features',
- 'description' => 'Available facilities and features',
- 'transformer' => 'text',
- 'options' => [
- 'Wheelchair Accessible' => 'Wheelchair Accessible',
- 'Free Parking' => 'Free Parking',
- 'Private Rooms' => 'Private Rooms',
- 'Air Conditioning' => 'Air Conditioning',
- 'WiFi' => 'WiFi',
- 'Gender Neutral Restroom' => 'Gender Neutral Restroom',
- 'LGBTQ+ Friendly' => 'LGBTQ+ Friendly',
- 'Sterilization Room' => 'Sterilization Room',
- 'Refreshments Available' => 'Refreshments Available',
- 'Street Level Access' => 'Street Level Access',
- 'Single Use Needles' => 'Single Use Needles',
- 'Consultation Room' => 'Consultation Room',
- 'Aftercare Products Available' => 'Aftercare Products Available',
- 'Walk-Ins Welcome' => 'Walk-Ins Welcome',
- 'By Appointment' => 'By Appointment Only',
- ],
- ],
-
- /**************************************************************
- LANGUAGES
- **************************************************************/
- 'availableLanguage' => [
- 'type' => 'repeater',
- 'label' => 'Languages Available',
- 'description' => 'Languages spoken or supported',
- 'transformer' => 'language_array',
- 'fields' => [
- 'language' => [
- 'type' => 'text',
- 'label' => 'Language',
- ]
- ]
- ],
-
- 'knowsLanguage' => [
- 'type' => 'repeater',
- 'label' => 'Languages Known',
- 'description' => 'Languages the person knows',
- 'transformer' => 'language_array',
- 'fields' => [
- 'language' => [
- 'type' => 'text',
- 'label' => 'Language',
- ]
- ]
- ],
-
- 'inLanguage' => [
- 'type' => 'radio',
- 'label' => 'In Language',
- 'options' => [
- 'en-CA' => 'English, Canadian',
- 'en-US' => 'English, American',
- 'fr-CA' => 'French, Canadian'
- ],
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- RATINGS & REVIEWS
- **************************************************************/
- 'aggregateRating' => [
- 'type' => 'group',
- 'label' => 'Aggregate Rating',
- 'description' => 'Overall rating and review count',
- 'transformer' => 'aggregate_rating',
- 'fields' => [
- 'ratingValue' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Rating Value',
- 'description' => 'Average rating (e.g., 4.5)',
- ],
- 'bestRating' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Best Rating',
- 'default' => 5,
- 'description' => 'Highest possible rating (e.g., 5)',
- ],
- 'worstRating' => [
- 'default' => 1,
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Worst Rating',
- 'description' => 'Lowest possible rating (e.g., 1)',
- ],
- 'ratingCount' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Rating Count',
- 'description' => 'Total number of ratings',
- ],
- 'reviewCount' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Review Count',
- 'description' => 'Total number of reviews',
- ]
- ]
- ],
-
- /**************************************************************
- KEYWORDS & CATEGORIZATION
- **************************************************************/
- 'keywords' => [
- 'type' => 'repeater',
- 'label' => 'Keywords',
- 'description' => 'Keywords or tags',
- 'transformer' => 'text_array',
- 'fields' => [
- 'keyword' => [
- 'type' => 'text',
- 'label' => 'Keyword',
- ]
- ]
- ],
-
- /**************************************************************
- PERSON FIELDS
- **************************************************************/
- 'givenName' => [
- 'type' => 'text',
- 'label' => 'First Name',
- 'transformer' => 'text',
- ],
-
- 'familyName' => [
- 'type' => 'text',
- 'label' => 'Last Name',
- 'transformer' => 'text',
- ],
-
- 'honorificPrefix' => [
- 'type' => 'text',
- 'label' => 'Honorific Prefix',
- 'description' => 'e.g., Dr., Mr., Ms.',
- 'transformer' => 'text',
- ],
-
- 'honorificSuffix' => [
- 'type' => 'text',
- 'label' => 'Honorific Suffix',
- 'description' => 'e.g., PhD, MD',
- 'transformer' => 'text',
- ],
-
- 'jobTitle' => [
- 'type' => 'text',
- 'label' => 'Job Title',
- 'transformer' => 'text',
- ],
-
- 'birthDate' => [
- 'type' => 'date',
- 'label' => 'Birth Date',
- 'description' => 'For public figures',
- 'transformer' => 'date',
- ],
-
- 'gender' => [
- 'type' => 'text',
- 'label' => 'Gender',
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- CREATIVE WORK FIELDS
- **************************************************************/
- 'author' => [
- 'type' => 'text',
- 'label' => 'Author',
- 'description' => 'Author name or reference',
- 'transformer' => 'text',
- ],
-
- 'creator' => [
- 'type' => 'text',
- 'label' => 'Creator',
- 'description' => 'Creator name or reference',
- 'transformer' => 'text',
- ],
-
- 'dateCreated' => [
- 'type' => 'date',
- 'label' => 'Date Created',
- 'transformer' => 'date',
- ],
-
- 'datePublished' => [
- 'type' => 'date',
- 'label' => 'Date Published',
- 'transformer' => 'date',
- ],
-
- 'dateModified' => [
- 'type' => 'date',
- 'label' => 'Date Modified',
- 'transformer' => 'date',
- ],
-
- /**************************************************************
- VISUAL ARTWORK FIELDS
- **************************************************************/
- 'artform' => [
- 'type' => 'text',
- 'label' => 'Art Form',
- 'description' => 'e.g., Painting, Sculpture, Tattoo',
- 'transformer' => 'text',
- ],
-
- 'artMedium' => [
- 'type' => 'text',
- 'label' => 'Art Medium',
- 'description' => 'e.g., Oil, Watercolor, Ink',
- 'transformer' => 'text',
- ],
-
- 'artworkSurface' => [
- 'type' => 'text',
- 'label' => 'Artwork Surface',
- 'description' => 'e.g., Canvas, Paper, Skin',
- 'transformer' => 'text',
- ],
-
- 'width' => [
- 'type' => 'text',
- 'label' => 'Width',
- 'description' => 'Width with unit (e.g., 10cm, 5in)',
- 'transformer' => 'dimension',
- ],
-
- 'height' => [
- 'type' => 'text',
- 'label' => 'Height',
- 'description' => 'Height with unit (e.g., 15cm, 8in)',
- 'transformer' => 'dimension',
- ],
-
- /**************************************************************
- EVENT FIELDS
- **************************************************************/
- 'startDate' => [
- 'type' => 'datetime',
- 'label' => 'Start Date/Time',
- 'transformer' => 'datetime',
- ],
-
- 'endDate' => [
- 'type' => 'datetime',
- 'label' => 'End Date/Time',
- 'transformer' => 'datetime',
- ],
-
- 'eventStatus' => [
- 'type' => 'select',
- 'label' => 'Event Status',
- 'options' => [
- 'https://schema.org/EventScheduled' => 'Scheduled',
- 'https://schema.org/EventCancelled' => 'Cancelled',
- 'https://schema.org/EventPostponed' => 'Postponed',
- 'https://schema.org/EventRescheduled' => 'Rescheduled',
- ],
- 'transformer' => 'text',
- ],
-
- 'eventAttendanceMode' => [
- 'type' => 'select',
- 'label' => 'Attendance Mode',
- 'options' => [
- 'https://schema.org/OfflineEventAttendanceMode' => 'In-Person',
- 'https://schema.org/OnlineEventAttendanceMode' => 'Online',
- 'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid',
- ],
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- PRODUCT FIELDS
- **************************************************************/
- 'brand' => [
- 'type' => 'group',
- 'label' => 'Brand',
- 'transformer' => 'brand_object',
- 'fields' => [
- 'type' => [
- 'type' => 'select',
- 'label' => 'Brand Type',
- 'options' => [
- 'text' => 'Text Only',
- 'organization' => 'Organization/Brand',
- ]
- ],
- 'name' => [
- 'type' => 'text',
- 'label' => 'Brand Name',
- ],
- 'url' => [
- 'type' => 'url',
- 'label' => 'Brand Website',
- 'condition' => [
- 'field' => 'type',
- 'value' => 'organization'
- ]
- ],
- 'logo' => [
- 'type' => 'upload',
- 'label' => 'Brand Logo',
- 'condition' => [
- 'field' => 'type',
- 'value' => 'organization'
- ]
- ],
- ]
- ],
-
- 'sku' => [
- 'type' => 'text',
- 'label' => 'SKU',
- 'description' => 'Stock Keeping Unit',
- 'transformer' => 'text',
- ],
-
- 'gtin' => [
- 'type' => 'text',
- 'label' => 'GTIN',
- 'description' => 'Global Trade Item Number',
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- SERVICES & OFFERS
- **************************************************************/
- 'hasOfferCatalog' => [
- 'type' => 'group',
- 'label' => 'Offer Catalog',
- 'transformer' => 'offer_catalog_from_posts',
- 'fields' => [
- 'source' => [
- 'type' => 'select',
- 'label' => 'Source',
- 'options' => [
- 'auto' => 'Auto from post type',
- 'manual' => 'Manual entry',
- ]
- ],
- 'post_type' => [
- 'type' => 'select',
- 'label' => 'Post Type',
- 'options' => $this->getContentPostTypes(),
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto'
- ]
- ],
- 'group_by_taxonomy' => [
- 'type' => 'true_false',
- 'label' => 'Group by category/taxonomy',
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto'
- ]
- ],
- 'taxonomy' => [
- 'type' => 'select',
- 'label' => 'Taxonomy',
- 'options' => $this->getContentTaxonomies(),
- 'condition' => [
- 'field' => 'group_by_taxonomy',
- 'value' => '1' // or '1' depending on how checkbox stores
- ]
- ]
- ]
- ],
-
- 'knowsAbout' => [
- 'type' => 'repeater',
- 'label' => 'Areas of Expertise',
- 'description' => 'Skills and specialties',
- 'transformer' => 'text_array',
- 'fields' => [
- 'topic' => [
- 'type' => 'text',
- 'label' => 'Topic',
- ]
- ]
- ],
-
- /**************************************************************
- CREDENTIALS & CERTIFICATIONS
- **************************************************************/
- 'hasCredential' => [
- 'type' => 'repeater',
- 'label' => 'Credentials / Certifications',
- 'description' => 'Professional certifications',
- 'transformer' => 'credential_array',
- 'fields' => [
- 'credentialCategory' => [
- 'type' => 'text',
- 'label' => 'Category',
- ],
- 'name' => [
- 'type' => 'text',
- 'label' => 'Name',
- ],
- 'issuedBy' => [
- 'type' => 'text',
- 'label' => 'Issued By',
- ]
- ]
- ],
-
- 'award' => [
- 'type' => 'repeater',
- 'label' => 'Awards & Recognition',
- 'transformer' => 'text_array',
- 'fields' => [
- 'award' => ['type' => 'text', 'label' => 'Award'],
- ]
- ],
- 'serviceArea' => [
- 'type' => 'repeater',
- 'label' => 'Service Areas',
- 'description' => 'Geographic areas served (cities, neighborhoods, or radius)',
- 'transformer' => 'service_area_array',
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Area Name'],
- 'type' => [
- 'type' => 'select',
- 'label' => 'Type',
- 'options' => [
- 'City' => 'City',
- 'AdministrativeArea' => 'Region/Province',
- 'GeoCircle' => 'Radius',
- ]
- ],
- 'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'],
- ]
- ],
-
- // Specialties
- 'makesOffer' => [
- 'type' => 'group',
- 'label' => 'Featured Offerings',
- 'transformer' => 'offers_from_posts',
- 'fields' => [
- 'source' => [
- 'type' => 'select',
- 'label' => 'Source',
- 'options' => [
- 'auto' => 'Auto from post type',
- 'manual' => 'Manual entry',
- ]
- ],
- 'post_type' => [
- 'type' => 'select',
- 'label' => 'Post Type',
- 'options' => $this->getContentPostTypes(),
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto'
- ]
- ],
- 'limit' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Featured Count',
- 'default' => 5,
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto'
- ]
- ],
- 'manual_items' => [
- 'type' => 'repeater',
- 'label' => 'Manual Offers',
- 'condition' => [
- 'field' => 'source',
- 'value' => 'manual'
- ],
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Offer Name'],
- 'description' => ['type' => 'textarea', 'label' => 'Description'],
- 'price' => ['type' => 'text', 'label' => 'Price/Range'],
- ]
- ]
- ]
- ],
-
- 'hasMenu' => [
- 'type' => 'group',
- 'label' => 'Menu Items',
- 'description' => 'Auto-populate from post type or enter manually',
- 'transformer' => 'menu_from_posts',
- 'fields' => [
- 'source' => [
- 'type' => 'select',
- 'label' => 'Source',
- 'options' => [
- 'auto' => 'Auto from post type',
- 'manual' => 'Manual entry',
- ],
- ],
- 'post_type' => [
- 'type' => 'select',
- 'label' => 'Post Type',
- 'options' => $this->getContentPostTypes(), // Dynamic callback
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto',
- 'operator' => '=='
- ]
- ],
- 'limit' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Number of items',
- 'default' => 10,
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto'
- ]
- ],
- 'orderby' => [
- 'type' => 'select',
- 'label' => 'Order By',
- 'options' => [
- 'menu_order' => 'Menu Order',
- 'title' => 'Title',
- 'date' => 'Date',
- ],
- 'condition' => [
- 'field' => 'source',
- 'value' => 'auto'
- ]
- ],
- 'manual_items' => [
- 'type' => 'repeater',
- 'label' => 'Manual Items',
- 'condition' => [
- 'field' => 'source',
- 'value' => 'manual'
- ],
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Item Name'],
- 'description' => ['type' => 'textarea', 'label' => 'Description'],
- 'price' => ['type' => 'text', 'label' => 'Price'],
- ]
- ]
- ]
- ],
-
- /**************************************************************
- FAQ FIELDS
- **************************************************************/
- 'mainEntity' => [
- 'type' => 'repeater',
- 'label' => 'FAQ Items',
- 'description' => 'Question and Answer pairs',
- 'transformer' => 'faq_array',
- 'fields' => [
- 'question' => [
- 'type' => 'text',
- 'label' => 'Question',
- ],
- 'answer' => [
- 'type' => 'text',
- 'label' => 'Answer',
- ]
- ]
- ],
- /**************************************************************
- FOOD & CUISINE
- **************************************************************/
- 'servesCuisine' => [
- 'type' => 'repeater',
- 'label' => 'Cuisine Types',
- 'description' => 'Types of cuisine served',
- 'transformer' => 'text_array',
- 'fields' => [
- 'cuisine' => [
- 'type' => 'text',
- 'label' => 'Cuisine Type',
- 'description' => 'e.g., Italian, Mexican, Vegan'
- ]
- ]
- ],
-
- 'menu' => [
- 'type' => 'url',
- 'label' => 'Menu URL',
- 'description' => 'Link to online menu',
- 'transformer' => 'url',
- ],
-
- /**************************************************************
- PRODUCT/OFFER FIELDS
- **************************************************************/
- 'offers' => [
- 'type' => 'group',
- 'label' => 'Offer Details',
- 'description' => 'Price and availability information',
- 'transformer' => 'offer_object',
- 'fields' => [
- 'price' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Price',
- ],
- 'priceCurrency' => [
- 'type' => 'text',
- 'label' => 'Currency',
- 'default' => 'USD',
- ],
- 'availability' => [
- 'type' => 'select',
- 'label' => 'Availability',
- 'options' => [
- 'InStock' => 'In Stock',
- 'PreOrder' => 'Pre-Order',
- 'SoldOut' => 'Sold Out',
- 'OutOfStock' => 'Out of Stock',
- 'Discontinued' => 'Discontinued',
- ]
- ],
- 'validFrom' => [
- 'type' => 'date',
- 'label' => 'Valid From',
- ],
- 'validThrough' => [
- 'type' => 'date',
- 'label' => 'Valid Through',
- ],
- ]
- ],
-
- 'mpn' => [
- 'type' => 'text',
- 'label' => 'Manufacturer Part Number',
- 'transformer' => 'text',
- ],
-
- /**************************************************************
- BUSINESS POLICIES & FEATURES
- **************************************************************/
- 'isAccessibleForFree' => [
- 'type' => 'true_false',
- 'label' => 'Accessible For Free',
- 'description' => 'Is this service/location accessible without payment?',
- 'transformer' => 'boolean',
- ],
-
- 'smokingAllowed' => [
- 'type' => 'true_false',
- 'label' => 'Smoking Allowed',
- 'transformer' => 'boolean',
- ],
-
- 'petsAllowed' => [
- 'type' => 'select',
- 'label' => 'Pets Allowed',
- 'options' => [
- '' => 'Not specified',
- 'yes' => 'Yes',
- 'no' => 'No',
- ],
- 'transformer' => 'boolean',
- ],
-
- /**************************************************************
- ORGANIZATION RELATIONSHIPS
- **************************************************************/
- 'parentOrganization' => [
- 'type' => 'group',
- 'label' => 'Parent Organization',
- 'description' => 'Organization this is a part of',
- 'transformer' => 'organization_reference',
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Organization Name'],
- 'url' => ['type' => 'url', 'label' => 'Website'],
- ]
- ],
-
- 'subOrganization' => [
- 'type' => 'repeater',
- 'label' => 'Sub-Organizations',
- 'description' => 'Child organizations or departments',
- 'transformer' => 'organization_reference_array',
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Organization Name'],
- 'url' => ['type' => 'url', 'label' => 'Website'],
- ]
- ],
-
- 'employee' => [
- 'type' => 'repeater',
- 'label' => 'Employees',
- 'transformer' => 'person_reference_array',
- 'fields' => [
- 'name' => ['type' => 'text', 'label' => 'Name'],
- 'jobTitle' => ['type' => 'text', 'label' => 'Job Title'],
- ]
- ],
-
- /**************************************************************
- HOSPITALITY (for hotels, etc.)
- **************************************************************/
- 'checkinTime' => [
- 'type' => 'time',
- 'label' => 'Check-in Time',
- 'transformer' => 'time',
- ],
-
- 'checkoutTime' => [
- 'type' => 'time',
- 'label' => 'Check-out Time',
- 'transformer' => 'time',
- ],
-
- 'starRating' => [
- 'type' => 'group',
- 'label' => 'Star Rating',
- 'transformer' => 'rating_object',
- 'fields' => [
- 'ratingValue' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Rating',
- 'min' => 1,
- 'max' => 5,
- ],
- ]
- ],
-
- /**************************************************************
- REVIEW & RATING
- **************************************************************/
- 'review' => [
- 'type' => 'repeater',
- 'label' => 'Reviews',
- 'transformer' => 'review_array',
- 'fields' => [
- 'author' => ['type' => 'text', 'label' => 'Reviewer Name'],
- 'reviewRating' => [
- 'type' => 'text',
- 'subtype' => 'number',
- 'label' => 'Rating',
- 'min' => 1,
- 'max' => 5,
- ],
- 'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'],
- 'datePublished' => ['type' => 'date', 'label' => 'Date'],
- ]
- ],
-
- /**************************************************************
- HEALTH & MEDICAL
- **************************************************************/
- 'medicalSpecialty' => [
- 'type' => 'repeater',
- 'label' => 'Medical Specialties',
- 'transformer' => 'text_array',
- 'fields' => [
- 'specialty' => ['type' => 'text', 'label' => 'Specialty']
- ]
- ],
-
- 'healthcareService' => [
- 'type' => 'repeater',
- 'label' => 'Healthcare Services',
- 'transformer' => 'text_array',
- 'fields' => [
- 'service' => ['type' => 'text', 'label' => 'Service']
- ]
- ],
- ];
- }
-
- /**
- * Register all type definitions
- * Each type lists the fields it uses
- */
- private function registerTypeDefinitions(): void
- {
- $this->typeDefinitions = [
- /**************************************************************
- GENERAL / SITE-WIDE
- **************************************************************/
- 'WebSite' => [
- 'label' => 'Website',
- 'group' => 'general',
- 'fields' => [
- 'name',
- 'description',
- 'url',
- 'inLanguage',
- 'potentialAction',
- 'hasPart',
- 'creator',
- ],
- ],
-
- /**************************************************************
- PAGE TYPES
- **************************************************************/
- 'WebPage' => [
- 'label' => 'Web Page',
- 'group' => 'page',
- 'fields' => [
- 'name',
- 'description',
- 'url',
- 'image',
- 'datePublished',
- 'dateModified',
- 'author',
- ],
- ],
-
- 'CollectionPage' => [
- 'label' => 'Collection Page',
- 'group' => 'page',
- 'extends' => 'WebPage',
- ],
-
- 'FAQPage' => [
- 'label' => 'FAQ Page',
- 'group' => 'page',
- 'extends' => 'WebPage',
- 'fields' => [
- 'mainEntity', // FAQ items
- ],
- ],
-
- /**************************************************************
- ORGANIZATION & BUSINESS
- **************************************************************/
- 'Organization' => [
- 'label' => 'Organization',
- 'group' => 'business',
- 'fields' => [
- 'name',
- 'legalName',
- 'alternateName',
- 'description',
- 'url',
- 'logo',
- 'image',
- 'email',
- 'telephone',
- 'sameAs',
- 'founders',
- 'foundingDate',
- 'numberOfEmployees',
- 'taxID',
- 'vatID',
- 'duns',
- 'slogan',
- 'disambiguatingDescription',
- ],
- ],
-
-
-
- 'LocalBusiness' => [
- 'label' => 'Local Business',
- 'group' => 'business',
- 'extends' => 'Organization',
- 'fields' => [
- 'location',
- 'openingHours',
- 'priceRange',
- 'currenciesAccepted',
- 'paymentAccepted',
- 'serviceArea',
- 'areaServed',
- 'hasMap',
- 'amenityFeature',
- 'availableLanguage',
- 'hasOfferCatalog',
- 'makesOffer',
- 'hasMenu',
- 'knowsAbout',
- 'hasCredential',
- 'aggregateRating',
- 'award',
- ],
- ],
-
- 'TattooParlor' => [
- 'label' => 'Tattoo Parlor',
- 'group' => 'business',
- 'extends' => 'LocalBusiness',
- 'fields' => [
- 'makesOffer', // Tattoo styles/services
- 'hasOfferCatalog', // Portfolio as catalog
- 'award',
- ],
- ],
-
- 'HealthBusiness' => [
- 'label' => 'Health Business',
- 'group' => 'business',
- 'extends' => 'LocalBusiness',
- 'description' => 'Healthcare providers',
- ],
-
- 'FoodEstablishment' => [
- 'label' => 'Food Establishment',
- 'group' => 'business',
- 'extends' => 'LocalBusiness',
- 'fields' => [
- 'hasMenu',
- 'servesCuisine',
- ],
- ],
- 'FoodTruck' => [
- 'label' => 'Food Truck',
- 'group' => 'business',
- 'extends' => 'FoodEstablishment',
- 'fields' => [
- 'serviceArea',
- ],
- ],
-
- 'Store' => [
- 'label' => 'Store / Shop',
- 'group' => 'business',
- 'extends' => 'LocalBusiness',
- 'fields' => [
- 'hasOfferCatalog',
- 'makesOffer',
- ],
- ],
-
- 'ProfessionalService' => [
- 'label' => 'Professional Service',
- 'group' => 'business',
- 'extends' => 'LocalBusiness',
- 'fields' => [
- 'serviceArea', // Where they operate
- 'makesOffer', // Services offered
- 'award', // Professional recognition
- ],
- ],
-
- /**************************************************************
- PERSON
- **************************************************************/
- 'Person' => [
- 'label' => 'Person',
- 'group' => 'person',
- 'fields' => [
- 'name',
- 'givenName',
- 'familyName',
- 'honorificPrefix',
- 'honorificSuffix',
- 'alternateName',
- 'description',
- 'image',
- 'url',
- 'email',
- 'telephone',
- 'sameAs',
- 'jobTitle',
- 'knowsLanguage',
- 'birthDate',
- 'gender',
- ],
- ],
-
- /**************************************************************
- CREATIVE WORKS
- **************************************************************/
- 'CreativeWork' => [
- 'label' => 'Creative Work',
- 'group' => 'creative',
- 'fields' => [
- 'name',
- 'description',
- 'image',
- 'author',
- 'creator',
- 'dateCreated',
- 'datePublished',
- 'dateModified',
- 'keywords',
- ],
- ],
-
- 'DefinedTermSet' => [
- 'label' => 'Defined Term',
- 'group' => 'creative',
- 'extends' => 'CreativeWork',
- 'fields' => [
- 'DefinedTerm',
- ]
- ],
-
- 'BeforeAfter' => [
- 'label' => 'Before & After Case',
- 'group' => 'creative',
- 'extends' => 'CreativeWork',
- 'fields' => [
- 'about', // Service (Laser Tattoo Removal)
- 'temporalCoverage', // Treatment period
- 'hasPart', // Individual images (as references)
- 'associatedMedia', // Alternative to hasPart
- 'additionalProperty', // Sessions, treatment area
- ],
- ],
-
- 'VisualArtwork' => [
- 'label' => 'Visual Artwork',
- 'group' => 'creative',
- 'extends' => 'CreativeWork',
- 'fields' => [
- 'artform',
- 'artMedium',
- 'artworkSurface',
- 'width',
- 'height',
- ],
- ],
-
- 'Tattoo' => [
- 'label' => 'Tattoo',
- 'group' => 'creative',
- 'extends' => 'VisualArtwork',
- 'description' => 'A tattoo artwork (custom extension)',
- ],
-
- 'Product' => [
- 'label' => 'Product',
- 'group' => 'creative',
- 'fields' => [
- 'name',
- 'description',
- 'image',
- 'brand',
- 'sku',
- 'gtin',
- 'offers', // Price, availability
- 'aggregateRating', // Reviews
- 'award', // Product awards
- ],
- ],
-
- /**************************************************************
- EVENTS
- **************************************************************/
- 'Event' => [
- 'label' => 'Event',
- 'group' => 'event',
- 'fields' => [
- 'name',
- 'description',
- 'image',
- 'startDate',
- 'endDate',
- 'location',
- 'eventStatus',
- 'eventAttendanceMode',
- ],
- ],
- ];
- }
-
- /**
- * Register type groups for UI organization
- */
- private function registerTypeGroups(): void
- {
- $this->typeGroups = [
- 'general' => 'General',
- 'page' => 'Page Types',
- 'business' => 'Business & Organization',
- 'person' => 'People',
- 'creative' => 'Creative Works',
- 'event' => 'Events',
- ];
- }
-
- /**
- * Register a custom field definition
- */
- public function registerField(string $fieldName, array $config): void
- {
- $this->fieldDefinitions[$fieldName] = $config;
- }
-
- /**
- * Register a custom type definition
- */
- public function registerType(string $typeName, array $config): void
- {
- $this->typeDefinitions[$typeName] = $config;
- }
-
- /**
- * Register a type group
- */
- public function registerGroup(string $key, string $label): void
- {
- $this->typeGroups[$key] = $label;
- }
-
- /**
- * Get post types for select options
- */
- public static function getContentPostTypes(): array
- {
- $options = ['' => '-- Select Post Type --'];
-
- if (defined('JVB_CONTENT')) {
- foreach (JVB_CONTENT as $key => $config) {
- $options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
- }
- }
-
- return $options;
- }
-
- /**
- * Get taxonomies for select options
- */
- public static function getContentTaxonomies(): array
- {
- $options = ['' => '-- Select Taxonomy --'];
-
- if (defined('JVB_TAXONOMY')) {
- foreach (JVB_TAXONOMY as $key => $config) {
- $options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key);
- }
- }
-
- return $options;
- }
-}
diff --git a/inc/managers/SEO/_setup.php b/inc/managers/SEO/_setup.php
index 7cb84e1..517bd54 100644
--- a/inc/managers/SEO/_setup.php
+++ b/inc/managers/SEO/_setup.php
@@ -1,6 +1,4 @@
<?php
-
-//require(JVB_DIR . '/inc/managers/SEO/SchemaRegistry.php');
require(JVB_DIR . '/inc/managers/SEO/FieldBuilder.php');
require(JVB_DIR . '/inc/managers/SEO/FieldOverrideBuilder.php');
require(JVB_DIR . '/inc/managers/SEO/TypeBuilder.php');
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index f6de609..e2b4a45 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -109,6 +109,7 @@
}
$output .= static::buildHint($config);
+ $output .= static::buildCharacterLimit($config);
$output .= static::buildDescription($name, $config);
$output .= '</div>';
@@ -179,6 +180,17 @@
);
}
+ protected static function buildCharacterLimit(array $config): string
+ {
+ if (empty($config['data']['limit'])) {
+ return '';
+ }
+ return sprintf(
+ '<span class="char-limit"><span class="current">0</span> / <span class="limit">%s</span></span>',
+ esc_html($config['data']['limit'])
+ );
+ }
+
protected static function buildLabel(string $name, array $config):string
{
if (!empty($config['label'])) {
--
Gitblit v1.10.0