update multicurrencies functionality on global level
This commit is contained in:
@@ -1,4 +1,89 @@
|
|||||||
jQuery(function($){
|
jQuery(function($){
|
||||||
|
|
||||||
|
console.log(window);
|
||||||
|
// helper: element -> vue component
|
||||||
|
function getVueByFieldId(fieldId) {
|
||||||
|
// Prefer registry if present
|
||||||
|
if (window.wpcftoSelectRegistry && window.wpcftoSelectRegistry[fieldId]) {
|
||||||
|
return window.wpcftoSelectRegistry[fieldId];
|
||||||
|
}
|
||||||
|
// fallback: find by id and read __vue__
|
||||||
|
var el = document.getElementById(fieldId);
|
||||||
|
return el && el.__vue__ ? el.__vue__ : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertLabelValueArray(arr) {
|
||||||
|
const result = {};
|
||||||
|
$.each(arr || [], function(_, obj) {
|
||||||
|
if (obj && obj.value != null) result[obj.value] = obj.label || obj.value;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultCurrencyDetected = false;
|
||||||
|
let multicurrencyActive = Boolean(formipay_admin_setting.multicurrency);
|
||||||
|
let all_currencies_array = formipay_admin_setting.all_currencies;
|
||||||
|
let global_currencies_array = formipay_admin_setting.global_selected_currencies;
|
||||||
|
let all_currencies_obj = formipay_admin_setting.all_currencies; // {value:label}
|
||||||
|
let global_currencies_obj = formipay_admin_setting.global_selected_currencies;
|
||||||
|
let saved_default_currency = formipay_admin_setting.default_currency;
|
||||||
|
// let defaultCurrencyVM = null;
|
||||||
|
// Default Currency Options
|
||||||
|
console.log(formipay_admin_setting);
|
||||||
|
|
||||||
|
function set_default_currency_options(fieldId, currenciesObj, multicurrenciesArray) {
|
||||||
|
const vm = getVueByFieldId(fieldId);
|
||||||
|
if (!vm || typeof vm.updateCurrencyScope !== 'function') {
|
||||||
|
console.error('wpcfto_select Vue instance not found or API missing for', fieldId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// vm accepts both shapes and an optional saved default
|
||||||
|
vm.updateCurrencyScope(currenciesObj, multicurrenciesArray || [], saved_default_currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bootstrap once field exists in DOM
|
||||||
|
const FIELD_ID = 'General-default_currency'; // your actual field_id
|
||||||
|
const detectTimer = setInterval(() => {
|
||||||
|
const vm = getVueByFieldId(FIELD_ID);
|
||||||
|
if (vm) {
|
||||||
|
const base = multicurrencyActive ? global_currencies_obj : all_currencies_obj;
|
||||||
|
set_default_currency_options(FIELD_ID, base, []); // initial
|
||||||
|
clearInterval(detectTimer);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
// reacting to multicurrency toggle
|
||||||
|
$(document).on('change', '[name=enable_multicurrency]', function() {
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
set_default_currency_options(FIELD_ID, global_currencies_obj, collectRepeaterSelectedCurrencies());
|
||||||
|
} else {
|
||||||
|
set_default_currency_options(FIELD_ID, all_currencies_obj, []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// gather repeater currencies into [{value,label}]
|
||||||
|
function collectRepeaterSelectedCurrencies() {
|
||||||
|
var items = [];
|
||||||
|
var repeaterItems = $('.multicurrencies.repeater').find('.wpcfto-repeater-single');
|
||||||
|
$.each(repeaterItems, function(_, obj) {
|
||||||
|
var label = $(obj).find('.wpcfto_group_title').text();
|
||||||
|
var value = $(obj).find('[field_native_name_inner=currency]').find('input').val();
|
||||||
|
if (value) items.push({ label: label || value, value: value });
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('change', '[field_native_name_inner=currency] input', function() {
|
||||||
|
set_default_currency_options(FIELD_ID, global_currencies_obj, collectRepeaterSelectedCurrencies());
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('repeater-item-removed repeater-item-added', function(event, repeater) {
|
||||||
|
if (repeater && repeater.field_name == 'multicurrencies') {
|
||||||
|
set_default_currency_options(FIELD_ID, global_currencies_obj, collectRepeaterSelectedCurrencies());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paypal
|
||||||
$(document).on('click', '.show-instruction', function(){
|
$(document).on('click', '.show-instruction', function(){
|
||||||
$('.global-paypal-instruction').slideToggle();
|
$('.global-paypal-instruction').slideToggle();
|
||||||
$(this).text(function(i, text){
|
$(this).text(function(i, text){
|
||||||
|
|||||||
@@ -33,6 +33,71 @@ function formipay_currency_array() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formipay_is_multi_currency_active() {
|
||||||
|
|
||||||
|
$formipay_settings = get_option('formipay_settings');
|
||||||
|
$is_active = $formipay_settings['enable_multicurrency'];
|
||||||
|
|
||||||
|
return (bool) $is_active;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function formipay_default_currency($return='raw') {
|
||||||
|
|
||||||
|
$formipay_settings = get_option('formipay_settings');
|
||||||
|
$default_currency = $formipay_settings['default_currency'];
|
||||||
|
|
||||||
|
switch ($return) {
|
||||||
|
|
||||||
|
case 'symbol':
|
||||||
|
$output = formipay_get_currency_data_by_value($default_currency, 'symbol');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'title':
|
||||||
|
$output = formipay_get_currency_data_by_value($default_currency, 'title');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'decimal_digits':
|
||||||
|
$output = $formipay_settings['default_currency_decimal_digits'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'decimal_symbol':
|
||||||
|
$output = $formipay_settings['default_currency_decimal_symbol'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'thousand_separator':
|
||||||
|
$output = $formipay_settings['default_currency_thousand_separator'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$output = $formipay_settings['default_currency'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function formipay_global_currency_options() {
|
||||||
|
|
||||||
|
$formipay_settings = get_option('formipay_settings');
|
||||||
|
// $currencies = (false !== boolval($formipay_settings['enable_multicurrency'])) ? formipay_default_currency() : [];
|
||||||
|
$currencies = [];
|
||||||
|
if(false !== boolval($formipay_settings['enable_multicurrency']) && !empty($formipay_settings['multicurrencies'])) {
|
||||||
|
foreach($formipay_settings['multicurrencies'] as $currency){
|
||||||
|
$currency_value = $currency['currency'];
|
||||||
|
$currency_label = formipay_get_currency_data_by_value($currency_value, 'title');
|
||||||
|
$currencies[$currency_value] = $currency_label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if(empty($currencies)){
|
||||||
|
// $currencies[formipay_default_currency()] = formipay_default_currency('title');
|
||||||
|
// }
|
||||||
|
|
||||||
|
return $currencies;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function formipay_country_array() {
|
function formipay_country_array() {
|
||||||
|
|
||||||
$json = file_get_contents(FORMIPAY_PATH . 'admin/assets/json/country.json');
|
$json = file_get_contents(FORMIPAY_PATH . 'admin/assets/json/country.json');
|
||||||
@@ -137,42 +202,6 @@ function formipay_get_currency_data_by_value($value, $data='') {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formipay_default_currency($return='raw') {
|
|
||||||
|
|
||||||
$formipay_settings = get_option('formipay_settings');
|
|
||||||
$default_currency = $formipay_settings['payment_default_currency'];
|
|
||||||
|
|
||||||
switch ($return) {
|
|
||||||
|
|
||||||
case 'symbol':
|
|
||||||
$output = formipay_get_currency_data_by_value($default_currency, 'symbol');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'title':
|
|
||||||
$output = formipay_get_currency_data_by_value($default_currency, 'title');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'decimal_digits':
|
|
||||||
$output = $formipay_settings['payment_default_currency_decimal_digits'];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'decimal_symbol':
|
|
||||||
$output = $formipay_settings['payment_default_currency_decimal_symbol'];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'thousand_separator':
|
|
||||||
$output = $formipay_settings['payment_default_currency_thousand_separator'];
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
$output = $formipay_settings['payment_default_currency'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function formipay_get_post_meta($post_id, $metakey) {
|
function formipay_get_post_meta($post_id, $metakey) {
|
||||||
|
|
||||||
$value = get_post_meta($post_id, $metakey, true);
|
$value = get_post_meta($post_id, $metakey, true);
|
||||||
@@ -875,11 +904,4 @@ function formipay_thankyoupage_allowed_html() {
|
|||||||
'td' => [],
|
'td' => [],
|
||||||
'br' => []
|
'br' => []
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// add_action('admin_notices', function() {
|
|
||||||
// global $current_screen;
|
|
||||||
// echo '<pre>';
|
|
||||||
// print_r($current_screen);
|
|
||||||
// echo '</pre>';
|
|
||||||
// });
|
|
||||||
@@ -24,6 +24,9 @@ define( 'FORMIPAY_NAME', 'Formipay' );
|
|||||||
define( 'FORMIPAY_VERSION', '1.0.0' );
|
define( 'FORMIPAY_VERSION', '1.0.0' );
|
||||||
define( 'FORMIPAY_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'FORMIPAY_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'FORMIPAY_URL', plugin_dir_url( __FILE__ ) );
|
define( 'FORMIPAY_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
define( 'FORMIPAY_MENU_SLUG', 'formipay' );
|
||||||
|
define( 'FORMIPAY_OPTION_KEY', 'formipay_settings' );
|
||||||
|
|
||||||
|
|
||||||
require_once FORMIPAY_PATH . 'admin/functions.php';
|
require_once FORMIPAY_PATH . 'admin/functions.php';
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class Init {
|
|||||||
'Formipay',
|
'Formipay',
|
||||||
'Formipay',
|
'Formipay',
|
||||||
'manage_options',
|
'manage_options',
|
||||||
'formipay',
|
FORMIPAY_MENU_SLUG,
|
||||||
'',
|
'',
|
||||||
FORMIPAY_URL . 'admin/assets/img/White.png',
|
FORMIPAY_URL . 'admin/assets/img/White.png',
|
||||||
5
|
5
|
||||||
|
|||||||
@@ -52,23 +52,6 @@ abstract class Payment {
|
|||||||
|
|
||||||
public function add_form_payment_menu($fields) {
|
public function add_form_payment_menu($fields) {
|
||||||
|
|
||||||
$gateways = apply_filters( 'formipay/form-config/tab:payments/gateways', [] );
|
|
||||||
|
|
||||||
$payment_options = [];
|
|
||||||
if(!empty($gateways)){
|
|
||||||
foreach($gateways as $gateway){
|
|
||||||
$id = $gateway['id'];
|
|
||||||
$label = $gateway['gateway'];
|
|
||||||
if(isset($gateway['channel'])){
|
|
||||||
$label .= ' - '.$gateway['channel'];
|
|
||||||
}
|
|
||||||
$payment_options[] = [
|
|
||||||
'id' => $id,
|
|
||||||
'label' => $label
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$payment_fields = [
|
$payment_fields = [
|
||||||
'payment_section_group' => array(
|
'payment_section_group' => array(
|
||||||
'type' => 'group_title',
|
'type' => 'group_title',
|
||||||
@@ -82,23 +65,6 @@ abstract class Payment {
|
|||||||
'submenu' => __( 'General', 'formipay' ),
|
'submenu' => __( 'General', 'formipay' ),
|
||||||
'value' => 'Payment Methods',
|
'value' => 'Payment Methods',
|
||||||
),
|
),
|
||||||
'payment_gateways' => array(
|
|
||||||
'type' => 'sorter',
|
|
||||||
'label' => __( 'Choose one or any', 'formipay' ),
|
|
||||||
'options' => array(
|
|
||||||
array(
|
|
||||||
'id' => 'inactive',
|
|
||||||
'name' => __( 'Inactive', 'formipay' ),
|
|
||||||
'options' => $payment_options,
|
|
||||||
),
|
|
||||||
array(
|
|
||||||
'id' => 'active',
|
|
||||||
'name' => __( 'Active', 'formipay' ),
|
|
||||||
'options' => [],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'submenu' => __( 'General', 'formipay' ),
|
|
||||||
),
|
|
||||||
'payment_label' => array(
|
'payment_label' => array(
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'label' => __( 'Show Payment Label', 'formipay' ),
|
'label' => __( 'Show Payment Label', 'formipay' ),
|
||||||
|
|||||||
@@ -21,6 +21,20 @@ class Settings {
|
|||||||
|
|
||||||
public function theme_option($setups){
|
public function theme_option($setups){
|
||||||
|
|
||||||
|
$gateways = apply_filters( 'formipay/form-config/tab:payments/gateways', [] );
|
||||||
|
|
||||||
|
$payment_checkboxes = [];
|
||||||
|
if(!empty($gateways)){
|
||||||
|
foreach($gateways as $gateway){
|
||||||
|
$id = $gateway['id'];
|
||||||
|
$label = $gateway['gateway'];
|
||||||
|
if(isset($gateway['channel'])){
|
||||||
|
$label .= ' - '.$gateway['channel'];
|
||||||
|
}
|
||||||
|
$payment_checkboxes[$id] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$general_fields = array(
|
$general_fields = array(
|
||||||
'business_group' => array(
|
'business_group' => array(
|
||||||
'type' => 'group_title',
|
'type' => 'group_title',
|
||||||
@@ -39,31 +53,98 @@ class Settings {
|
|||||||
'label' => __( 'Currency', 'formipay' ),
|
'label' => __( 'Currency', 'formipay' ),
|
||||||
'group' => 'started'
|
'group' => 'started'
|
||||||
),
|
),
|
||||||
'payment_default_currency' => array(
|
'enable_multicurrency' => [
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => __( 'Enable Multi Currency', 'formipay' )
|
||||||
|
],
|
||||||
|
'multicurrencies' => [
|
||||||
|
'type' => 'repeater',
|
||||||
|
'label' => __( 'Currencies', 'formipay' ),
|
||||||
|
'fields' => [
|
||||||
|
'currency' => array(
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => __('Default Currency', 'formipay'),
|
||||||
|
'value' => 'IDR:::Indonesian rupiah:::Rp',
|
||||||
|
'options' => formipay_currency_as_options(),
|
||||||
|
'required' => true,
|
||||||
|
'searchable' => true,
|
||||||
|
'is_group_title' => true
|
||||||
|
),
|
||||||
|
'decimal_digits' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'label' => __('Decimal Digits', 'formipay'),
|
||||||
|
'value' => '2',
|
||||||
|
'required' => true,
|
||||||
|
),
|
||||||
|
'decimal_symbol' => array(
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => __('Decimal Symbol', 'formipay'),
|
||||||
|
'value' => '.',
|
||||||
|
'required' => true,
|
||||||
|
),
|
||||||
|
'thousand_separator' => array(
|
||||||
|
'type' => 'text',
|
||||||
|
'label' => __('Thousand Separator Symbol', 'formipay'),
|
||||||
|
'value' => ',',
|
||||||
|
'required' => true,
|
||||||
|
),
|
||||||
|
'payment_gateways' => array(
|
||||||
|
'type' => 'multi_checkbox',
|
||||||
|
'label' => __( 'Payment Gateways', 'formipay' ),
|
||||||
|
'options' => $payment_checkboxes,
|
||||||
|
'submenu' => __( 'General', 'formipay' ),
|
||||||
|
),
|
||||||
|
'payment_gateways_select' => array(
|
||||||
|
'type' => 'multiselect',
|
||||||
|
'label' => __( 'Payment Gateways', 'formipay' ),
|
||||||
|
'options' => $payment_checkboxes,
|
||||||
|
'submenu' => __( 'General', 'formipay' ),
|
||||||
|
'placeholder' => 'Select related Payments'
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'required' => true,
|
||||||
|
'dependency' => [
|
||||||
|
'key' => 'enable_multicurrency',
|
||||||
|
'value' => 'not_empty'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'default_currency' => array(
|
||||||
'type' => 'select',
|
'type' => 'select',
|
||||||
'label' => __('Default Currency', 'formipay'),
|
'label' => __('Default Currency', 'formipay'),
|
||||||
'value' => 'IDR:::Indonesian rupiah:::Rp',
|
'value' => 'IDR:::Indonesian rupiah:::Rp',
|
||||||
'options' => formipay_currency_as_options(),
|
'options' => formipay_currency_as_options(),
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'searchable' => true,
|
'searchable' => true
|
||||||
),
|
),
|
||||||
'payment_default_currency_decimal_digits' => array(
|
'default_currency_decimal_digits' => array(
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'label' => __('Decimal Digits', 'formipay'),
|
'label' => __('Decimal Digits', 'formipay'),
|
||||||
'value' => '2',
|
'value' => '2',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
'dependency' => [
|
||||||
|
'key' => 'enable_multicurrency',
|
||||||
|
'value' => 'empty'
|
||||||
|
]
|
||||||
),
|
),
|
||||||
'payment_default_currency_decimal_symbol' => array(
|
'default_currency_decimal_symbol' => array(
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'label' => __('Decimal Symbol', 'formipay'),
|
'label' => __('Decimal Symbol', 'formipay'),
|
||||||
'value' => '.',
|
'value' => '.',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
'dependency' => [
|
||||||
|
'key' => 'enable_multicurrency',
|
||||||
|
'value' => 'empty'
|
||||||
|
]
|
||||||
),
|
),
|
||||||
'payment_default_currency_thousand_separator' => array(
|
'default_currency_thousand_separator' => array(
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'label' => __('Thousand Separator Symbol', 'formipay'),
|
'label' => __('Thousand Separator Symbol', 'formipay'),
|
||||||
'value' => ',',
|
'value' => ',',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
|
'dependency' => [
|
||||||
|
'key' => 'enable_multicurrency',
|
||||||
|
'value' => 'empty'
|
||||||
|
],
|
||||||
'group' => 'ended'
|
'group' => 'ended'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -211,7 +292,11 @@ class Settings {
|
|||||||
wp_localize_script( 'admin-setting-script', 'formipay_admin_setting', [
|
wp_localize_script( 'admin-setting-script', 'formipay_admin_setting', [
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
'site_url' => site_url(),
|
'site_url' => site_url(),
|
||||||
'nonce' => wp_create_nonce('formipay-admin-nonce')
|
'nonce' => wp_create_nonce('formipay-admin-nonce'),
|
||||||
|
'multicurrency' => formipay_is_multi_currency_active(),
|
||||||
|
'all_currencies' => formipay_currency_as_options(),
|
||||||
|
'global_selected_currencies' => formipay_global_currency_options(),
|
||||||
|
'default_currency' => formipay_default_currency()
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
vendor/wpcfto.zip
vendored
BIN
vendor/wpcfto.zip
vendored
Binary file not shown.
2
vendor/wpcfto/WPCFTO.php
vendored
2
vendor/wpcfto/WPCFTO.php
vendored
@@ -14,6 +14,7 @@
|
|||||||
* @license http://www.gnu.org/licenses/gpl-3.0.html
|
* @license http://www.gnu.org/licenses/gpl-3.0.html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
add_action('plugins_loaded', function () {
|
add_action('plugins_loaded', function () {
|
||||||
|
|
||||||
if (!function_exists('get_plugin_data')) {
|
if (!function_exists('get_plugin_data')) {
|
||||||
@@ -30,6 +31,7 @@ add_action('plugins_loaded', function () {
|
|||||||
define('STM_WPCFTO_FILE', __FILE__);
|
define('STM_WPCFTO_FILE', __FILE__);
|
||||||
define('STM_WPCFTO_PATH', dirname(STM_WPCFTO_FILE));
|
define('STM_WPCFTO_PATH', dirname(STM_WPCFTO_FILE));
|
||||||
define('STM_WPCFTO_URL', plugin_dir_url(STM_WPCFTO_FILE));
|
define('STM_WPCFTO_URL', plugin_dir_url(STM_WPCFTO_FILE));
|
||||||
|
define('WPCFTO_FORK','dewebox-2025.08');
|
||||||
|
|
||||||
class Stylemix_WPCFTO
|
class Stylemix_WPCFTO
|
||||||
{
|
{
|
||||||
|
|||||||
2
vendor/wpcfto/metaboxes/assets/css/main.css
vendored
2
vendor/wpcfto/metaboxes/assets/css/main.css
vendored
File diff suppressed because one or more lines are too long
348
vendor/wpcfto/metaboxes/assets/js/metaboxes.js
vendored
348
vendor/wpcfto/metaboxes/assets/js/metaboxes.js
vendored
@@ -19,33 +19,201 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatLabel(fieldId, str, tab) {
|
function formatLabelPath(parts) {
|
||||||
if(fieldId == str){
|
return parts.filter(Boolean).map(function (s) {
|
||||||
str = str
|
return String(s).trim();
|
||||||
.split('-') // Split by hyphen
|
}).filter(Boolean).join(' → ');
|
||||||
.map(part =>
|
|
||||||
part
|
|
||||||
.split('_') // Split by underscore
|
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1)) // Capitalize
|
|
||||||
.join(' ')
|
|
||||||
)
|
|
||||||
.join(' → ');
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
return tab + ' → ' + str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerSwalEmptyRequired(fieldId, firstLabel, tab) {
|
function triggerSwalEmptyRequired(arg1, firstLabel, tab) {
|
||||||
|
// Supports both old signature (fieldId, firstLabel, tab)
|
||||||
|
// and new signature (object { id, tab, group, repeater, label, parent })
|
||||||
|
var path = '';
|
||||||
|
var p;
|
||||||
|
if (typeof arg1 === 'object' && arg1 !== null) {
|
||||||
|
p = arg1;
|
||||||
|
// Include parent (repeater parent field label) when present
|
||||||
|
path = formatLabelPath([p.tab, p.group, p.parent, p.repeater, p.label]);
|
||||||
|
} else {
|
||||||
|
// Fallback to old behavior
|
||||||
|
if (firstLabel) {
|
||||||
|
path = formatLabelPath([tab, firstLabel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wider dialog for repeater breadcrumb to avoid wrapping
|
||||||
|
var isRepeaterContext = (typeof p !== 'undefined') && (!!p.parent || !!p.repeater);
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Empty Required Field',
|
title: 'Empty Required Field',
|
||||||
html: firstLabel ? 'Check: <b>' + formatLabel(fieldId, firstLabel, tab) + '</b>' : '',
|
html: path ? 'Check: <b>' + path + '</b>' : '',
|
||||||
|
width: isRepeaterContext ? '50em' : undefined,
|
||||||
customClass: {
|
customClass: {
|
||||||
confirmButton: 'btn text-bg-primary'
|
confirmButton: 'btn text-bg-primary'
|
||||||
|
},
|
||||||
|
didOpen: (popup) => {
|
||||||
|
if (isRepeaterContext) {
|
||||||
|
var htmlC = popup.querySelector('.swal2-html-container');
|
||||||
|
if (htmlC) {
|
||||||
|
htmlC.style.whiteSpace = 'nowrap';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === DeweBox/Nuxy required/visibility helpers (global) ===
|
||||||
|
function isBoxChildVisible($box, includeHidden) {
|
||||||
|
if (includeHidden === true) return true; // scan *all* tabs/sections
|
||||||
|
return $box.is(':visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFieldValue($input) {
|
||||||
|
const id = $input.attr('id');
|
||||||
|
const tag = $input.prop('tagName');
|
||||||
|
const type= ($input.attr('type') || '').toLowerCase();
|
||||||
|
|
||||||
|
// TinyMCE
|
||||||
|
if (tag === 'TEXTAREA' && typeof window.tinymce !== 'undefined') {
|
||||||
|
const ed = tinymce.get(id);
|
||||||
|
if (ed) return ed.getContent();
|
||||||
|
}
|
||||||
|
// CodeMirror via WP code editor (textarea sibling with .CodeMirror)
|
||||||
|
if (tag === 'TEXTAREA' && window.wp && wp.codeEditor) {
|
||||||
|
const cmWrap = $input.next('.CodeMirror');
|
||||||
|
if (cmWrap.length && cmWrap[0].CodeMirror) {
|
||||||
|
return cmWrap[0].CodeMirror.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type === 'checkbox' || type === 'radio') {
|
||||||
|
const name = $input.attr('name');
|
||||||
|
return !!$(`[name="${name}"]:checked`).length;
|
||||||
|
}
|
||||||
|
return $.trim($input.val() || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRequiredEmpty($input) {
|
||||||
|
if (!$input.is('[required]')) return false;
|
||||||
|
const tag = $input.prop('tagName');
|
||||||
|
const type = ($input.attr('type') || '').toLowerCase();
|
||||||
|
if (type === 'checkbox' || type === 'radio') {
|
||||||
|
const name = $input.attr('name');
|
||||||
|
return !$(`[name="${name}"]:checked`).length;
|
||||||
|
}
|
||||||
|
const v = readFieldValue($input);
|
||||||
|
return v === '' || v === null || typeof v === 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectInvalidFields(includeHidden) {
|
||||||
|
const invalid = [];
|
||||||
|
// Ensure TinyMCE mirrors content back to textarea before reading values
|
||||||
|
if (window.tinymce) tinymce.triggerSave();
|
||||||
|
|
||||||
|
$('.wpcfto-box-child').each(function () {
|
||||||
|
const $boxChild = $(this);
|
||||||
|
const fieldId = $boxChild.attr('data-field');
|
||||||
|
const $tab = $boxChild.closest('.wpcfto-tab');
|
||||||
|
const tabId = $tab.attr('id');
|
||||||
|
const tabTitle = $(`[data-section=${tabId}]`).text();
|
||||||
|
|
||||||
|
if (!isBoxChildVisible($boxChild, includeHidden)) return;
|
||||||
|
|
||||||
|
// Try to find nearest group title before this field
|
||||||
|
const groupLabel = ($boxChild.prevAll('.wpcfto_group_started:first')
|
||||||
|
.find('.wpcfto-field-aside__label span:first-child').text() || '').trim();
|
||||||
|
|
||||||
|
if ($boxChild.hasClass('repeater')) {
|
||||||
|
// Repeater parent label (the field label for the repeater itself)
|
||||||
|
const parentLabel = ($boxChild.find('.wpcfto-field-aside__label span:first-child').first().text() || '').trim();
|
||||||
|
|
||||||
|
// checker for the parent itself
|
||||||
|
if($boxChild.find('.wpcfto-repeater-single').length == 0){
|
||||||
|
invalid.push({
|
||||||
|
id: fieldId,
|
||||||
|
tab: tabTitle,
|
||||||
|
group: groupLabel,
|
||||||
|
parent: parentLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$boxChild.find('.wpcfto-repeater-single').each(function (idx) {
|
||||||
|
const $item = $(this);
|
||||||
|
|
||||||
|
// Repeater item label heuristics (prefer the Vue-rendered title)
|
||||||
|
const titleEl = $item.find('.wpcfto_group_title').first();
|
||||||
|
let repeaterLabel = (titleEl.text() || '').toString().trim();
|
||||||
|
|
||||||
|
if (!repeaterLabel) {
|
||||||
|
repeaterLabel = (
|
||||||
|
$item.find('[data-repeater-label]').val() ||
|
||||||
|
$item.find('[name*="[label]"]').val() ||
|
||||||
|
$item.find('[name*="[title]"]').val() ||
|
||||||
|
$item.find('[name*="[name]"]').val() ||
|
||||||
|
$item.find('.wpcfto-repeater-title, .wpcfto-repeater-item-label').first().text() ||
|
||||||
|
''
|
||||||
|
).toString().trim();
|
||||||
|
}
|
||||||
|
if (!repeaterLabel) repeaterLabel = `Item #${idx+1}`;
|
||||||
|
|
||||||
|
$item.find('input, textarea, select').filter('[required]').each(function () {
|
||||||
|
const $f = $(this);
|
||||||
|
if (isRequiredEmpty($f)) {
|
||||||
|
const emptyLabel = $f.closest('.wpcfto_generic_field')
|
||||||
|
.find('.wpcfto-field-aside__label span:first-child').text() || fieldId;
|
||||||
|
invalid.push({
|
||||||
|
id: fieldId,
|
||||||
|
tab: tabTitle,
|
||||||
|
group: groupLabel,
|
||||||
|
parent: parentLabel,
|
||||||
|
repeater: repeaterLabel,
|
||||||
|
label: emptyLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$boxChild.find('input, textarea, select').filter('[required]').each(function () {
|
||||||
|
const $f = $(this);
|
||||||
|
if (isRequiredEmpty($f)) {
|
||||||
|
const fieldLabel = $(`[data-field=${fieldId}] label > span:first-child`).text() || fieldId;
|
||||||
|
invalid.push({
|
||||||
|
id: fieldId,
|
||||||
|
tab: tabTitle,
|
||||||
|
group: groupLabel,
|
||||||
|
repeater: null,
|
||||||
|
label: fieldLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstInvalidField(includeHidden) {
|
||||||
|
const inv = collectInvalidFields(includeHidden);
|
||||||
|
return inv.length ? inv[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusFirstInvalid(invalid) {
|
||||||
|
if (!invalid) return;
|
||||||
|
const $targetTab = $(`[data-field=${invalid.id}]`).closest('.wpcfto-tab');
|
||||||
|
const tabId = $targetTab.attr('id');
|
||||||
|
if (tabId) {
|
||||||
|
// try Vue's changeTab if available
|
||||||
|
if (typeof vm !== 'undefined' && vm.changeTab) {
|
||||||
|
vm.changeTab(tabId);
|
||||||
|
} else {
|
||||||
|
$('#' + tabId).addClass('active').siblings('.wpcfto-tab').removeClass('active');
|
||||||
|
$(`[data-section="${tabId}"]`).closest('.wpcfto-nav').addClass('active').siblings('.wpcfto-nav').removeClass('active');
|
||||||
|
}
|
||||||
|
$('html, body').animate({ scrollTop: $(`[data-field=${invalid.id}]`).offset().top - 120 }, 'fast');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$('[data-vue]').each(function () {
|
$('[data-vue]').each(function () {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
var data_var = $this.attr('data-vue');
|
var data_var = $this.attr('data-vue');
|
||||||
@@ -77,96 +245,56 @@
|
|||||||
mounted: function mounted() {
|
mounted: function mounted() {
|
||||||
this.getSettings();
|
this.getSettings();
|
||||||
this.clearEmptyGroups();
|
this.clearEmptyGroups();
|
||||||
|
|
||||||
// Intercept #publish (WP Update/Publish) button
|
// Intercept Classic Editor Publish/Save buttons
|
||||||
const vm = this;
|
const vm = this;
|
||||||
$(document).on('click', '#publish', function(e) {
|
vm.$nextTick(() => {
|
||||||
vm.validateAllFields();
|
$(document).off('click.deweboxPublish').on('click.deweboxPublish', '#publish, #save-post', function(e) {
|
||||||
const invalidField = vm.validateAllFields();
|
const invalidField = firstInvalidField(true);
|
||||||
if (invalidField) {
|
if (invalidField) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let firstInvalidFieldId = invalidField.id;
|
if (typeof window.Swal === 'function') {
|
||||||
let firstLabel = invalidField.label;
|
triggerSwalEmptyRequired(invalidField);
|
||||||
let tabTitle = invalidField.tab;
|
} else {
|
||||||
if (typeof window.Swal === 'function') {
|
alert('Please fill all required fields before saving.' + (invalidField.label ? '\nFirst missing: ' + invalidField.label : ''));
|
||||||
triggerSwalEmptyRequired(firstInvalidFieldId, firstLabel, tabTitle);
|
}
|
||||||
} else {
|
focusFirstInvalid(invalidField);
|
||||||
alert('Please fill all required fields before saving.' + (firstLabel ? '\nFirst missing: ' + firstLabel : ''));
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gutenberg: lock publish when invalid and show SweetAlert on attempt
|
||||||
|
if (window.wp && wp.data && wp.data.select('core/editor')) {
|
||||||
|
const { subscribe, dispatch } = wp.data;
|
||||||
|
const LOCK_KEY = 'wpcfto-required-lock';
|
||||||
|
|
||||||
|
const applyLock = () => {
|
||||||
|
const invalid = firstInvalidField(true);
|
||||||
|
if (invalid) {
|
||||||
|
dispatch('core/editor').lockPostSaving(LOCK_KEY);
|
||||||
|
} else {
|
||||||
|
dispatch('core/editor').unlockPostSaving(LOCK_KEY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
applyLock();
|
||||||
|
subscribe(applyLock);
|
||||||
|
|
||||||
|
$(document).off('click.deweboxGutenberg').on('click.deweboxGutenberg', '.editor-post-publish-button, .editor-post-publish-panel__toggle', function(e){
|
||||||
|
const invalid = firstInvalidField(true);
|
||||||
|
if (invalid) {
|
||||||
|
e.preventDefault();
|
||||||
|
triggerSwalEmptyRequired(invalid);
|
||||||
|
focusFirstInvalid(invalid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
validateAllFields: function validateAllFields() {
|
validateAllFields: function validateAllFields() {
|
||||||
let domInvalidFields = [];
|
const invalid = firstInvalidField(true);
|
||||||
$('.wpcfto-box-child').each(function () {
|
return invalid ? invalid : null;
|
||||||
const boxChild = $(this);
|
|
||||||
const fieldId = boxChild.attr('data-field');
|
|
||||||
const fieldTab = boxChild.closest('.wpcfto-tab').attr('id');
|
|
||||||
const fieldTabTitle = $(`[data-section=${fieldTab}]`).text();
|
|
||||||
const $inputs = boxChild.find('input, textarea, select');
|
|
||||||
|
|
||||||
if(boxChild.hasClass('repeater')){
|
|
||||||
const repeater_item = boxChild.find('.wpcfto-repeater-single');
|
|
||||||
repeater_item.each(function(key, field){
|
|
||||||
const _key = key+1;
|
|
||||||
const repeater_item_inputs = $(this).find('input, textarea, select');
|
|
||||||
repeater_item_inputs.each(function () {
|
|
||||||
const $field = $(this);
|
|
||||||
const name = $field.attr('name');
|
|
||||||
// if (!$field.is(':visible')) return;
|
|
||||||
if (!$field.is('[required]')) return;
|
|
||||||
|
|
||||||
let isEmpty = false;
|
|
||||||
if ($field.is(':checkbox') || $field.is(':radio')) {
|
|
||||||
if (!$(`[name="${name}"]`).val()) isEmpty = true;
|
|
||||||
} else {
|
|
||||||
if (!$.trim($field.val())) isEmpty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty) {
|
|
||||||
const fieldSubTabTitle = fieldTabTitle +'→ Item #'+_key ;
|
|
||||||
const fieldSubLabel = $field.closest('.wpcfto_generic_field').find('.wpcfto-field-aside__label span:first-child').text()
|
|
||||||
domInvalidFields.push({
|
|
||||||
id: fieldId,
|
|
||||||
tab: fieldSubTabTitle,
|
|
||||||
label: fieldSubLabel
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
$inputs.each(function () {
|
|
||||||
const $field = $(this);
|
|
||||||
const name = $field.attr('name');
|
|
||||||
|
|
||||||
if (!$field.is('[required]')) return;
|
|
||||||
|
|
||||||
let isEmpty = false;
|
|
||||||
if ($field.is(':checkbox') || $field.is(':radio')) {
|
|
||||||
if (!$(`[name="${name}"]`).val()) isEmpty = true;
|
|
||||||
} else {
|
|
||||||
if (!$.trim($field.val())) isEmpty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty) {
|
|
||||||
domInvalidFields.push({
|
|
||||||
id: fieldId,
|
|
||||||
tab: fieldTabTitle,
|
|
||||||
label: $(`[data-field=${fieldId}] label > span:first-child`).text()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
if (domInvalidFields.length > 0) {
|
|
||||||
console.log(domInvalidFields);
|
|
||||||
return domInvalidFields[0]; // Return the first invalid field object
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
getFieldLabelById(fieldId) {
|
getFieldLabelById(fieldId) {
|
||||||
let label = '';
|
let label = '';
|
||||||
@@ -256,17 +384,17 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveSettings: function saveSettings(id) {
|
saveSettings: function saveSettings(id) {
|
||||||
const invalidField = this.validateAllFields();
|
// Ensure editors sync back to DOM before validation/post
|
||||||
|
if (window.tinymce) tinymce.triggerSave();
|
||||||
|
|
||||||
|
const invalidField = firstInvalidField(true);
|
||||||
if (invalidField) {
|
if (invalidField) {
|
||||||
let firstInvalidFieldId = invalidField.id;
|
|
||||||
let firstLabel = invalidField.label;
|
|
||||||
var firstTab = firstInvalidFieldId.split('-');
|
|
||||||
var _firstTab = firstTab[0];
|
|
||||||
if (typeof window.Swal === 'function') {
|
if (typeof window.Swal === 'function') {
|
||||||
triggerSwalEmptyRequired(firstInvalidFieldId, firstLabel, _firstTab);
|
triggerSwalEmptyRequired(invalidField);
|
||||||
} else {
|
} else {
|
||||||
alert('Please fill all required fields before saving.' + (firstLabel ? '\nFirst missing: ' + firstLabel : ''));
|
alert('Please fill all required fields before saving.' + (invalidField.label ? '\nFirst missing: ' + invalidField.label : ''));
|
||||||
}
|
}
|
||||||
|
focusFirstInvalid(invalidField);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +474,8 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
addRepeaterRow() {
|
addRepeaterRow() {
|
||||||
if (!this.validateAllFields()) {
|
const invalid = firstInvalidField(true);
|
||||||
|
if (invalid) {
|
||||||
if (typeof window.Swal === 'function') {
|
if (typeof window.Swal === 'function') {
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
@@ -355,9 +484,10 @@
|
|||||||
} else {
|
} else {
|
||||||
alert('Please fill all required fields before adding a new row.');
|
alert('Please fill all required fields before adding a new row.');
|
||||||
}
|
}
|
||||||
|
focusFirstInvalid(invalid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Your existing logic to add repeater row
|
// TODO: your existing logic to actually add a repeater row goes here
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
106
vendor/wpcfto/metaboxes/assets/js/validationMixin.js
vendored
106
vendor/wpcfto/metaboxes/assets/js/validationMixin.js
vendored
@@ -1,54 +1,104 @@
|
|||||||
|
|
||||||
window.validationMixin = {
|
window.validationMixin = {
|
||||||
methods: {
|
methods: {
|
||||||
|
// --- Helpers
|
||||||
|
_depsArray() {
|
||||||
|
// Support both `dependencies: [...]` and legacy single `dependency: {...}`
|
||||||
|
if (this.fields && Array.isArray(this.fields.dependencies)) return this.fields.dependencies;
|
||||||
|
if (this.fields && this.fields.dependency && typeof this.fields.dependency === 'object') return [this.fields.dependency];
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
_isNotEmpty(val) {
|
||||||
|
if (val === undefined || val === null) return false;
|
||||||
|
if (typeof val === 'string') return val.trim() !== '';
|
||||||
|
if (Array.isArray(val)) return val.length > 0;
|
||||||
|
// numbers: 0 should be considered not-empty (valid) for required numeric fields
|
||||||
|
if (typeof val === 'number') return true;
|
||||||
|
if (typeof val === 'boolean') return val === true;
|
||||||
|
// objects: consider not empty if it has at least one key
|
||||||
|
if (typeof val === 'object') return Object.keys(val).length > 0;
|
||||||
|
return !!val;
|
||||||
|
},
|
||||||
|
_equals(a, b) {
|
||||||
|
// Loose compare for primitives, but normalize truthy/empty strings
|
||||||
|
if (Array.isArray(a) || Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
return String(a) == String(b);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Visibility according to dependencies
|
||||||
isVisible() {
|
isVisible() {
|
||||||
// If fields or dependencies do not exist, field is visible
|
const deps = this._depsArray();
|
||||||
if (!this.fields || !Array.isArray(this.fields.dependencies)) {
|
if (!deps) return true;
|
||||||
return true;
|
|
||||||
}
|
// Parent holds child refs by field key
|
||||||
// dependencies is an array, safe to use every()
|
return deps.every(dep => {
|
||||||
return this.fields.dependencies.every(dep => {
|
|
||||||
if (!dep || !dep.field) return true;
|
if (!dep || !dep.field) return true;
|
||||||
const depValue = this.$parent?.$refs[dep.field]?.value;
|
|
||||||
return depValue === dep.value;
|
// Prefer parent $refs value (set by Nuxy components)
|
||||||
|
const depValue = this.$parent?.$refs?.[dep.field]?.value;
|
||||||
|
|
||||||
|
// Operator
|
||||||
|
const op = dep.operator || dep.op || (dep.value === 'not_empty' ? 'not_empty' : 'equals');
|
||||||
|
|
||||||
|
if (op === 'not_empty') {
|
||||||
|
return this._isNotEmpty(depValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default equals
|
||||||
|
return this._equals(depValue, dep.value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
validateField() {
|
|
||||||
// Check if fields exists before accessing it
|
|
||||||
if (!this.fields) {
|
|
||||||
return true; // If fields is undefined, consider validation passed
|
|
||||||
}
|
|
||||||
|
|
||||||
const visible = this.isVisible();
|
|
||||||
const required = this.fields.required === true;
|
|
||||||
const value = this.field_value;
|
|
||||||
|
|
||||||
// Must be unique for each field instance!
|
// --- Required validation by type
|
||||||
|
validateField() {
|
||||||
|
if (!this.fields) return true;
|
||||||
|
|
||||||
|
const visible = this.isVisible();
|
||||||
|
const required = 'required' in this.fields && this.fields.required === true;
|
||||||
|
const type = this.fields.type || '';
|
||||||
|
const value = this.field_value;
|
||||||
|
|
||||||
|
let filled;
|
||||||
|
if (!required) {
|
||||||
|
filled = true;
|
||||||
|
} else if (!visible) {
|
||||||
|
// If not visible, treat as valid (global validator already skips hidden)
|
||||||
|
filled = true;
|
||||||
|
} else if (type === 'checkbox') {
|
||||||
|
filled = value === 1 || value === true || value === '1' || value === 'true';
|
||||||
|
} else if (type === 'repeater' && required) {
|
||||||
|
filled = this.fields.value && this.fields.value.length > 0;
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
filled = value.length > 0;
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
// 0 is a valid number for required numeric fields
|
||||||
|
filled = true;
|
||||||
|
} else {
|
||||||
|
filled = this._isNotEmpty(value);
|
||||||
|
}
|
||||||
|
|
||||||
const uniqueFieldId = this.field_id || (this.fields && this.fields.field_id);
|
const uniqueFieldId = this.field_id || (this.fields && this.fields.field_id);
|
||||||
|
|
||||||
const isValid = !required || (visible && value !== undefined && value !== null && value !== '');
|
// Emit once per change/mount with stable payload
|
||||||
|
|
||||||
// Only emit if we have a field_id
|
|
||||||
if (uniqueFieldId) {
|
if (uniqueFieldId) {
|
||||||
this.$root.$emit('field-validation', {
|
this.$root.$emit('field-validation', {
|
||||||
fieldId: uniqueFieldId,
|
fieldId: uniqueFieldId,
|
||||||
isValid
|
isValid: !!filled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return !!filled;
|
||||||
return isValid;
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
field_value() {
|
field_value() {
|
||||||
// Only call validateField if it exists
|
|
||||||
if (typeof this.validateField === 'function') {
|
if (typeof this.validateField === 'function') {
|
||||||
this.validateField();
|
this.validateField();
|
||||||
const isValid = this.validateField();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// Only call validateField if it exists
|
|
||||||
if (typeof this.validateField === 'function') {
|
if (typeof this.validateField === 'function') {
|
||||||
this.validateField();
|
this.validateField();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,12 @@
|
|||||||
// This is a string path like: data['variation']['fields']['my_field']
|
// This is a string path like: data['variation']['fields']['my_field']
|
||||||
$field_ref = "data['{$section_name}']['fields']['{$field_name}']";
|
$field_ref = "data['{$section_name}']['fields']['{$field_name}']";
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<wpcfto_codemirror
|
<wpcfto_codemirror
|
||||||
:fields='<?php echo esc_attr($field_ref); ?>'
|
:fields='<?php echo esc_attr($field_ref); ?>'
|
||||||
:field_label='<?php echo esc_attr($field_label); ?>'
|
:field_label='<?php echo esc_attr($field_label); ?>'
|
||||||
:field_name="'<?php echo esc_js($field_name); ?>'"
|
:field_name="'<?php echo esc_js($field_name); ?>'"
|
||||||
:field_id="'<?php echo esc_js($field_id); ?>'"
|
:field_id="'<?php echo esc_js($field_id); ?>'"
|
||||||
:field_value="'<?php echo esc_js($field_value); ?>'"
|
:field_value="'<?php echo esc_js($field_value); ?>'"
|
||||||
|
|
||||||
|
@wpcfto-get-value="$set(<?php echo $field_ref; ?>, 'value', $event)"
|
||||||
></wpcfto_codemirror>
|
></wpcfto_codemirror>
|
||||||
@@ -25,4 +25,4 @@ $field = "data['{$section_name}']['fields']['{$field_name}']";
|
|||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="<?php echo esc_attr($field_name); ?>"
|
name="<?php echo esc_attr($field_name); ?>"
|
||||||
v-bind:id="'<?php echo esc_attr($field_id); ?>'"
|
v-bind:id="'<?php echo esc_attr($field_id); ?>'"
|
||||||
v-model="JSON.stringify(<?php echo esc_attr(wp_unslash($field_value)); ?>)" />
|
:value="JSON.stringify(<?php echo esc_attr(wp_unslash($field_value)); ?>)" />
|
||||||
25
vendor/wpcfto/metaboxes/fields/multiselect.php
vendored
25
vendor/wpcfto/metaboxes/fields/multiselect.php
vendored
@@ -13,18 +13,13 @@
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<wpcfto_multiselect v-bind:fields="<?php echo esc_attr($field); ?>"
|
<wpcfto_multiselect
|
||||||
v-bind:field_label="<?php echo esc_attr($field_label); ?>"
|
v-bind:fields='<?php echo wp_json_encode($field); ?>'
|
||||||
v-bind:field_name="'<?php echo esc_attr($field_name); ?>'"
|
v-bind:field_label='<?php echo wp_json_encode($field_label); ?>'
|
||||||
v-bind:field_id="'<?php echo esc_attr($field_id); ?>'"
|
v-bind:field_name='<?php echo wp_json_encode($field_name); ?>'
|
||||||
v-bind:field_value="<?php echo esc_attr($field_value); ?>"
|
v-bind:field_id='<?php echo wp_json_encode($field_id); ?>'
|
||||||
v-bind:field_options="<?php echo esc_attr($field); ?>['options']"
|
v-bind:field_value='<?php echo wp_json_encode($field_value); ?>'
|
||||||
v-bind:field_data='<?php echo str_replace("'", "", json_encode($field_data)); ?>'
|
v-bind:field_options='<?php echo wp_json_encode(isset($field['options']) ? $field['options'] : []); ?>'
|
||||||
@wpcfto-get-value="$set(<?php echo esc_attr($field) ?>, 'value', $event)">
|
v-bind:field_data='<?php echo wp_json_encode($field_data); ?>'
|
||||||
</wpcfto_multiselect>
|
@wpcfto-get-value="$set(<?php echo json_encode($field_name); ?>, 'value', $event)">
|
||||||
|
</wpcfto_multiselect>
|
||||||
<input type="hidden"
|
|
||||||
:style="{'width' : '100%'}"
|
|
||||||
name="<?php echo esc_attr($field_name); ?>"
|
|
||||||
v-bind:id="'<?php echo esc_attr($field_id); ?>'"
|
|
||||||
v-model="JSON.stringify(<?php echo esc_attr(wp_unslash($field_value)); ?>)"/>
|
|
||||||
25
vendor/wpcfto/metaboxes/fields/repeater.php
vendored
25
vendor/wpcfto/metaboxes/fields/repeater.php
vendored
@@ -15,18 +15,13 @@ wp_enqueue_script('my-super-component', STM_WPCFTO_URL . '/metaboxes/general_com
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<wpcfto_repeater v-bind:fields="<?php echo esc_attr($field); ?>"
|
<wpcfto_repeater
|
||||||
v-bind:parent_repeater="'parent'"
|
v-bind:fields="<?php echo esc_attr($field); ?>"
|
||||||
v-bind:field_label="<?php echo esc_attr($field_label); ?>"
|
v-bind:parent_repeater="'parent'"
|
||||||
v-bind:field_name="'<?php echo esc_attr($field_name); ?>'"
|
v-bind:field_label="<?php echo esc_attr($field_label); ?>"
|
||||||
v-bind:field_id="'<?php echo esc_attr($field_id); ?>'"
|
v-bind:field_name="'<?php echo esc_attr($field_name); ?>'"
|
||||||
v-bind:field_value="<?php echo esc_attr($field_value); ?>"
|
v-bind:field_id="'<?php echo esc_attr($field_id); ?>'"
|
||||||
v-bind:field_data='<?php echo str_replace("'", "", json_encode($field_data)); ?>'
|
v-bind:field_value="<?php echo esc_attr($field_value); ?>"
|
||||||
@wpcfto-get-value="$set(<?php echo esc_attr($field) ?>, 'value', $event)">
|
v-bind:field_data='<?php echo str_replace("'", "", json_encode($field_data)); ?>'
|
||||||
</wpcfto_repeater>
|
@wpcfto-get-value="$set(<?php echo esc_attr($field) ?>, 'value', $event)"
|
||||||
|
></wpcfto_repeater>
|
||||||
<input type="hidden"
|
|
||||||
:style="{'width' : '100%'}"
|
|
||||||
name="<?php echo esc_attr($field_name); ?>"
|
|
||||||
v-bind:id="'<?php echo esc_attr($field_id); ?>'"
|
|
||||||
v-model="JSON.stringify(<?php echo esc_attr(wp_unslash($field_value)); ?>)" />
|
|
||||||
@@ -24,8 +24,19 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Editor -->
|
<!-- Editor (textarea transformed by CodeMirror) -->
|
||||||
<textarea :id="field_id" :name="field_name"></textarea>
|
<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>
|
</div>
|
||||||
|
|
||||||
<wpcfto_fields_aside_after :fields="fields"></wpcfto_fields_aside_after>
|
<wpcfto_fields_aside_after :fields="fields"></wpcfto_fields_aside_after>
|
||||||
@@ -45,27 +56,62 @@
|
|||||||
'css': 'CSS',
|
'css': 'CSS',
|
||||||
'htmlmixed': 'HTML',
|
'htmlmixed': 'HTML',
|
||||||
'text/plain': 'Plain Text'
|
'text/plain': 'Plain Text'
|
||||||
}
|
},
|
||||||
|
_origConsoleError: null,
|
||||||
|
_origConsoleWarn: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
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(() => {
|
this.$nextTick(() => {
|
||||||
const selector = '#' + this.field_id;
|
const selector = '#' + this.field_id;
|
||||||
this.waitForElementVisible(selector, () => {
|
this.waitForElementVisible(selector, () => {
|
||||||
const fieldObj = window.eval(this.fields);
|
const fieldObj = (this.fields && typeof this.fields === 'object') ? this.fields : {};
|
||||||
let rawValue = fieldObj.value || '';
|
let rawValue = '';
|
||||||
this.value = this.formatCode(rawValue, fieldObj.mode);
|
if (typeof this.field_value === 'string' && !this.isPathString(this.field_value)) {
|
||||||
|
rawValue = this.field_value;
|
||||||
this.initCodeMirror(fieldObj);
|
} 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.refreshOnTabActivation();
|
||||||
this.addMutationObserver();
|
this.addMutationObserver();
|
||||||
this.initResizeObserver(); // 👈 Add this
|
this.initResizeObserver();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isPathString(str) {
|
||||||
|
return typeof str === 'string' && /data\s*\[\s*['\"]/i.test(str);
|
||||||
|
},
|
||||||
initResizeObserver() {
|
initResizeObserver() {
|
||||||
const container = document.getElementById(this.field_id)?.closest('.wpcfto-field-content');
|
const container = document.getElementById(this.field_id)?.closest('.wpcfto-field-content');
|
||||||
if (!container || typeof ResizeObserver === 'undefined') return;
|
if (!container || typeof ResizeObserver === 'undefined') return;
|
||||||
@@ -224,18 +270,63 @@
|
|||||||
|
|
||||||
if (this.editor) return;
|
if (this.editor) return;
|
||||||
|
|
||||||
this.editor = CodeMirror.fromTextArea(textarea, {
|
// During CM construction, force non‑passive listeners on the editor DOM only
|
||||||
mode: fieldObj?.mode || 'htmlmixed',
|
const typesToFix = { touchstart:1, touchmove:1, wheel:1, mousewheel:1 };
|
||||||
theme: fieldObj?.theme || 'default',
|
const origAdd = EventTarget.prototype.addEventListener;
|
||||||
lineNumbers: true,
|
EventTarget.prototype.addEventListener = function(type, listener, options){
|
||||||
lineWrapping: true,
|
try {
|
||||||
tabSize: 2,
|
if ((type in typesToFix) && this && (
|
||||||
autoCloseBrackets: true,
|
(this.classList && this.classList.contains('CodeMirror')) ||
|
||||||
matchBrackets: true
|
(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);
|
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
|
// Force multiple refreshes to ensure layout renders properly
|
||||||
setTimeout(() => this.editor.refresh(), 50);
|
setTimeout(() => this.editor.refresh(), 50);
|
||||||
setTimeout(() => this.editor.refresh(), 300);
|
setTimeout(() => this.editor.refresh(), 300);
|
||||||
@@ -246,34 +337,28 @@
|
|||||||
const newValue = cm.getValue();
|
const newValue = cm.getValue();
|
||||||
this.value = newValue;
|
this.value = newValue;
|
||||||
|
|
||||||
try {
|
const mode = (fieldObj && fieldObj.mode) ? String(fieldObj.mode).toLowerCase() : 'htmlmixed';
|
||||||
const mode = fieldObj?.mode || 'htmlmixed';
|
let output = newValue;
|
||||||
const normalizedMode = (mode || '').toLowerCase();
|
if (mode.indexOf('json') !== -1) {
|
||||||
|
try { output = JSON.stringify(JSON.parse(newValue)); } catch(_) { output = newValue; }
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
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) {
|
if (this.editor) {
|
||||||
this.editor.off('change');
|
this.editor.off('change');
|
||||||
this.editor.toTextArea();
|
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 ",
|
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() {
|
mounted: function mounted() {
|
||||||
this.checkboxes = typeof this.field_value === 'string' && WpcftoIsJsonString(this.field_value) ? JSON.parse(this.field_value) : this.field_value;
|
// Normalize initial value from parent (can be JSON string, array, or empty)
|
||||||
|
if (typeof this.field_value === 'string' && WpcftoIsJsonString(this.field_value)) {
|
||||||
if (this.checkboxes.length === 0) {
|
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 = [];
|
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: {},
|
methods: {},
|
||||||
watch: {
|
watch: {
|
||||||
checkboxes: {
|
checkboxes: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler: function handler(checkboxes) {
|
handler: function handler(checkboxes) {
|
||||||
|
// Ensure array
|
||||||
|
if (!Array.isArray(checkboxes)) {
|
||||||
|
checkboxes = (checkboxes == null) ? [] : [checkboxes];
|
||||||
|
}
|
||||||
this.$emit('wpcfto-get-value', 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){
|
(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";
|
"use strict";
|
||||||
|
|
||||||
const Repeater = Swal.mixin({
|
const Repeater = (window.Swal && typeof window.Swal.mixin === 'function')
|
||||||
customClass: {
|
? window.Swal.mixin({
|
||||||
|
customClass: {
|
||||||
container: 'wpcfto-settings',
|
container: 'wpcfto-settings',
|
||||||
confirmButton: 'button'
|
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', {
|
Vue.component('wpcfto_repeater', {
|
||||||
|
mixins: (window.validationMixin ? [window.validationMixin] : []),
|
||||||
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
||||||
data: function data() {
|
data: function data() {
|
||||||
return {
|
return {
|
||||||
@@ -18,8 +26,23 @@ Vue.component('wpcfto_repeater', {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="wpcfto_generic_field wpcfto_generic_field_repeater wpcfto-repeater unflex_fields">
|
<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">
|
<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 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="wpcfto_group_title" @click="toggleArea(area)" v-html="getGroupTitle(area, area_key)"></div>
|
||||||
<div class="repeater_inner" :class="{ closed: area.closed_tab }">
|
<div class="repeater_inner" :class="{ closed: area.closed_tab }">
|
||||||
@@ -71,10 +94,101 @@ Vue.component('wpcfto_repeater', {
|
|||||||
item.closed_tab = true;
|
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: {
|
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
|
// This method validates all visible required fields in all rows before adding a new row
|
||||||
validateAndAddArea: function() {
|
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 = [];
|
var invalidFields = [];
|
||||||
// Loop through all repeater rows and all fields
|
// Loop through all repeater rows and all fields
|
||||||
this.repeater.forEach((area, area_key) => {
|
this.repeater.forEach((area, area_key) => {
|
||||||
@@ -91,6 +205,8 @@ Vue.component('wpcfto_repeater', {
|
|||||||
if (childComponent.isVisible && childComponent.fields && childComponent.fields.required) {
|
if (childComponent.isVisible && childComponent.fields && childComponent.fields.required) {
|
||||||
if (!childComponent.validateField()) {
|
if (!childComponent.validateField()) {
|
||||||
invalidFields.push({
|
invalidFields.push({
|
||||||
|
repeater_label: this._props.field_label,
|
||||||
|
group_title: group_title,
|
||||||
label: childComponent.fields.label || field_name_inner,
|
label: childComponent.fields.label || field_name_inner,
|
||||||
area: area_key + 1
|
area: area_key + 1
|
||||||
});
|
});
|
||||||
@@ -102,20 +218,25 @@ Vue.component('wpcfto_repeater', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (invalidFields.length > 0) {
|
if (invalidFields.length > 0) {
|
||||||
// Compose error message
|
// Compose error message
|
||||||
var msg = 'Please fill all required fields before adding a new item:\n\n';
|
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');
|
const firstInvalidItem = invalidFields[0];
|
||||||
// Use SweetAlert2 if available, otherwise alert()
|
msg += 'Check item #'+firstInvalidItem.area+': <b>'+this.formatLabelPath([firstInvalidItem.group_title, firstInvalidItem.label])+'</b>';
|
||||||
if (typeof window.Swal === 'function') {
|
// Use SweetAlert2 if available, otherwise alert()
|
||||||
Repeater.fire({
|
if (typeof window.Swal === 'function') {
|
||||||
icon: 'warning',
|
Repeater.fire({
|
||||||
title: 'Required fields missing',
|
icon: 'warning',
|
||||||
html: msg.replace(/\n/g, '<br>')
|
title: 'Required fields missing',
|
||||||
});
|
html: msg.replace(/\n/g, '<br>'),
|
||||||
} else {
|
width: 'fit-content',
|
||||||
alert(msg);
|
customClass: {
|
||||||
}
|
confirmButton: 'btn text-bg-primary'
|
||||||
return;
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(msg);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// If all required fields are valid, add a new row
|
// If all required fields are valid, add a new row
|
||||||
this.addArea();
|
this.addArea();
|
||||||
@@ -134,6 +255,9 @@ Vue.component('wpcfto_repeater', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.validateField();
|
this.validateField();
|
||||||
|
if (typeof jQuery !== 'undefined') {
|
||||||
|
jQuery(document).trigger('repeater-item-added', [this._props]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
toggleArea: function toggleArea(area) {
|
toggleArea: function toggleArea(area) {
|
||||||
// Close all other rows
|
// Close all other rows
|
||||||
@@ -159,6 +283,10 @@ Vue.component('wpcfto_repeater', {
|
|||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
this.repeater.splice(areaIndex, 1);
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
if (!this.validateAllRows()) {
|
// Block if repeater required but empty OR if any child invalid
|
||||||
return; // Prevent save if validation fails
|
const selfValid = this.validateField();
|
||||||
}
|
// const rowsValid = this.validateAllRows();
|
||||||
|
if (!selfValid) { return; }
|
||||||
// Proceed with save logic here...
|
// Proceed with save logic here...
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -253,7 +382,8 @@ Vue.component('wpcfto_repeater', {
|
|||||||
repeater: {
|
repeater: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler: function handler(repeater) {
|
handler: function handler(repeater) {
|
||||||
this.$emit('wpcfto-get-value', repeater);
|
this.$emit('wpcfto-get-value', repeater);
|
||||||
|
this.validateField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,148 +2,207 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
Vue.component('wpcfto_select', {
|
Vue.component('wpcfto_select', {
|
||||||
mixins: [window.validationMixin],
|
mixins: [window.validationMixin],
|
||||||
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
props: ['fields', 'field_label', 'field_name', 'field_id', 'field_value'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: '',
|
value: '',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
dropdownOpen: false,
|
dropdownOpen: false,
|
||||||
$refs: {
|
// local reactive copy so we don't mutate props
|
||||||
nativeSelect: null,
|
localOptions: {}, // {value: label}
|
||||||
searchBox: null
|
localSearchable: false
|
||||||
}
|
};
|
||||||
};
|
},
|
||||||
|
computed: {
|
||||||
|
optionCount() {
|
||||||
|
return Object.keys(this.localOptions || {}).length;
|
||||||
},
|
},
|
||||||
computed: {
|
filteredOptions() {
|
||||||
filteredOptions() {
|
const options = this.localOptions || {};
|
||||||
const options = this.fields.options || {};
|
const selectedValue = this.value;
|
||||||
const selectedValue = this.value;
|
|
||||||
|
// Not searchable → show all
|
||||||
// If not searchable, show all options
|
if (!this.localSearchable) return options;
|
||||||
if (!this.fields.searchable) {
|
|
||||||
return options;
|
const count = Object.keys(options).length;
|
||||||
}
|
const hasSearch = this.searchTerm && this.searchTerm.length >= 3;
|
||||||
|
|
||||||
// If searchTerm is empty or less than 3 chars
|
// If small list (≤10): show all when no search term, else filter
|
||||||
if (!this.searchTerm || this.searchTerm.length < 3) {
|
if (count <= 10) {
|
||||||
// Show only selected option if exists
|
if (!this.searchTerm) return options;
|
||||||
if (selectedValue && options[selectedValue]) {
|
const term = this.searchTerm.toLowerCase();
|
||||||
return { [selectedValue]: options[selectedValue] };
|
return Object.fromEntries(
|
||||||
}
|
Object.entries(options).filter(([key, label]) =>
|
||||||
// Otherwise show nothing
|
String(label).toLowerCase().includes(term) ||
|
||||||
return {};
|
String(key).toLowerCase().includes(term)
|
||||||
}
|
)
|
||||||
|
);
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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: `
|
selectedLabel() {
|
||||||
<div class="wpcfto_generic_field wpcfto_generic_field__select" :class="{ open: dropdownOpen }">
|
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">
|
document.addEventListener('click', this.handleClickOutside);
|
||||||
<!-- Render custom searchable select only if searchable -->
|
},
|
||||||
<div v-if="fields.searchable" class="wpcfto-custom-select" :id="field_id" @click.stop="toggleDropdown">
|
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">
|
// replace options reactively
|
||||||
{{ selectedLabel }}
|
this.localOptions = Object.assign({}, optionsToSet);
|
||||||
<span class="wpcfto-arrow" :class="{ open: dropdownOpen }">▾</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="dropdownOpen" class="wpcfto-options-dropdown">
|
// decide the selected value
|
||||||
<input ref="searchBox" type="text" class="wpcfto-select-search" v-model="searchTerm" placeholder="Search..." @click.stop />
|
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;">
|
// notify & sync hidden input
|
||||||
<li v-for="(label, key) in filteredOptions" :key="key"
|
this.$nextTick(() => {
|
||||||
:class="{ selected: key === value }"
|
this.emitAndSync();
|
||||||
@click.stop="selectOption(key)">
|
});
|
||||||
{{ label }}
|
},
|
||||||
</li>
|
|
||||||
<li v-if="Object.keys(filteredOptions).length === 0" class="no-options">No options found</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</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 -->
|
selectOption(key) {
|
||||||
<div v-else class="wpcfto-admin-select">
|
this.value = key;
|
||||||
<select
|
this.emitAndSync();
|
||||||
v-bind:name="field_name"
|
this.dropdownOpen = false;
|
||||||
v-model="value"
|
},
|
||||||
v-bind:id="field_id"
|
|
||||||
:required="fields.required === true">
|
emitAndSync() {
|
||||||
<option v-for="(option, key) in fields.options" :value="key" :key="key">
|
this.$emit('wpcfto-get-value', this.value);
|
||||||
{{ option }}
|
const native = this.$refs.nativeSelect;
|
||||||
</option>
|
if (native) {
|
||||||
</select>
|
native.value = this.value;
|
||||||
</div>
|
// 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>
|
</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 -->
|
<!-- Native select -->
|
||||||
<input ref="nativeSelect" v-if="fields.searchable" type="hidden" :name="field_name" v-model="value" :required="fields.required === true" />
|
<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>
|
</div>
|
||||||
`,
|
|
||||||
watch: {
|
<wpcfto_fields_aside_after :fields="fields"></wpcfto_fields_aside_after>
|
||||||
value(newVal) {
|
|
||||||
this.$emit('wpcfto-get-value', newVal);
|
<!-- Hidden input to submit form value for custom select -->
|
||||||
this.$nextTick(() => {
|
<input ref="nativeSelect" v-if="localSearchable" type="hidden" :name="field_name" v-model="value" :required="fields.required === true" />
|
||||||
if (typeof window.wpcftoDependency !== 'undefined') {
|
</div>
|
||||||
window.wpcftoDependency.check(this.field_id);
|
`
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZha2VfYzRiZDk3ZTMuanMiXSwibmFtZXMiOlsiVnVlIiwiY29tcG9uZW50IiwicHJvcHMiLCJkYXRhIiwidmFsdWUiLCJ0ZW1wbGF0ZSIsIm1vdW50ZWQiLCJmaWVsZF92YWx1ZSIsIm1ldGhvZHMiLCJ3YXRjaCIsIl92YWx1ZSIsIiRlbWl0Il0sIm1hcHBpbmdzIjoiQUFBQTs7QUFFQUEsR0FBRyxDQUFDQyxTQUFKLENBQWMsZUFBZCxFQUErQjtBQUM3QkMsRUFBQUEsS0FBSyxFQUFFLENBQUMsUUFBRCxFQUFXLGFBQVgsRUFBMEIsWUFBMUIsRUFBd0MsVUFBeEMsRUFBb0QsYUFBcEQsQ0FEc0I7QUFFN0JDLEVBQUFBLElBQUksRUFBRSxTQUFTQSxJQUFULEdBQWdCO0FBQ3BCLFdBQU87QUFDTEMsTUFBQUEsS0FBSyxFQUFFO0FBREYsS0FBUDtBQUdELEdBTjRCO0FBTzdCQyxFQUFBQSxRQUFRLEVBQUUsNndCQVBtQjtBQVE3QkMsRUFBQUEsT0FBTyxFQUFFLFNBQVNBLE9BQVQsR0FBbUI7QUFDMUIsU0FBS0YsS0FBTCxHQUFhLEtBQUtHLFdBQWxCO0FBQ0QsR0FWNEI7QUFXN0JDLEVBQUFBLE9BQU8sRUFBRSxFQVhvQjtBQVk3QkMsRUFBQUEsS0FBSyxFQUFFO0FBQ0xMLElBQUFBLEtBQUssRUFBRSxTQUFTQSxLQUFULENBQWVNLE1BQWYsRUFBdUI7QUFDNUIsV0FBS0MsS0FBTCxDQUFXLGtCQUFYLEVBQStCRCxNQUEvQjtBQUNEO0FBSEk7QUFac0IsQ0FBL0IiLCJzb3VyY2VzQ29udGVudCI6WyJcInVzZSBzdHJpY3RcIjtcblxuVnVlLmNvbXBvbmVudCgnd3BjZnRvX3NlbGVjdCcsIHtcbiAgcHJvcHM6IFsnZmllbGRzJywgJ2ZpZWxkX2xhYmVsJywgJ2ZpZWxkX25hbWUnLCAnZmllbGRfaWQnLCAnZmllbGRfdmFsdWUnXSxcbiAgZGF0YTogZnVuY3Rpb24gZGF0YSgpIHtcbiAgICByZXR1cm4ge1xuICAgICAgdmFsdWU6ICcnXG4gICAgfTtcbiAgfSxcbiAgdGVtcGxhdGU6IFwiXFxuICAgICAgICA8ZGl2IGNsYXNzPVxcXCJ3cGNmdG9fZ2VuZXJpY19maWVsZCB3cGNmdG9fZ2VuZXJpY19maWVsZF9fc2VsZWN0XFxcIj5cXG5cXG4gICAgICAgICAgICA8d3BjZnRvX2ZpZWxkc19hc2lkZV9iZWZvcmUgOmZpZWxkcz1cXFwiZmllbGRzXFxcIiA6ZmllbGRfbGFiZWw9XFxcImZpZWxkX2xhYmVsXFxcIj48L3dwY2Z0b19maWVsZHNfYXNpZGVfYmVmb3JlPlxcbiAgICAgICAgICAgIFxcbiAgICAgICAgICAgIDxkaXYgY2xhc3M9XFxcIndwY2Z0by1maWVsZC1jb250ZW50XFxcIj5cXG4gICAgICAgICAgICAgICAgPGRpdiBjbGFzcz1cXFwid3BjZnRvLWFkbWluLXNlbGVjdFxcXCI+XFxuICAgICAgICAgICAgICAgICAgICA8c2VsZWN0IHYtYmluZDpuYW1lPVxcXCJmaWVsZF9uYW1lXFxcIlxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2LW1vZGVsPVxcXCJ2YWx1ZVxcXCJcXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdi1iaW5kOmlkPVxcXCJmaWVsZF9pZFxcXCI+XFxuICAgICAgICAgICAgICAgICAgICAgICAgPG9wdGlvbiB2LWZvcj1cXFwiKG9wdGlvbiwga2V5KSBpbiBmaWVsZHNbJ29wdGlvbnMnXVxcXCIgdi1iaW5kOnZhbHVlPVxcXCJrZXlcXFwiPnt7IG9wdGlvbiB9fTwvb3B0aW9uPlxcbiAgICAgICAgICAgICAgICAgICAgPC9zZWxlY3Q+XFxuICAgICAgICAgICAgICAgIDwvZGl2PlxcbiAgICAgICAgICAgIDwvZGl2PlxcblxcbiAgICAgICAgICAgIDx3cGNmdG9fZmllbGRzX2FzaWRlX2FmdGVyIDpmaWVsZHM9XFxcImZpZWxkc1xcXCI+PC93cGNmdG9fZmllbGRzX2FzaWRlX2FmdGVyPlxcblxcbiAgICAgICAgPC9kaXY+XFxuICAgIFwiLFxuICBtb3VudGVkOiBmdW5jdGlvbiBtb3VudGVkKCkge1xuICAgIHRoaXMudmFsdWUgPSB0aGlzLmZpZWxkX3ZhbHVlO1xuICB9LFxuICBtZXRob2RzOiB7fSxcbiAgd2F0Y2g6IHtcbiAgICB2YWx1ZTogZnVuY3Rpb24gdmFsdWUoX3ZhbHVlKSB7XG4gICAgICB0aGlzLiRlbWl0KCd3cGNmdG8tZ2V0LXZhbHVlJywgX3ZhbHVlKTtcbiAgICB9XG4gIH1cbn0pOyJdfQ==
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZha2VfYzRiZDk3ZTMuanMiXSwibmFtZXMiOlsiVnVlIiwiY29tcG9uZW50IiwicHJvcHMiLCJkYXRhIiwidmFsdWUiLCJ0ZW1wbGF0ZSIsIm1vdW50ZWQiLCJmaWVsZF92YWx1ZSIsIm1ldGhvZHMiLCJ3YXRjaCIsIl92YWx1ZSIsIiRlbWl0Il0sIm1hcHBpbmdzIjoiQUFBQTs7QUFFQUEsR0FBRyxDQUFDQyxTQUFKLENBQWMsZUFBZCxFQUErQjtBQUM3QkMsRUFBQUEsS0FBSyxFQUFFLENBQUMsUUFBRCxFQUFXLGFBQVgsRUFBMEIsWUFBMUIsRUFBd0MsVUFBeEMsRUFBb0QsYUFBcEQsQ0FEc0I7QUFFN0JDLEVBQUFBLElBQUksRUFBRSxTQUFTQSxJQUFULEdBQWdCO0FBQ3BCLFdBQU87QUFDTEMsTUFBQUEsS0FBSyxFQUFFO0FBREYsS0FBUDtBQUdELEdBTjRCO0FBTzdCQyxFQUFBQSxRQUFRLEVBQUUsNndCQVBtQjtBQVE3QkMsRUFBQUEsT0FBTyxFQUFFLFNBQVNBLE9BQVQsR0FBbUI7QUFDMUIsU0FBS0YsS0FBTCxHQUFhLEtBQUtHLFdBQWxCO0FBQ0QsR0FWNEI7QUFXN0JDLEVBQUFBLE9BQU8sRUFBRSxFQVhvQjtBQVk3QkMsRUFBQUEsS0FBSyxFQUFFO0FBQ0xMLElBQUFBLEtBQUssRUFBRSxTQUFTQSxLQUFULENBQWVNLE1BQWYsRUFBdUI7QUFDNUIsV0FBS0MsS0FBTCxDQUFXLGtCQUFYLEVBQStCRCxNQUEvQjtBQUNEO0FBSEk7QUFac0IsQ0FBL0IiLCJzb3VyY2VzQ29udGVudCI6WyJcInVzZSBzdHJpY3RcIjtcblxuVnVlLmNvbXBvbmVudCgnd3BjZnRvX3NlbGVjdCcsIHtcbiAgcHJvcHM6IFsnZmllbGRzJywgJ2ZpZWxkX2xhYmVsJywgJ2ZpZWxkX25hbWUnLCAnZmllbGRfaWQnLCAnZmllbGRfdmFsdWUnXSxcbiAgZGF0YTogZnVuY3Rpb24gZGF0YSgpIHtcbiAgICByZXR1cm4ge1xuICAgICAgdmFsdWU6ICcnXG4gICAgfTtcbiAgfSxcbiAgdGVtcGxhdGU6IFwiXFxuICAgICAgICA8ZGl2IGNsYXNzPVxcXCJ3cGNmdG9fZ2VuZXJpY19maWVsZCB3cGNmdG9fZ2VuZXJpY19maWVsZF9fc2VsZWN0XFxcIj5cXG5cXG4gICAgICAgICAgICA8d3BjZnRvX2ZpZWxkc19hc2lkZV9iZWZvcmUgOmZpZWxkcz1cXFwiZmllbGRzXFxcIiA6ZmllbGRfbGFiZWw9XFxcImZpZWxkX2xhYmVsXFxcIj48L3dwY2Z0b19maWVsZHNfYXNpZGVfYmVmb3JlPlxcbiAgICAgICAgICAgIFxcbiAgICAgICAgICAgIDxkaXYgY2xhc3M9XFxcIndwY2Z0by1maWVsZC1jb250ZW50XFxcIj5cXG4gICAgICAgICAgICAgICAgPGRpdiBjbGFzcz1cXFwid3BjZnRvLWFkbWluLXNlbGVjdFxcXCI+XFxuICAgICAgICAgICAgICAgICAgICA8c2VsZWN0IHYtYmluZDpuYW1lPVxcXCJmaWVsZF9uYW1lXFxcIlxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB2LW1vZGVsPVxcXCJ2YWx1ZVxcXCJcXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgdi1iaW5kOmlkPVxcXCJmaWVsZF9pZFxcXCI+XFxuICAgICAgICAgICAgICAgICAgICAgICAgPG9wdGlvbiB2LWZvcj1cXFwiKG9wdGlvbiwga2V5KSBpbiBmaWVsZHNbJ29wdGlvbnMnXVxcXCIgdi1iaW5kOnZhbHVlPVxcXCJrZXlcXFwiPnt7IG9wdGlvbiB9fTwvb3B0aW9uPlxcbiAgICAgICAgICAgICAgICAgICAgPC9zZWxlY3Q+XFxuICAgICAgICAgICAgICAgIDwvZGl2PlxcbiAgICAgICAgICAgIDwvZGl2PlxcblxcbiAgICAgICAgICAgIDx3cGNmdG9fZmllbGRzX2FzaWRlX2FmdGVyIDpmaWVsZHM9XFxcImZpZWxkc1xcXCI+PC93cGNmdG9fZmllbGRzX2FzaWRlX2FmdGVyPlxcblxcbiAgICAgICAgPC9kaXY+XFxuICAgIFwiLFxuICBtb3VudGVkOiBmdW5jdGlvbiBtb3VudGVkKCkge1xuICAgIHRoaXMudmFsdWUgPSB0aGlzLmZpZWxkX3ZhbHVlO1xuICB9LFxuICBtZXRob2RzOiB7fSxcbiAgd2F0Y2g6IHtcbiAgICB2YWx1ZTogZnVuY3Rpb24gdmFsdWUoX3ZhbHVlKSB7XG4gICAgICB0aGlzLiRlbWl0KCd3cGNmdG8tZ2V0LXZhbHVlJywgX3ZhbHVlKTtcbiAgICB9XG4gIH1cbn0pOyJdfQ==
|
||||||
},{}]},{},[1])
|
},{}]},{},[1])
|
||||||
8
vendor/wpcfto/metaboxes/metabox.php
vendored
8
vendor/wpcfto/metaboxes/metabox.php
vendored
@@ -162,11 +162,13 @@ class STM_Metaboxes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function wpcfto_save_number( $value ) {
|
public function wpcfto_save_number( $value ) {
|
||||||
$value = floatval( $value );
|
// Preserve zero as a valid value; do not coerce to empty string.
|
||||||
if ( 0 === $value ) {
|
// This aligns server-side with client-side required validation.
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
// Allow string "0", numeric 0, or any float.
|
||||||
|
$value = is_numeric($value) ? $value + 0 : $value;
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user