fix wpcfto select and repeater related visibility and validation
This commit is contained in:
@@ -34,15 +34,22 @@ Vue.component('wpcfto_repeater', {
|
||||
<div class="wpcfto-field-content">
|
||||
<!-- Required proxy: forces validation when repeater is empty -->
|
||||
<input
|
||||
v-if="fields && fields.required === true"
|
||||
class="wpcfto-required-proxy"
|
||||
type="text"
|
||||
:name="field_name + '__required__'"
|
||||
:name="field_id + '__required__'"
|
||||
:value="(repeater && repeater.length ? 'ok' : '')"
|
||||
:required="fields && fields.required === true && (!repeater || repeater.length === 0)"
|
||||
:disabled="!(fields && fields.required === true && (!repeater || repeater.length === 0))"
|
||||
:required="!repeater || repeater.length === 0"
|
||||
:disabled="repeater && repeater.length > 0"
|
||||
tabindex="-1" readonly
|
||||
style="position:absolute; left:-9999px; top:auto; width:1px; height:1px; opacity:0; pointer-events:none;"
|
||||
/>
|
||||
<!-- Hidden data carrier to persist repeater rows via standard form submit -->
|
||||
<input
|
||||
type="hidden"
|
||||
:name="field_name"
|
||||
:value="jsonValue"
|
||||
/>
|
||||
<div v-for="(area, area_key) in repeater" class="wpcfto-repeater-single" :class="'wpcfto-repeater_' + field_name + '_' + area_key">
|
||||
<div class="wpcfto_group_title" @click="toggleArea(area)" v-html="getGroupTitle(area, area_key)"></div>
|
||||
<div class="repeater_inner" :class="{ closed: area.closed_tab }">
|
||||
@@ -94,44 +101,74 @@ Vue.component('wpcfto_repeater', {
|
||||
item.closed_tab = true;
|
||||
});
|
||||
}
|
||||
// Normalize rows to include only declared fields (copy defaults if missing)
|
||||
if (this.fields && this.fields.fields) {
|
||||
this.repeater = this.repeater.map((row) => {
|
||||
const normalized = { closed_tab: true };
|
||||
Object.keys(this.fields.fields).forEach((fname) => {
|
||||
if (typeof row[fname] !== 'undefined') {
|
||||
normalized[fname] = row[fname];
|
||||
} else {
|
||||
const def = this.fields.fields[fname] && this.fields.fields[fname].value;
|
||||
if (typeof def !== 'undefined') normalized[fname] = def;
|
||||
}
|
||||
});
|
||||
return normalized;
|
||||
});
|
||||
}
|
||||
// Initial validation state
|
||||
if (typeof this.validateField === 'function') this.validateField();
|
||||
// Block Save when repeater is required but empty (length check only)
|
||||
const handler = (e) => {
|
||||
// Only enforce when this component is in DOM
|
||||
if (!this.$el || !document.body.contains(this.$el)) return;
|
||||
const emptyRequired = !!(this.fields && this.fields.required === true && (!this.repeater || this.repeater.length === 0));
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
try {
|
||||
const btns = document.querySelectorAll('.wpcfto_save_settings, .wpcfto_save_metabox');
|
||||
btns.forEach(btn => btn.addEventListener('click', handler, true));
|
||||
this.__wpcftoSaveHandler = handler;
|
||||
} catch(e) {}
|
||||
// Also prevent <form> submission if invalid (works for both settings + metabox)
|
||||
try {
|
||||
const form = this.$el.closest('form');
|
||||
if (form) {
|
||||
const onSubmit = (e) => {
|
||||
const emptyRequired = !!(this.fields && this.fields.required === true && (!this.repeater || this.repeater.length === 0));
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
form.addEventListener('submit', onSubmit, true);
|
||||
this.__wpcftoFormSubmitHandler = onSubmit;
|
||||
}
|
||||
} catch(e) {}
|
||||
if (this.fields && this.fields.required === true) {
|
||||
// Block Save when repeater is required but empty (length check only)
|
||||
const handler = (e) => {
|
||||
// Only enforce when this component is in DOM
|
||||
if (!this.$el || !document.body.contains(this.$el)) return;
|
||||
const emptyRequired = !this.repeater || this.repeater.length === 0;
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
try {
|
||||
const btns = document.querySelectorAll('.wpcfto_save_settings, .wpcfto_save_metabox');
|
||||
btns.forEach(btn => btn.addEventListener('click', handler, true));
|
||||
this.__wpcftoSaveHandler = handler;
|
||||
} catch(e) {}
|
||||
// Also prevent <form> submission if invalid (works for both settings + metabox)
|
||||
try {
|
||||
const form = this.$el.closest('form');
|
||||
if (form) {
|
||||
const onSubmit = (e) => {
|
||||
const emptyRequired = !this.repeater || this.repeater.length === 0;
|
||||
if (emptyRequired) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.focusSelf();
|
||||
}
|
||||
};
|
||||
form.addEventListener('submit', onSubmit, true);
|
||||
this.__wpcftoFormSubmitHandler = onSubmit;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (typeof this.validateField === 'function') this.validateField();
|
||||
},
|
||||
beforeDestroy: function () {
|
||||
if (this.__teardown) this.__teardown();
|
||||
if (this.__wpcftoSaveHandler) {
|
||||
try {
|
||||
const btns = document.querySelectorAll('.wpcfto_save_settings, .wpcfto_save_metabox');
|
||||
btns.forEach(btn => btn.removeEventListener('click', this.__wpcftoSaveHandler, true));
|
||||
} catch(e) {}
|
||||
this.__wpcftoSaveHandler = null;
|
||||
}
|
||||
if (this.__wpcftoFormSubmitHandler) {
|
||||
try {
|
||||
const form = this.$el && this.$el.closest ? this.$el.closest('form') : null;
|
||||
if (form) form.removeEventListener('submit', this.__wpcftoFormSubmitHandler, true);
|
||||
} catch(e) {}
|
||||
this.__wpcftoFormSubmitHandler = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
focusSelf() {
|
||||
@@ -154,28 +191,41 @@ Vue.component('wpcfto_repeater', {
|
||||
}).filter(Boolean).join(' → ');
|
||||
},
|
||||
validateField() {
|
||||
let isValid = true;
|
||||
if ('required' in this.fields && this.fields.required === true) {
|
||||
isValid = !!(this.fields.value.length > 0);
|
||||
// If this repeater is not required, always treat as valid and clean any invalid markers
|
||||
if (!(this.fields && this.fields.required === true)) {
|
||||
try { this.$el.classList.remove('wpcfto-invalid'); } catch(e){}
|
||||
try { this.$el.removeAttribute('aria-invalid'); } catch(e){}
|
||||
try {
|
||||
const fid = this.field_id || (this.fields && this.fields.field_id);
|
||||
if (fid) this.$root.$emit('field-validation', { fieldId: fid, isValid: true });
|
||||
} catch(e) {}
|
||||
return true;
|
||||
}
|
||||
// Repeater-level required: valid iff it has at least one row
|
||||
let isValid = true;
|
||||
if (this.fields && this.fields.required === true) {
|
||||
const len = (this.repeater && Array.isArray(this.repeater)) ? this.repeater.length : 0;
|
||||
isValid = len > 0;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
try { this.$el.classList.add('wpcfto-invalid'); } catch(e){}
|
||||
try { this.$el.setAttribute('aria-invalid','true'); } catch(e){}
|
||||
// Tell the global collector (validationMixin) about our state
|
||||
// Notify global validator (validationMixin listener)
|
||||
try {
|
||||
const fid = this.field_id || (this.fields && this.fields.field_id);
|
||||
if (fid) this.$root.$emit('field-validation', { fieldId: fid, isValid: false });
|
||||
} catch(e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// valid state
|
||||
try { this.$el.classList.remove('wpcfto-invalid'); } catch(e){}
|
||||
try { this.$el.removeAttribute('aria-invalid'); } catch(e){}
|
||||
try {
|
||||
const fid = this.field_id || (this.fields && this.fields.field_id);
|
||||
if (fid) this.$root.$emit('field-validation', { fieldId: fid, isValid: true });
|
||||
} catch(e) {}
|
||||
|
||||
return true;
|
||||
},
|
||||
// This method validates all visible required fields in all rows before adding a new row
|
||||
@@ -382,10 +432,40 @@ Vue.component('wpcfto_repeater', {
|
||||
repeater: {
|
||||
deep: true,
|
||||
handler: function handler(repeater) {
|
||||
this.$emit('wpcfto-get-value', repeater);
|
||||
// Build a clean payload with only declared inner fields
|
||||
const payload = (Array.isArray(repeater) ? repeater : []).map((row) => {
|
||||
const out = {};
|
||||
if (this.fields && this.fields.fields) {
|
||||
Object.keys(this.fields.fields).forEach((fname) => {
|
||||
if (typeof row[fname] !== 'undefined') out[fname] = row[fname];
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
this.$emit('wpcfto-get-value', payload);
|
||||
this.validateField();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jsonValue() {
|
||||
const list = Array.isArray(this.repeater) ? this.repeater : [];
|
||||
const fieldsDef = (this.fields && this.fields.fields) ? this.fields.fields : null;
|
||||
const payload = list.map((row) => {
|
||||
const out = {};
|
||||
if (fieldsDef) {
|
||||
Object.keys(fieldsDef).forEach((fname) => {
|
||||
if (typeof row[fname] !== 'undefined') out[fname] = row[fname];
|
||||
else {
|
||||
const def = fieldsDef[fname] && fieldsDef[fname].value;
|
||||
if (typeof def !== 'undefined') out[fname] = def;
|
||||
}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
try { return JSON.stringify(payload); } catch(e) { return '[]'; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user