update multicurrencies functionality on global level
This commit is contained in:
@@ -24,8 +24,19 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<!-- Editor (textarea transformed by CodeMirror) -->
|
||||
<textarea :id="field_id" :name="field_name"></textarea>
|
||||
|
||||
<!-- Hidden mirror for required + form submit -->
|
||||
<input
|
||||
type="text"
|
||||
:name="field_name"
|
||||
:value="value"
|
||||
:required="fields && fields.required === true"
|
||||
tabindex="-1"
|
||||
readonly
|
||||
style="position:absolute;opacity:0;height:0;width:0;pointer-events:none;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<wpcfto_fields_aside_after :fields="fields"></wpcfto_fields_aside_after>
|
||||
@@ -45,27 +56,62 @@
|
||||
'css': 'CSS',
|
||||
'htmlmixed': 'HTML',
|
||||
'text/plain': 'Plain Text'
|
||||
}
|
||||
},
|
||||
_origConsoleError: null,
|
||||
_origConsoleWarn: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
// Silence extremely noisy passive-listener warnings (harmless browser noise)
|
||||
if (!this._origConsoleError && typeof console !== 'undefined') {
|
||||
this._origConsoleError = console.error;
|
||||
this._origConsoleWarn = console.warn;
|
||||
const drop = (fn) => function(){
|
||||
const msg = arguments && arguments[0];
|
||||
if (typeof msg === 'string' && msg.indexOf('Unable to preventDefault inside passive event listener invocation.') !== -1) return;
|
||||
return fn.apply(console, arguments);
|
||||
};
|
||||
try { console.error = drop(this._origConsoleError); } catch(e){}
|
||||
try { console.warn = drop(this._origConsoleWarn); } catch(e){}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
const selector = '#' + this.field_id;
|
||||
this.waitForElementVisible(selector, () => {
|
||||
const fieldObj = window.eval(this.fields);
|
||||
let rawValue = fieldObj.value || '';
|
||||
this.value = this.formatCode(rawValue, fieldObj.mode);
|
||||
|
||||
this.initCodeMirror(fieldObj);
|
||||
const fieldObj = (this.fields && typeof this.fields === 'object') ? this.fields : {};
|
||||
let rawValue = '';
|
||||
if (typeof this.field_value === 'string' && !this.isPathString(this.field_value)) {
|
||||
rawValue = this.field_value;
|
||||
} else if (fieldObj && typeof fieldObj.value === 'string') {
|
||||
rawValue = fieldObj.value;
|
||||
} else {
|
||||
rawValue = '';
|
||||
}
|
||||
this.value = this.formatCode(rawValue, fieldObj.mode || this.currentMode);
|
||||
|
||||
this.initCodeMirror({
|
||||
mode: fieldObj.mode || this.currentMode,
|
||||
theme: fieldObj.theme || 'default'
|
||||
});
|
||||
|
||||
this.$emit('wpcfto-get-value', this.value);
|
||||
|
||||
// Nudge global validators after init
|
||||
this.$nextTick(() => {
|
||||
try { if (typeof this.validateField === 'function') this.validateField(); } catch(e){}
|
||||
});
|
||||
|
||||
this.refreshOnTabActivation();
|
||||
this.addMutationObserver();
|
||||
this.initResizeObserver(); // 👈 Add this
|
||||
this.initResizeObserver();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
isPathString(str) {
|
||||
return typeof str === 'string' && /data\s*\[\s*['\"]/i.test(str);
|
||||
},
|
||||
initResizeObserver() {
|
||||
const container = document.getElementById(this.field_id)?.closest('.wpcfto-field-content');
|
||||
if (!container || typeof ResizeObserver === 'undefined') return;
|
||||
@@ -224,18 +270,63 @@
|
||||
|
||||
if (this.editor) return;
|
||||
|
||||
this.editor = CodeMirror.fromTextArea(textarea, {
|
||||
mode: fieldObj?.mode || 'htmlmixed',
|
||||
theme: fieldObj?.theme || 'default',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true
|
||||
});
|
||||
// During CM construction, force non‑passive listeners on the editor DOM only
|
||||
const typesToFix = { touchstart:1, touchmove:1, wheel:1, mousewheel:1 };
|
||||
const origAdd = EventTarget.prototype.addEventListener;
|
||||
EventTarget.prototype.addEventListener = function(type, listener, options){
|
||||
try {
|
||||
if ((type in typesToFix) && this && (
|
||||
(this.classList && this.classList.contains('CodeMirror')) ||
|
||||
(this.closest && typeof this.closest === 'function' && this.closest('.CodeMirror'))
|
||||
)){
|
||||
if (options === undefined) options = { passive: false };
|
||||
else if (typeof options === 'boolean') options = { capture: !!options, passive: false };
|
||||
else if (typeof options === 'object') options = Object.assign({}, options, { passive: false });
|
||||
}
|
||||
} catch(e){}
|
||||
return origAdd.call(this, type, listener, options);
|
||||
};
|
||||
try {
|
||||
this.editor = CodeMirror.fromTextArea(textarea, {
|
||||
mode: fieldObj?.mode || 'htmlmixed',
|
||||
theme: fieldObj?.theme || 'default',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
readOnly: false,
|
||||
dragDrop: true,
|
||||
autofocus: false,
|
||||
viewportMargin: Infinity,
|
||||
inputStyle: 'contenteditable',
|
||||
scrollbarStyle: 'native',
|
||||
});
|
||||
} finally {
|
||||
// Always restore global addEventListener immediately after CM attaches its handlers
|
||||
EventTarget.prototype.addEventListener = origAdd;
|
||||
}
|
||||
|
||||
this.editor.setValue(this.value);
|
||||
|
||||
// Strong focus sequence to prevent immediate blur from surrounding UI scripts
|
||||
try { this.editor.focus(); } catch(e){}
|
||||
setTimeout(() => { try { this.editor.focus(); } catch(e){} }, 10);
|
||||
setTimeout(() => { try { this.editor.refresh(); this.editor.focus(); } catch(e){} }, 50);
|
||||
|
||||
// Ensure clicks focus the editor and mitigate passive-listener side effects
|
||||
const wrapper = this.editor.getWrapperElement();
|
||||
if (wrapper) {
|
||||
// Reduce need for preventDefault by disabling native touch scroll here
|
||||
wrapper.style.touchAction = 'none';
|
||||
wrapper.style.webkitUserSelect = 'text';
|
||||
wrapper.style.userSelect = 'text';
|
||||
const focusEditor = () => { try { this.editor.focus(); } catch(e){} };
|
||||
wrapper.addEventListener('mousedown', focusEditor, {capture: true, passive: false});
|
||||
wrapper.addEventListener('click', focusEditor, {capture: true, passive: false});
|
||||
wrapper.addEventListener('pointerdown', focusEditor, {capture: true, passive: false});
|
||||
}
|
||||
|
||||
// Force multiple refreshes to ensure layout renders properly
|
||||
setTimeout(() => this.editor.refresh(), 50);
|
||||
setTimeout(() => this.editor.refresh(), 300);
|
||||
@@ -246,34 +337,28 @@
|
||||
const newValue = cm.getValue();
|
||||
this.value = newValue;
|
||||
|
||||
try {
|
||||
const mode = fieldObj?.mode || 'htmlmixed';
|
||||
const normalizedMode = (mode || '').toLowerCase();
|
||||
|
||||
let minified = newValue;
|
||||
|
||||
if (normalizedMode.includes('javascript') || normalizedMode.includes('json')) {
|
||||
const parsed = JSON.parse(newValue);
|
||||
minified = JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
fieldObj.value = minified;
|
||||
textarea.value = minified;
|
||||
|
||||
const event = new Event('input', { bubbles: true });
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
} catch (e) {
|
||||
fieldObj.value = newValue;
|
||||
textarea.value = newValue;
|
||||
|
||||
const event = new Event('input', { bubbles: true });
|
||||
textarea.dispatchEvent(event);
|
||||
const mode = (fieldObj && fieldObj.mode) ? String(fieldObj.mode).toLowerCase() : 'htmlmixed';
|
||||
let output = newValue;
|
||||
if (mode.indexOf('json') !== -1) {
|
||||
try { output = JSON.stringify(JSON.parse(newValue)); } catch(_) { output = newValue; }
|
||||
}
|
||||
|
||||
textarea.value = output;
|
||||
const event = new Event('input', { bubbles: true });
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
this.$emit('wpcfto-get-value', output);
|
||||
|
||||
// Re-run global required validation if present
|
||||
try { if (typeof this.validateField === 'function') this.validateField(); } catch(e){}
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this._origConsoleError) { try { console.error = this._origConsoleError; } catch(e){} }
|
||||
if (this._origConsoleWarn) { try { console.warn = this._origConsoleWarn; } catch(e){} }
|
||||
this._origConsoleError = null;
|
||||
this._origConsoleWarn = null;
|
||||
if (this.editor) {
|
||||
this.editor.off('change');
|
||||
this.editor.toTextArea();
|
||||
|
||||
@@ -11,18 +11,34 @@ Vue.component('wpcfto_multi_checkbox', {
|
||||
},
|
||||
template: "\n <div class=\"wpcfto_generic_field wpcfto_generic_field_multi_checkbox\" v-bind:class=\"field_id\">\n\n <wpcfto_fields_aside_before :fields=\"fields\" :field_label=\"field_label\"></wpcfto_fields_aside_before>\n\n <div class=\"wpcfto-field-content\">\n <div class=\"wpcfto_multi_checkbox wpcfto-admin-checkbox\">\n <label v-for=\"(option, key) in fields['options']\">\n <div class=\"wpcfto-admin-checkbox-wrapper\" v-bind:class=\"{'active' : checkboxes.includes(key)}\">\n <div class=\"wpcfto-checkbox-switcher\"></div>\n <input type=\"checkbox\" v-model=\"checkboxes\" v-bind:value=\"key\" :key=\"key\"/>\n </div>\n <span v-html=\"option\"></span>\n </label>\n </div>\n </div>\n\n <wpcfto_fields_aside_after :fields=\"fields\"></wpcfto_fields_aside_after>\n \n </div>\n ",
|
||||
mounted: function mounted() {
|
||||
this.checkboxes = typeof this.field_value === 'string' && WpcftoIsJsonString(this.field_value) ? JSON.parse(this.field_value) : this.field_value;
|
||||
|
||||
if (this.checkboxes.length === 0) {
|
||||
// Normalize initial value from parent (can be JSON string, array, or empty)
|
||||
if (typeof this.field_value === 'string' && WpcftoIsJsonString(this.field_value)) {
|
||||
try { this.checkboxes = JSON.parse(this.field_value); } catch(e){ this.checkboxes = []; }
|
||||
} else if (Array.isArray(this.field_value)) {
|
||||
this.checkboxes = this.field_value.slice();
|
||||
} else if (this.field_value) {
|
||||
// If a single scalar slipped in, coerce to array
|
||||
this.checkboxes = [this.field_value];
|
||||
} else {
|
||||
this.checkboxes = [];
|
||||
}
|
||||
if (!Array.isArray(this.checkboxes)) this.checkboxes = [];
|
||||
|
||||
// Emit immediately so the hidden input :value reflects the current state
|
||||
this.$emit('wpcfto-get-value', this.checkboxes);
|
||||
if (typeof this.validateField === 'function') this.validateField();
|
||||
},
|
||||
methods: {},
|
||||
watch: {
|
||||
checkboxes: {
|
||||
deep: true,
|
||||
handler: function handler(checkboxes) {
|
||||
// Ensure array
|
||||
if (!Array.isArray(checkboxes)) {
|
||||
checkboxes = (checkboxes == null) ? [] : [checkboxes];
|
||||
}
|
||||
this.$emit('wpcfto-get-value', checkboxes);
|
||||
if (typeof this.validateField === 'function') this.validateField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,14 +1,22 @@
|
||||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
const Repeater = Swal.mixin({
|
||||
customClass: {
|
||||
const Repeater = (window.Swal && typeof window.Swal.mixin === 'function')
|
||||
? window.Swal.mixin({
|
||||
customClass: {
|
||||
container: 'wpcfto-settings',
|
||||
confirmButton: 'button'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
: {
|
||||
// Fallback that mimics Swal.fire returning a Promise
|
||||
fire: (opts) => Promise.resolve({
|
||||
isConfirmed: window.confirm((opts && (opts.text || opts.html || opts.title)) || 'Are you sure?')
|
||||
})
|
||||
};
|
||||
|
||||
Vue.component('wpcfto_repeater', {
|
||||
mixins: (window.validationMixin ? [window.validationMixin] : []),
|
||||
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
||||
data: function data() {
|
||||
return {
|
||||
@@ -18,8 +26,23 @@ Vue.component('wpcfto_repeater', {
|
||||
},
|
||||
template: `
|
||||
<div class="wpcfto_generic_field wpcfto_generic_field_repeater wpcfto-repeater unflex_fields">
|
||||
<wpcfto_fields_aside_before :fields="fields" :field_label="field_label"></wpcfto_fields_aside_before>
|
||||
<wpcfto_fields_aside_before
|
||||
:fields="fields"
|
||||
:field_label="field_label"
|
||||
:required="fields && fields.required === true"
|
||||
/>
|
||||
<div class="wpcfto-field-content">
|
||||
<!-- Required proxy: forces validation when repeater is empty -->
|
||||
<input
|
||||
class="wpcfto-required-proxy"
|
||||
type="text"
|
||||
:name="field_name + '__required__'"
|
||||
:value="(repeater && repeater.length ? 'ok' : '')"
|
||||
:required="fields && fields.required === true && (!repeater || repeater.length === 0)"
|
||||
:disabled="!(fields && fields.required === true && (!repeater || repeater.length === 0))"
|
||||
tabindex="-1" readonly
|
||||
style="position:absolute; left:-9999px; top:auto; width:1px; height:1px; opacity:0; pointer-events:none;"
|
||||
/>
|
||||
<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 }">
|
||||
@@ -71,10 +94,101 @@ Vue.component('wpcfto_repeater', {
|
||||
item.closed_tab = true;
|
||||
});
|
||||
}
|
||||
// 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 (typeof this.validateField === 'function') this.validateField();
|
||||
},
|
||||
beforeDestroy: function () {
|
||||
if (this.__teardown) this.__teardown();
|
||||
},
|
||||
methods: {
|
||||
focusSelf() {
|
||||
try {
|
||||
const rect = this.$el.getBoundingClientRect();
|
||||
const top = window.pageYOffset + rect.top - 60;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
} catch(e) {}
|
||||
try { this.$el.classList.add('wpcfto-invalid'); } catch(e) {}
|
||||
},
|
||||
// scrollIntoViewIfNeeded() {
|
||||
// try {
|
||||
// const top = window.pageYOffset + this.$el.getBoundingClientRect().top - 60;
|
||||
// window.scrollTo({ top, behavior: 'smooth' });
|
||||
// } catch(e) {}
|
||||
// },
|
||||
formatLabelPath(parts) {
|
||||
return parts.filter(Boolean).map(function (s) {
|
||||
return String(s).trim();
|
||||
}).filter(Boolean).join(' → ');
|
||||
},
|
||||
validateField() {
|
||||
let isValid = true;
|
||||
if ('required' in this.fields && this.fields.required === true) {
|
||||
isValid = !!(this.fields.value.length > 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
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
validateAndAddArea: function() {
|
||||
const fieldsInRepeater = this._props.fields.fields;
|
||||
let group_title = null;
|
||||
Object.values(fieldsInRepeater).forEach(field => {
|
||||
if ('is_group_title' in field) {
|
||||
if (field.is_group_title !== false) {
|
||||
group_title = field.label;
|
||||
}
|
||||
}
|
||||
});
|
||||
var invalidFields = [];
|
||||
// Loop through all repeater rows and all fields
|
||||
this.repeater.forEach((area, area_key) => {
|
||||
@@ -91,6 +205,8 @@ Vue.component('wpcfto_repeater', {
|
||||
if (childComponent.isVisible && childComponent.fields && childComponent.fields.required) {
|
||||
if (!childComponent.validateField()) {
|
||||
invalidFields.push({
|
||||
repeater_label: this._props.field_label,
|
||||
group_title: group_title,
|
||||
label: childComponent.fields.label || field_name_inner,
|
||||
area: area_key + 1
|
||||
});
|
||||
@@ -102,20 +218,25 @@ Vue.component('wpcfto_repeater', {
|
||||
}
|
||||
});
|
||||
if (invalidFields.length > 0) {
|
||||
// Compose error message
|
||||
var msg = 'Please fill all required fields before adding a new item:\n\n';
|
||||
msg += invalidFields.map(f => 'Item #' + f.area + ': ' + f.label).join('\n');
|
||||
// Use SweetAlert2 if available, otherwise alert()
|
||||
if (typeof window.Swal === 'function') {
|
||||
Repeater.fire({
|
||||
icon: 'warning',
|
||||
title: 'Required fields missing',
|
||||
html: msg.replace(/\n/g, '<br>')
|
||||
});
|
||||
} else {
|
||||
alert(msg);
|
||||
}
|
||||
return;
|
||||
// Compose error message
|
||||
var msg = 'Please fill all required fields before adding a new item:\n\n';
|
||||
const firstInvalidItem = invalidFields[0];
|
||||
msg += 'Check item #'+firstInvalidItem.area+': <b>'+this.formatLabelPath([firstInvalidItem.group_title, firstInvalidItem.label])+'</b>';
|
||||
// Use SweetAlert2 if available, otherwise alert()
|
||||
if (typeof window.Swal === 'function') {
|
||||
Repeater.fire({
|
||||
icon: 'warning',
|
||||
title: 'Required fields missing',
|
||||
html: msg.replace(/\n/g, '<br>'),
|
||||
width: 'fit-content',
|
||||
customClass: {
|
||||
confirmButton: 'btn text-bg-primary'
|
||||
},
|
||||
});
|
||||
} else {
|
||||
alert(msg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If all required fields are valid, add a new row
|
||||
this.addArea();
|
||||
@@ -134,6 +255,9 @@ Vue.component('wpcfto_repeater', {
|
||||
}
|
||||
});
|
||||
this.validateField();
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
jQuery(document).trigger('repeater-item-added', [this._props]);
|
||||
}
|
||||
},
|
||||
toggleArea: function toggleArea(area) {
|
||||
// Close all other rows
|
||||
@@ -159,6 +283,10 @@ Vue.component('wpcfto_repeater', {
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
this.repeater.splice(areaIndex, 1);
|
||||
// jQuery action hook trigger example:
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
jQuery(document).trigger('repeater-item-removed', [this._props, areaIndex]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -243,9 +371,10 @@ Vue.component('wpcfto_repeater', {
|
||||
return true;
|
||||
},
|
||||
saveChanges() {
|
||||
if (!this.validateAllRows()) {
|
||||
return; // Prevent save if validation fails
|
||||
}
|
||||
// Block if repeater required but empty OR if any child invalid
|
||||
const selfValid = this.validateField();
|
||||
// const rowsValid = this.validateAllRows();
|
||||
if (!selfValid) { return; }
|
||||
// Proceed with save logic here...
|
||||
}
|
||||
},
|
||||
@@ -253,7 +382,8 @@ Vue.component('wpcfto_repeater', {
|
||||
repeater: {
|
||||
deep: true,
|
||||
handler: function handler(repeater) {
|
||||
this.$emit('wpcfto-get-value', repeater);
|
||||
this.$emit('wpcfto-get-value', repeater);
|
||||
this.validateField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,148 +2,207 @@
|
||||
"use strict";
|
||||
|
||||
Vue.component('wpcfto_select', {
|
||||
mixins: [window.validationMixin],
|
||||
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
searchTerm: '',
|
||||
dropdownOpen: false,
|
||||
$refs: {
|
||||
nativeSelect: null,
|
||||
searchBox: null
|
||||
}
|
||||
};
|
||||
mixins: [window.validationMixin],
|
||||
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
searchTerm: '',
|
||||
dropdownOpen: false,
|
||||
// local reactive copy so we don't mutate props
|
||||
localOptions: {}, // {value: label}
|
||||
localSearchable: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
optionCount() {
|
||||
return Object.keys(this.localOptions || {}).length;
|
||||
},
|
||||
computed: {
|
||||
filteredOptions() {
|
||||
const options = this.fields.options || {};
|
||||
const selectedValue = this.value;
|
||||
|
||||
// If not searchable, show all options
|
||||
if (!this.fields.searchable) {
|
||||
return options;
|
||||
}
|
||||
|
||||
// If searchTerm is empty or less than 3 chars
|
||||
if (!this.searchTerm || this.searchTerm.length < 3) {
|
||||
// Show only selected option if exists
|
||||
if (selectedValue && options[selectedValue]) {
|
||||
return { [selectedValue]: options[selectedValue] };
|
||||
}
|
||||
// Otherwise show nothing
|
||||
return {};
|
||||
}
|
||||
|
||||
// If searchTerm has 3 or more chars, filter options
|
||||
const term = this.searchTerm.toLowerCase();
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).filter(([key, label]) =>
|
||||
label.toLowerCase().includes(term) || key.toLowerCase().includes(term)
|
||||
)
|
||||
);
|
||||
},
|
||||
selectedLabel() {
|
||||
return this.fields.options[this.value] || this.field_label || 'Select';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.value = this.field_value;
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
if (this.dropdownOpen) {
|
||||
this.searchTerm = '';
|
||||
}
|
||||
setTimeout(() => {
|
||||
const searchBox = this.$refs.searchBox;
|
||||
if (searchBox) {
|
||||
searchBox.focus()
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
selectOption(key) {
|
||||
this.value = key;
|
||||
this.$emit('wpcfto-get-value', key);
|
||||
this.dropdownOpen = false;
|
||||
// Manually trigger change event on hidden input
|
||||
const nativeSelect = this.$refs.nativeSelect;
|
||||
if (nativeSelect) {
|
||||
nativeSelect.value = key;
|
||||
const event = new Event('change', { bubbles: true });
|
||||
nativeSelect.dispatchEvent(event);
|
||||
}
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
if (!this.$el.contains(event.target)) {
|
||||
this.dropdownOpen = false;
|
||||
}
|
||||
filteredOptions() {
|
||||
const options = this.localOptions || {};
|
||||
const selectedValue = this.value;
|
||||
|
||||
// Not searchable → show all
|
||||
if (!this.localSearchable) return options;
|
||||
|
||||
const count = Object.keys(options).length;
|
||||
const hasSearch = this.searchTerm && this.searchTerm.length >= 3;
|
||||
|
||||
// If small list (≤10): show all when no search term, else filter
|
||||
if (count <= 10) {
|
||||
if (!this.searchTerm) return options;
|
||||
const term = this.searchTerm.toLowerCase();
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).filter(([key, label]) =>
|
||||
String(label).toLowerCase().includes(term) ||
|
||||
String(key).toLowerCase().includes(term)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Large list (>10): keep the 3+ chars rule
|
||||
if (!hasSearch) {
|
||||
// Show only the selected option (if any)
|
||||
if (selectedValue && options[selectedValue]) {
|
||||
return { [selectedValue]: options[selectedValue] };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Filter when 3+ chars
|
||||
const term = this.searchTerm.toLowerCase();
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).filter(([key, label]) =>
|
||||
String(label).toLowerCase().includes(term) ||
|
||||
String(key).toLowerCase().includes(term)
|
||||
)
|
||||
);
|
||||
},
|
||||
template: `
|
||||
<div class="wpcfto_generic_field wpcfto_generic_field__select" :class="{ open: dropdownOpen }">
|
||||
selectedLabel() {
|
||||
return this.localOptions[this.value] || this.field_label || 'Select';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// make a local copy of props for reactivity and to avoid mutating props
|
||||
this.localOptions = Object.assign({}, (this.fields && this.fields.options) || {});
|
||||
this.localSearchable = !!(this.fields && this.fields.searchable);
|
||||
this.value = this.field_value;
|
||||
|
||||
<wpcfto_fields_aside_before :fields="fields" :field_label="field_label" :required="fields.required === true"></wpcfto_fields_aside_before>
|
||||
// Register for external control (optional but handy)
|
||||
try {
|
||||
window.wpcftoSelectRegistry = window.wpcftoSelectRegistry || {};
|
||||
window.wpcftoSelectRegistry[this.field_id] = this;
|
||||
} catch (e) {}
|
||||
|
||||
<div class="wpcfto-field-content">
|
||||
<!-- Render custom searchable select only if searchable -->
|
||||
<div v-if="fields.searchable" class="wpcfto-custom-select" :id="field_id" @click.stop="toggleDropdown">
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
if (window.wpcftoSelectRegistry) {
|
||||
delete window.wpcftoSelectRegistry[this.field_id];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// External API to replace options & pick the right value
|
||||
// currencies: {value: label}
|
||||
// multicurrencies: [{value, label}]
|
||||
updateCurrencyScope(currencies, multicurrencies = [], savedDefault = null) {
|
||||
let optionsToSet = {};
|
||||
if (Array.isArray(multicurrencies) && multicurrencies.length > 0) {
|
||||
// standardize array -> object
|
||||
multicurrencies.forEach(obj => {
|
||||
if (obj && obj.value != null) optionsToSet[obj.value] = obj.label || obj.value;
|
||||
});
|
||||
} else {
|
||||
optionsToSet = currencies || {};
|
||||
}
|
||||
|
||||
<div class="wpcfto-selected-value">
|
||||
{{ selectedLabel }}
|
||||
<span class="wpcfto-arrow" :class="{ open: dropdownOpen }">▾</span>
|
||||
</div>
|
||||
// replace options reactively
|
||||
this.localOptions = Object.assign({}, optionsToSet);
|
||||
|
||||
<div v-if="dropdownOpen" class="wpcfto-options-dropdown">
|
||||
<input ref="searchBox" type="text" class="wpcfto-select-search" v-model="searchTerm" placeholder="Search..." @click.stop />
|
||||
// decide the selected value
|
||||
const want = savedDefault != null ? savedDefault : this.value;
|
||||
if (want && Object.prototype.hasOwnProperty.call(this.localOptions, want)) {
|
||||
this.value = want;
|
||||
} else {
|
||||
// pick first option or empty
|
||||
const firstKey = Object.keys(this.localOptions)[0] || '';
|
||||
this.value = firstKey || '';
|
||||
}
|
||||
|
||||
<ul class="wpcfto-options-list" style="padding-left: 0;">
|
||||
<li v-for="(label, key) in filteredOptions" :key="key"
|
||||
:class="{ selected: key === value }"
|
||||
@click.stop="selectOption(key)">
|
||||
{{ label }}
|
||||
</li>
|
||||
<li v-if="Object.keys(filteredOptions).length === 0" class="no-options">No options found</li>
|
||||
</ul>
|
||||
</div>
|
||||
// notify & sync hidden input
|
||||
this.$nextTick(() => {
|
||||
this.emitAndSync();
|
||||
});
|
||||
},
|
||||
|
||||
</div>
|
||||
toggleDropdown() {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
if (this.dropdownOpen) this.searchTerm = '';
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.searchBox;
|
||||
if (el) el.focus();
|
||||
});
|
||||
},
|
||||
|
||||
<!-- Render native select if not searchable -->
|
||||
<div v-else class="wpcfto-admin-select">
|
||||
<select
|
||||
v-bind:name="field_name"
|
||||
v-model="value"
|
||||
v-bind:id="field_id"
|
||||
:required="fields.required === true">
|
||||
<option v-for="(option, key) in fields.options" :value="key" :key="key">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
selectOption(key) {
|
||||
this.value = key;
|
||||
this.emitAndSync();
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
|
||||
emitAndSync() {
|
||||
this.$emit('wpcfto-get-value', this.value);
|
||||
const native = this.$refs.nativeSelect;
|
||||
if (native) {
|
||||
native.value = this.value;
|
||||
// ensure form listeners like WPCFTO dependency see it
|
||||
native.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (typeof window.wpcftoDependency !== 'undefined') {
|
||||
window.wpcftoDependency.check(this.field_id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleClickOutside(event) {
|
||||
if (!this.$el.contains(event.target)) {
|
||||
this.dropdownOpen = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// if parent updates field_value prop (rare), keep in sync
|
||||
field_value(nv) {
|
||||
if (nv !== this.value) {
|
||||
this.value = nv;
|
||||
this.$nextTick(this.emitAndSync);
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="wpcfto_generic_field wpcfto_generic_field__select" :class="{ open: dropdownOpen }" :data-field="field_id">
|
||||
|
||||
<wpcfto_fields_aside_before :fields="fields" :field_label="field_label" :required="fields.required === true"></wpcfto_fields_aside_before>
|
||||
|
||||
<div class="wpcfto-field-content">
|
||||
|
||||
<!-- Custom searchable -->
|
||||
<div v-if="localSearchable" class="wpcfto-custom-select" :id="field_id" @click.stop="toggleDropdown">
|
||||
<div class="wpcfto-selected-value">
|
||||
{{ selectedLabel }}
|
||||
<span class="wpcfto-arrow" :class="{ open: dropdownOpen }">▾</span>
|
||||
</div>
|
||||
|
||||
<wpcfto_fields_aside_after :fields="fields"></wpcfto_fields_aside_after>
|
||||
<div v-if="dropdownOpen" class="wpcfto-options-dropdown">
|
||||
<input ref="searchBox" type="text" class="wpcfto-select-search" v-model="searchTerm" placeholder="Search..." @click.stop />
|
||||
<ul class="wpcfto-options-list" style="padding-left:0;">
|
||||
<li v-for="(label, key) in filteredOptions" :key="key"
|
||||
:class="{ selected: key === value }"
|
||||
@click.stop="selectOption(key)">
|
||||
{{ label }}
|
||||
</li>
|
||||
<li v-if="Object.keys(filteredOptions).length === 0" class="no-options">No options found</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden input to submit form value for custom select -->
|
||||
<input ref="nativeSelect" v-if="fields.searchable" type="hidden" :name="field_name" v-model="value" :required="fields.required === true" />
|
||||
<!-- Native select -->
|
||||
<div v-else class="wpcfto-admin-select">
|
||||
<select :name="field_name" v-model="value" :id="field_id" :required="fields.required === true">
|
||||
<option v-for="(option, key) in localOptions" :value="key" :key="key">{{ option }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.$emit('wpcfto-get-value', newVal);
|
||||
this.$nextTick(() => {
|
||||
if (typeof window.wpcftoDependency !== 'undefined') {
|
||||
window.wpcftoDependency.check(this.field_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
<wpcfto_fields_aside_after :fields="fields"></wpcfto_fields_aside_after>
|
||||
|
||||
<!-- Hidden input to submit form value for custom select -->
|
||||
<input ref="nativeSelect" v-if="localSearchable" type="hidden" :name="field_name" v-model="value" :required="fields.required === true" />
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZha2VfYzRiZDk3ZTMuanMiXSwibmFtZXMiOlsiVnVlIiwiY29tcG9uZW50IiwicHJvcHMiLCJkYXRhIiwidmFsdWUiLCJ0ZW1wbGF0ZSIsIm1vdW50ZWQiLCJmaWVsZF92YWx1ZSIsIm1ldGhvZHMiLCJ3YXRjaCIsIl92YWx1ZSIsIiRlbWl0Il0sIm1hcHBpbmdzIjoiQUFBQTs7QUFFQUEsR0FBRyxDQUFDQyxTQUFKLENBQWMsZUFBZCxFQUErQjtBQUM3QkMsRUFBQUEsS0FBSyxFQUFFLENBQUMsUUFBRCxFQUFXLGFBQVgsRUFBMEIsWUFBMUIsRUFBd0MsVUFBeEMsRUFBb0QsYUFBcEQsQ0FEc0I7QUFFN0JDLEVBQUFBLElBQUksRUFBRSxTQUFTQSxJQUFULEdBQWdCO0FBQ3BCLFdBQU87QUFDTEMsTUFBQUEsS0FBSyxFQUFFO0FBREYsS0FBUDtBQUdELEdBTjRCO0FBTzdCQyxFQUFBQSxRQUFRLEVBQUUsNndCQVBtQjtBQVE3QkMsRUFBQUEsT0FBTyxFQUFFLFNBQVNBLE9BQVQsR0FBbUI7QUFDMUIsU0FBS0YsS0FBTCxHQUFhLEtBQUtHLFdBQWxCO0FBQ0QsR0FWNEI7QUFXN0JDLEVBQUFBLE9BQU8sRUFBRSxFQVhvQjtBQVk3QkMsRUFBQUEsS0FBSyxFQUFFO0FBQ0xMLElBQUFBLEtBQUssRUFBRSxTQUFTQSxLQUFULENBQWVNLE1BQWYsRUFBdUI7QUFDNUIsV0FBS0MsS0FBTCxDQUFXLGtCQUFYLEVBQStCRCxNQUEvQjtBQUNEO0FBSEk7QUFac0IsQ0FBL0IiLCJzb3VyY2VzQ29udGVudCI6WyJcInVzZSBzdHJpY3RcIjtcblxuVnVlLmNvbXBvbmVudCgnd3BjZnRvX3NlbGVjdCcsIHtcbiAgcHJvcHM6IFsnZmllbGRzJywgJ2ZpZWxkX2xhYmVsJywgJ2ZpZWxkX25hbWUnLCAnZmllbGRfaWQnLCAnZmllbGRfdmFsdWUnXSxcbiAgZGF0YTogZnVuY3Rpb24gZGF0YSgpIHtcbiAgICByZXR1cm4ge1xuICAgICAgdmFsdWU6ICcnXG4gICAgfTtcbiAgfSxcbiAgdGVtcGxhdGU6IFwiXFxuICAgICAgICA8ZGl2IGNsYXNzPVxcXCJ3cGNmdG9fZ2VuZXJpY19maWVsZCB3cGNmdG9fZ2VuZXJpY19maWVsZF9fc2VsZWN0XFxcIj5cXG5cXG4gICAgICAgICAgICA8d3BjZnRvX2ZpZWxkc19hc2lkZV9iZWZvcmUgOmZpZWxkcz1cXFwiZmllbGRzXFxcIiA6ZmllbGRfbGFiZWw9XFxcImZpZWxkX2xhYmVsXFxcIj48L3dwY2Z0b19maWVsZHNfYXNpZGVfYmVmb3JlPlxcbiAgICAgICAgICAgIFxcbiAgICAgICAgICAgIDxkaXYgY2xhc3M9XFxcIndwY2Z0by1maWVsZC1jb250ZW50XFxcIj5cXG4gICAgICAgICAgICAgICAgPGRpdiBjbGFzcz1cXFwid3BjZnRvLWFkbWluLXNlbGVjdFxcXCI+XFxuICAgICAgICAgICAgICAgICAgICA8c2VsZWN0IHYtYmluZDpuYW1lPVxcXCJmaWVsZF9uYW1lXFxcIlxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2LW1vZGVsPVxcXCJ2YWx1ZVxcXCJcXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdi1iaW5kOmlkPVxcXCJmaWVsZF9pZFxcXCI+XFxuICAgICAgICAgICAgICAgICAgICAgICAgPG9wdGlvbiB2LWZvcj1cXFwiKG9wdGlvbiwga2V5KSBpbiBmaWVsZHNbJ29wdGlvbnMnXVxcXCIgdi1iaW5kOnZhbHVlPVxcXCJrZXlcXFwiPnt7IG9wdGlvbiB9fTwvb3B0aW9uPlxcbiAgICAgICAgICAgICAgICAgICAgPC9zZWxlY3Q+XFxuICAgICAgICAgICAgICAgIDwvZGl2PlxcbiAgICAgICAgICAgIDwvZGl2PlxcblxcbiAgICAgICAgICAgIDx3cGNmdG9fZmllbGRzX2FzaWRlX2FmdGVyIDpmaWVsZHM9XFxcImZpZWxkc1xcXCI+PC93cGNmdG9fZmllbGRzX2FzaWRlX2FmdGVyPlxcblxcbiAgICAgICAgPC9kaXY+XFxuICAgIFwiLFxuICBtb3VudGVkOiBmdW5jdGlvbiBtb3VudGVkKCkge1xuICAgIHRoaXMudmFsdWUgPSB0aGlzLmZpZWxkX3ZhbHVlO1xuICB9LFxuICBtZXRob2RzOiB7fSxcbiAgd2F0Y2g6IHtcbiAgICB2YWx1ZTogZnVuY3Rpb24gdmFsdWUoX3ZhbHVlKSB7XG4gICAgICB0aGlzLiRlbWl0KCd3cGNmdG8tZ2V0LXZhbHVlJywgX3ZhbHVlKTtcbiAgICB9XG4gIH1cbn0pOyJdfQ==
|
||||
},{}]},{},[1])
|
||||
Reference in New Issue
Block a user