| @@ -12,9 +12,9 @@ | ||
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | 14 | * Helper class to render specific param types. |
| 15 | 15 | * |
| 16 | - * @since 1.16.9 (J) - 1.6.9 (WP) | |
| 16 | + * @since 1.16.9 (J) - 1.6.9 (WP) | |
| 17 | 17 | */ |
| 18 | 18 | final class VBOParamsRendering |
| 19 | 19 | { |
| 20 | 20 | /** |
| @@ -104,10 +104,15 @@ | ||
| 104 | 104 | if (!$this->params) { |
| 105 | 105 | return ''; |
| 106 | 106 | } |
| 107 | 107 | |
| 108 | + // build the HTML params string | |
| 108 | 109 | $html = ''; |
| 109 | 110 | |
| 111 | + // list of conditional field rules | |
| 112 | + $js_conditional_fields = []; | |
| 113 | + | |
| 114 | + // scan all field params | |
| 110 | 115 | foreach ($this->params as $param_name => $param_config) { |
| 111 | 116 | if (empty($param_name)) { |
| 112 | 117 | continue; |
| 113 | 118 | } |
| @@ -119,23 +124,67 @@ | ||
| 119 | 124 | $labelhelp = $param_config['help']; |
| 120 | 125 | } |
| 121 | 126 | |
| 122 | 127 | $nested_style = (isset($param_config['nested']) && $param_config['nested']); |
| 123 | - $hidden_wrapper = false; | |
| 124 | - if (isset($param_config['conditional']) && isset($this->params[$param_config['conditional']])) { | |
| 125 | - $check_cond = isset($this->params[$param_config['conditional']]['default']) ? $this->params[$param_config['conditional']]['default'] : null; | |
| 126 | - $check_cond = isset($this->settings[$param_config['conditional']]) ? $this->settings[$param_config['conditional']] : $check_cond; | |
| 127 | - if (!is_null($check_cond) && (!$check_cond || !strcasecmp($check_cond, 'off'))) { | |
| 128 | - // hide current field because the field to who this is dependant is "off" or disabled (i.e. 0) | |
| 129 | - $hidden_wrapper = true; | |
| 128 | + $hidden_wrapper = $param_config['type'] === 'hidden'; | |
| 129 | + if (!empty($param_config['conditional'])) { | |
| 130 | + /** | |
| 131 | + * The conditional property can specify the value to check from the parent field | |
| 132 | + * i.e. "test_mode:1" means that the value should be equal to 1. Alternative, | |
| 133 | + * "test_mode!:1" means that the value should be different than 1. | |
| 134 | + * | |
| 135 | + * @since 1.18.2 (J) - 1.8.2 (WP) | |
| 136 | + */ | |
| 137 | + $check_cond_field = $param_config['conditional']; | |
| 138 | + $check_cond_oper = null; | |
| 139 | + $check_cond_value = null; | |
| 140 | + if (preg_match('/^([a-z0-9_\-]+)(!?:)(.*?)$/i', (string) $check_cond_field, $cond_matches)) { | |
| 141 | + // conditional instruction detected | |
| 142 | + $check_cond_field = $cond_matches[1]; | |
| 143 | + $check_cond_oper = $cond_matches[2]; | |
| 144 | + $check_cond_value = $cond_matches[3]; | |
| 130 | 145 | } |
| 146 | + | |
| 147 | + if (isset($this->params[$check_cond_field])) { | |
| 148 | + // get current value of the conditional parent field | |
| 149 | + $check_cond = $this->params[$check_cond_field]['default'] ?? null; | |
| 150 | + $check_cond = $this->settings[$check_cond_field] ?? $check_cond; | |
| 151 | + if (!is_null($check_cond)) { | |
| 152 | + // legacy syntax "conditional" => "test_mode" | |
| 153 | + if (!$check_cond_oper && (!$check_cond || !strcasecmp((string) $check_cond, 'off'))) { | |
| 154 | + // hide current field because the field to who this is dependant is "off" or disabled (i.e. 0) | |
| 155 | + $hidden_wrapper = true; | |
| 156 | + } | |
| 157 | + // conditional (equal) syntax "conditional" => "test_mode:1" | |
| 158 | + if ($check_cond_oper === ':' && $check_cond != $check_cond_value) { | |
| 159 | + // equal condition not met, hide the field | |
| 160 | + $hidden_wrapper = true; | |
| 161 | + } | |
| 162 | + // conditional (different) syntax "conditional" => "test_mode!:0" | |
| 163 | + if ($check_cond_oper === '!:' && $check_cond == $check_cond_value) { | |
| 164 | + // different condition not met, hide the field | |
| 165 | + $hidden_wrapper = true; | |
| 166 | + } | |
| 167 | + } | |
| 168 | + } | |
| 169 | + | |
| 170 | + if (!is_null($check_cond_value)) { | |
| 171 | + // the field is dependant on another through a syntax, memorize the condition for JS | |
| 172 | + $js_conditional_fields[$check_cond_field][] = [ | |
| 173 | + 'field' => $param_name, | |
| 174 | + 'oper' => $check_cond_oper, | |
| 175 | + 'value' => $check_cond_value, | |
| 176 | + 'multiple' => !empty($param_config['multiple']), | |
| 177 | + 'custom' => $param_config['type'] === 'custom', | |
| 178 | + ]; | |
| 179 | + } | |
| 131 | 180 | } |
| 132 | 181 | |
| 133 | 182 | $html .= '<div class="vbo-param-container' . (in_array($param_config['type'], ['textarea', 'visual_html']) ? ' vbo-param-container-full' : '') . ($nested_style ? ' vbo-param-nested' : '') . '"' . ($hidden_wrapper ? ' style="display: none;"' : '') . '>'; |
| 134 | - if (strlen($label) > 0 && (!isset($param_config['hidden']) || $param_config['hidden'] != true)) { | |
| 183 | + if (strlen($label) && (!isset($param_config['hidden']) || $param_config['hidden'] != true)) { | |
| 135 | 184 | $html .= '<div class="vbo-param-label">' . $label . '</div>'; |
| 136 | 185 | } |
| 137 | - $html .= '<div class="vbo-param-setting">'; | |
| 186 | + $html .= '<div class="vbo-param-setting"' . ($param_config['type'] === 'custom' ? ' data-custom="' . $this->inputName . '[' . $param_name . ']' . '"' : '') . '>'; | |
| 138 | 187 | |
| 139 | 188 | // render field |
| 140 | 189 | $html .= $this->getField($param_name, $param_config); |
| 141 | 190 | |
| @@ -140,9 +189,15 @@ | ||
| 140 | 189 | $html .= $this->getField($param_name, $param_config); |
| 141 | 190 | |
| 142 | 191 | // check for assets to be loaded, only once to obtain individual setups |
| 143 | 192 | if ($load_assets) { |
| 144 | - $this->loadAssets($load_once = true); | |
| 193 | + if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) { | |
| 194 | + // concatenate script(s) to HTML string when doing an AJAX request | |
| 195 | + $html .= "\n" . '<script>' . implode("\n", $this->buildScriptAssets($load_once = true)) . '</script>'; | |
| 196 | + } else { | |
| 197 | + // add script declaration(s) to document | |
| 198 | + $this->loadAssets($load_once = true); | |
| 199 | + } | |
| 145 | 200 | } |
| 146 | 201 | |
| 147 | 202 | if ($labelhelp) { |
| 148 | 203 | $html .= '<span class="vbo-param-setting-comment">' . $labelhelp . '</span>'; |
| @@ -151,8 +206,98 @@ | ||
| 151 | 206 | $html .= '</div>'; |
| 152 | 207 | $html .= '</div>'; |
| 153 | 208 | } |
| 154 | 209 | |
| 210 | + if ($js_conditional_fields) { | |
| 211 | + // build the JS script for handling contional field changes | |
| 212 | + $js_conditional_fields_json = json_encode($js_conditional_fields); | |
| 213 | + $base_input_name = $this->inputName; | |
| 214 | + $html .= | |
| 215 | +<<<HTML | |
| 216 | +<script> | |
| 217 | + VBOCore.DOMLoaded(() => { | |
| 218 | + let js_conditional_fields = $js_conditional_fields_json; | |
| 219 | + let base_input_name = "$base_input_name"; | |
| 220 | + | |
| 221 | + // scan all parent fields | |
| 222 | + Object.keys(js_conditional_fields).forEach((field_name) => { | |
| 223 | + let parent_fields = Array.from(document.querySelectorAll('[name="' + base_input_name + '[' + field_name + ']"]')).filter((field_input) => { | |
| 224 | + // get only valid input fields | |
| 225 | + return (field_input.matches('input') || field_input.matches('select') || field_input.matches('textarea')) && !field_input.matches('input[type="hidden"]'); | |
| 226 | + }); | |
| 227 | + | |
| 228 | + let parent_field = parent_fields[0] || null; | |
| 229 | + if (!parent_field) { | |
| 230 | + // invalid input field selected | |
| 231 | + return; | |
| 232 | + } | |
| 233 | + | |
| 234 | + if (!Array.isArray(js_conditional_fields[field_name])) { | |
| 235 | + // invalid parent conditions | |
| 236 | + return; | |
| 237 | + } | |
| 238 | + | |
| 239 | + // add change event listener | |
| 240 | + parent_field.addEventListener('change', (e) => { | |
| 241 | + // get the parent field current value | |
| 242 | + let parent_value = e.target.value; | |
| 243 | + if (e.target.matches('input[type="checkbox"]')) { | |
| 244 | + // checkbox fields should rely on their checked status | |
| 245 | + parent_value = e.target.checked ? '1' : '0'; | |
| 246 | + } | |
| 247 | + | |
| 248 | + // scan all dependant fields | |
| 249 | + js_conditional_fields[field_name].forEach((condition) => { | |
| 250 | + let field_selector = condition?.multiple ? '[name="' + base_input_name + '[' + condition.field + '][]"]' : '[name="' + base_input_name + '[' + condition.field + ']"]'; | |
| 251 | + if (condition.custom === true) { | |
| 252 | + field_selector = '[data-custom="' + base_input_name + '[' + condition.field + ']"]'; | |
| 253 | + } | |
| 254 | + let cond_fields = Array.from(document.querySelectorAll(field_selector)).filter((input_field) => { | |
| 255 | + // get only valid input fields or custom containers | |
| 256 | + if (input_field.matches('input[type="hidden"][data-type="file_upload"]')) { | |
| 257 | + return true; | |
| 258 | + } | |
| 259 | + if (condition.custom === true && input_field.matches('.vbo-param-setting[data-custom]')) { | |
| 260 | + return true; | |
| 261 | + } | |
| 262 | + return (input_field.matches('input') || input_field.matches('select') || input_field.matches('textarea')) && !input_field.matches('input[type="hidden"]'); | |
| 263 | + }); | |
| 264 | + | |
| 265 | + let cond_field = cond_fields[0] || null; | |
| 266 | + if (!cond_field) { | |
| 267 | + // invalid conditional field selected | |
| 268 | + return; | |
| 269 | + } | |
| 270 | + | |
| 271 | + // find the conditional field container | |
| 272 | + let cond_field_target = cond_field.closest('.vbo-param-container'); | |
| 273 | + if (!cond_field_target) { | |
| 274 | + // conditional field container not found | |
| 275 | + return; | |
| 276 | + } | |
| 277 | + | |
| 278 | + // validate condition syntax | |
| 279 | + if ((condition.oper == ':' && parent_value != condition.value) || (condition.oper == '!:' && parent_value == condition.value)) { | |
| 280 | + // hide conditional field not matching the condition syntax | |
| 281 | + cond_field_target.style.display = 'none'; | |
| 282 | + } else { | |
| 283 | + // show conditional field | |
| 284 | + cond_field_target.style.display = ''; | |
| 285 | + } | |
| 286 | + }); | |
| 287 | + }); | |
| 288 | + | |
| 289 | + // dispatch delayed change event | |
| 290 | + setTimeout(() => { | |
| 291 | + parent_field.dispatchEvent(new Event('change')); | |
| 292 | + }, 100); | |
| 293 | + }); | |
| 294 | + | |
| 295 | + }); | |
| 296 | +</script> | |
| 297 | +HTML; | |
| 298 | + } | |
| 299 | + | |
| 155 | 300 | // JS helper functions |
| 156 | 301 | $html .= $this->getScripts(); |
| 157 | 302 | |
| 158 | 303 | return $html; |
| @@ -158,17 +303,20 @@ | ||
| 158 | 303 | return $html; |
| 159 | 304 | } |
| 160 | 305 | |
| 161 | 306 | /** |
| 162 | - * Loads the requested assets, if any. | |
| 307 | + * Builds the requested script assets, if any. | |
| 163 | 308 | * |
| 164 | 309 | * @param bool $load_once True to unset the assets after loading. |
| 165 | 310 | * |
| 166 | - * @return void | |
| 311 | + * @return array | |
| 312 | + * | |
| 313 | + * @since 1.18.0 (J) - 1.8.0 (WP) | |
| 167 | 314 | */ |
| 168 | - public function loadAssets($load_once = false) | |
| 315 | + public function buildScriptAssets($load_once = false) | |
| 169 | 316 | { |
| 170 | - $doc = JFactory::getDocument(); | |
| 317 | + $scripts = []; | |
| 318 | + | |
| 171 | 319 | foreach ($this->assets as $asset_type => $asset_elements) { |
| 172 | 320 | if ($asset_type === 'select2') { |
| 173 | 321 | // build list of selectors |
| 174 | 322 | $ids_list = implode(', ', array_map(function($el) { |
| @@ -178,17 +326,18 @@ | ||
| 178 | 326 | // check for asset options |
| 179 | 327 | $asset_options = $this->assets['select2_options'] ?? null; |
| 180 | 328 | $asset_options_str = $asset_options ? json_encode($asset_options) : ''; |
| 181 | 329 | |
| 182 | - // add script declaration | |
| 330 | + // always attempt to load assets | |
| 183 | 331 | VikBooking::getVboApplication()->loadSelect2(); |
| 184 | - $doc->addScriptDeclaration( | |
| 185 | -<<<JS | |
| 332 | + | |
| 333 | + // build and push script | |
| 334 | + $scripts[] = | |
| 335 | +<<<JAVASCRIPT | |
| 186 | 336 | jQuery(function() { |
| 187 | 337 | jQuery('$ids_list').select2($asset_options_str); |
| 188 | 338 | }); |
| 189 | -JS | |
| 190 | - ); | |
| 339 | +JAVASCRIPT; | |
| 191 | 340 | } |
| 192 | 341 | |
| 193 | 342 | if ($load_once) { |
| 194 | 343 | unset($this->assets[$asset_type]); |
| @@ -193,11 +342,29 @@ | ||
| 193 | 342 | if ($load_once) { |
| 194 | 343 | unset($this->assets[$asset_type]); |
| 195 | 344 | } |
| 196 | 345 | } |
| 346 | + | |
| 347 | + return $scripts; | |
| 197 | 348 | } |
| 198 | 349 | |
| 199 | 350 | /** |
| 351 | + * Loads the requested assets, if any. | |
| 352 | + * | |
| 353 | + * @param bool $load_once True to unset the assets after loading. | |
| 354 | + * | |
| 355 | + * @return void | |
| 356 | + */ | |
| 357 | + public function loadAssets($load_once = false) | |
| 358 | + { | |
| 359 | + $doc = JFactory::getDocument(); | |
| 360 | + | |
| 361 | + foreach ($this->buildScriptAssets($load_once) as $script) { | |
| 362 | + $doc->addScriptDeclaration($script); | |
| 363 | + } | |
| 364 | + } | |
| 365 | + | |
| 366 | + /** | |
| 200 | 367 | * Gets the necessary script tags. |
| 201 | 368 | * |
| 202 | 369 | * @return string |
| 203 | 370 | */ |
| @@ -302,15 +469,180 @@ | ||
| 302 | 469 | } |
| 303 | 470 | } |
| 304 | 471 | $html .= '</select>' . "\n"; |
| 305 | 472 | break; |
| 473 | + case 'listings': | |
| 474 | + // build attributes list | |
| 475 | + $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name); | |
| 476 | + $elements_attr = [ | |
| 477 | + 'name' => $this->inputName . '[' . $param_name . ']', | |
| 478 | + ]; | |
| 479 | + if ($param_config['multiple'] ?? null) { | |
| 480 | + $elements_attr['multiple'] = 'multiple'; | |
| 481 | + $elements_attr['name'] .= '[]'; | |
| 482 | + } | |
| 483 | + $custom_attr = (array) ($param_config['attributes'] ?? []); | |
| 484 | + unset($custom_attr['id'], $custom_attr['name']); | |
| 485 | + $elements_attr = array_merge($elements_attr, $custom_attr); | |
| 486 | + | |
| 487 | + $wrapped = false; | |
| 488 | + $style_selection = false; | |
| 489 | + if ($param_config['inline'] ?? true) { | |
| 490 | + // wrap the select within an additional div | |
| 491 | + $html .= '<div class="' . (($param_config['multiple'] ?? null) ? 'vbo-multiselect-inline-elems-wrap' : 'vbo-singleselect-inline-elems-wrap') . '">'; | |
| 492 | + $wrapped = true; | |
| 493 | + $style_selection = (bool) ($param_config['multiple'] ?? null); | |
| 494 | + } elseif ($param_config['wrapdivcls'] ?? null) { | |
| 495 | + // wrap the select within a custom div | |
| 496 | + $html .= '<div class="' . $param_config['wrapdivcls'] . '">'; | |
| 497 | + $wrapped = true; | |
| 498 | + } | |
| 499 | + | |
| 500 | + // obtain the necessary HTML code for rendering | |
| 501 | + $html .= VikBooking::getVboApplication()->renderElementsDropDown([ | |
| 502 | + 'id' => $element_id, | |
| 503 | + 'elements' => 'listings', | |
| 504 | + 'placeholder' => ($param_config['asset_options']['placeholder'] ?? null), | |
| 505 | + 'allow_clear' => ($param_config['asset_options']['allowClear'] ?? $param_config['asset_options']['allow_clear'] ?? null), | |
| 506 | + 'attributes' => $elements_attr, | |
| 507 | + 'selected_value' => (is_scalar($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_scalar($default_paramv ?? null) ? $default_paramv : null)), | |
| 508 | + 'selected_values' => (is_array($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_array($default_paramv ?? null) ? $default_paramv : null)), | |
| 509 | + 'style_selection' => $style_selection, | |
| 510 | + ]); | |
| 511 | + | |
| 512 | + if ($wrapped) { | |
| 513 | + // close the select div wrapper | |
| 514 | + $html .= '</div>'; | |
| 515 | + } | |
| 516 | + break; | |
| 517 | + case 'elements': | |
| 518 | + // build attributes list | |
| 519 | + $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name); | |
| 520 | + $elements_attr = [ | |
| 521 | + 'name' => $this->inputName . '[' . $param_name . ']', | |
| 522 | + ]; | |
| 523 | + if ($param_config['multiple'] ?? null) { | |
| 524 | + $elements_attr['multiple'] = 'multiple'; | |
| 525 | + $elements_attr['name'] .= '[]'; | |
| 526 | + } | |
| 527 | + $custom_attr = (array) ($param_config['attributes'] ?? []); | |
| 528 | + unset($custom_attr['id'], $custom_attr['name']); | |
| 529 | + $elements_attr = array_merge($elements_attr, $custom_attr); | |
| 530 | + | |
| 531 | + $wrapped = false; | |
| 532 | + $style_selection = false; | |
| 533 | + if ($param_config['inline'] ?? true) { | |
| 534 | + // wrap the select within an additional div | |
| 535 | + $html .= '<div class="' . (($param_config['multiple'] ?? null) ? 'vbo-multiselect-inline-elems-wrap' : 'vbo-singleselect-inline-elems-wrap') . '">'; | |
| 536 | + $wrapped = true; | |
| 537 | + $style_selection = (bool) ($param_config['multiple'] ?? null); | |
| 538 | + } elseif ($param_config['wrapdivcls'] ?? null) { | |
| 539 | + // wrap the select within a custom div | |
| 540 | + $html .= '<div class="' . $param_config['wrapdivcls'] . '">'; | |
| 541 | + $wrapped = true; | |
| 542 | + } | |
| 543 | + | |
| 544 | + // obtain the necessary HTML code for rendering | |
| 545 | + $html .= VikBooking::getVboApplication()->renderElementsDropDown([ | |
| 546 | + 'id' => $element_id, | |
| 547 | + 'placeholder' => ($param_config['asset_options']['placeholder'] ?? null), | |
| 548 | + 'allow_clear' => ($param_config['asset_options']['allowClear'] ?? $param_config['asset_options']['allow_clear'] ?? null), | |
| 549 | + 'attributes' => $elements_attr, | |
| 550 | + 'element_def_img_uri' => ($param_config['element_def_img_uri'] ?? ''), | |
| 551 | + 'style_selection' => ($param_config['style_selection'] ?? $style_selection), | |
| 552 | + 'selected_value' => (is_scalar($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_scalar($default_paramv ?? null) ? $default_paramv : null)), | |
| 553 | + 'selected_values' => (is_array($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_array($default_paramv ?? null) ? $default_paramv : null)), | |
| 554 | + ], (array) ($param_config['elements'] ?? []), (array) ($param_config['groups'] ?? [])); | |
| 555 | + | |
| 556 | + if ($wrapped) { | |
| 557 | + // close the select div wrapper | |
| 558 | + $html .= '</div>'; | |
| 559 | + } | |
| 560 | + break; | |
| 561 | + case 'tags': | |
| 562 | + // build attributes list | |
| 563 | + $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name); | |
| 564 | + $elements_attr = [ | |
| 565 | + 'name' => $this->inputName . '[' . $param_name . ']', | |
| 566 | + ]; | |
| 567 | + if ($param_config['multiple'] ?? null) { | |
| 568 | + $elements_attr['multiple'] = 'multiple'; | |
| 569 | + $elements_attr['name'] .= '[]'; | |
| 570 | + } | |
| 571 | + $custom_attr = (array) ($param_config['attributes'] ?? []); | |
| 572 | + unset($custom_attr['id'], $custom_attr['name']); | |
| 573 | + $elements_attr = array_merge($elements_attr, $custom_attr); | |
| 574 | + | |
| 575 | + $wrapped = false; | |
| 576 | + $style_selection = (bool) ($param_config['style_selection'] ?? null); | |
| 577 | + if ($param_config['inline'] ?? true) { | |
| 578 | + // wrap the select within an additional div | |
| 579 | + $html .= '<div class="' . (($param_config['multiple'] ?? null) ? 'vbo-multiselect-inline-elems-wrap' : 'vbo-singleselect-inline-elems-wrap') . '">'; | |
| 580 | + $wrapped = true; | |
| 581 | + $style_selection = (bool) ($param_config['multiple'] ?? null); | |
| 582 | + } elseif ($param_config['wrapdivcls'] ?? null) { | |
| 583 | + // wrap the select within a custom div | |
| 584 | + $html .= '<div class="' . $param_config['wrapdivcls'] . '">'; | |
| 585 | + $wrapped = true; | |
| 586 | + } | |
| 587 | + | |
| 588 | + // obtain the necessary HTML code for rendering | |
| 589 | + $html .= VikBooking::getVboApplication()->renderTagsDropDown([ | |
| 590 | + 'id' => $element_id, | |
| 591 | + 'placeholder' => ($param_config['asset_options']['placeholder'] ?? null), | |
| 592 | + 'allow_clear' => ($param_config['asset_options']['allowClear'] ?? $param_config['asset_options']['allow_clear'] ?? null), | |
| 593 | + 'attributes' => $elements_attr, | |
| 594 | + 'selected_value' => (is_scalar($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_scalar($default_paramv ?? null) ? $default_paramv : null)), | |
| 595 | + 'selected_values' => (is_array($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_array($default_paramv ?? null) ? $default_paramv : null)), | |
| 596 | + 'style_selection' => $style_selection, | |
| 597 | + ], (array) ($param_config['tags'] ?? []), (array) ($param_config['groups'] ?? [])); | |
| 598 | + | |
| 599 | + if ($wrapped) { | |
| 600 | + // close the select div wrapper | |
| 601 | + $html .= '</div>'; | |
| 602 | + } | |
| 603 | + break; | |
| 604 | + case 'datetime': | |
| 605 | + // build attributes list | |
| 606 | + $element_id = 'vik-dtp-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name); | |
| 607 | + $elements_attr = [ | |
| 608 | + 'name' => $this->inputName . '[' . $param_name . ']', | |
| 609 | + 'value' => $this->settings[$param_name] ?? $default_paramv ?: '', | |
| 610 | + ]; | |
| 611 | + $custom_attr = (array) ($param_config['attributes'] ?? []); | |
| 612 | + unset($custom_attr['id'], $custom_attr['name'], $custom_attr['value']); | |
| 613 | + $elements_attr = array_merge($elements_attr, $custom_attr); | |
| 614 | + | |
| 615 | + // obtain the necessary HTML code for rendering | |
| 616 | + $html .= VikBooking::getVboApplication()->renderDateTimePicker([ | |
| 617 | + 'id' => $element_id, | |
| 618 | + 'attributes' => $elements_attr, | |
| 619 | + ]); | |
| 620 | + break; | |
| 621 | + case 'time': | |
| 622 | + // build attributes list | |
| 623 | + $element_id = 'vik-tp-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name); | |
| 624 | + $elements_attr = [ | |
| 625 | + 'name' => $this->inputName . '[' . $param_name . ']', | |
| 626 | + 'value' => $this->settings[$param_name] ?? $default_paramv ?: '', | |
| 627 | + ]; | |
| 628 | + $custom_attr = (array) ($param_config['attributes'] ?? []); | |
| 629 | + unset($custom_attr['id'], $custom_attr['name'], $custom_attr['value']); | |
| 630 | + $elements_attr = array_merge($elements_attr, $custom_attr); | |
| 631 | + | |
| 632 | + // obtain the necessary HTML code for rendering | |
| 633 | + $html .= VikBooking::getVboApplication()->renderTimePicker([ | |
| 634 | + 'id' => $element_id, | |
| 635 | + 'attributes' => $elements_attr, | |
| 636 | + ]); | |
| 637 | + break; | |
| 306 | 638 | case 'password': |
| 307 | 639 | $html .= '<div class="btn-wrapper input-append">'; |
| 308 | - $html .= '<input type="password" name="' . $this->inputName . '[' . $param_name . ']" value="'.(isset($this->settings[$param_name]) ? JHtml::fetch('esc_attr', $this->settings[$param_name]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"' . $inp_attr . '/>'; | |
| 640 | + $html .= '<input type="password" name="' . $this->inputName . '[' . $param_name . ']" value="'.(isset($this->settings[$param_name]) ? JHtml::fetch('esc_attr', $this->settings[$param_name]) : JHtml::fetch('esc_attr', $default_paramv)).'" autocomplete="new-password" size="20"' . $inp_attr . '/>'; | |
| 309 | 641 | $html .= '<button type="button" class="btn btn-primary" onclick="vboParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>'; |
| 310 | 642 | $html .= '</div>'; |
| 311 | 643 | // set flag for JS helper |
| 312 | - $$this->scripts[] = $param_config['type']; | |
| 644 | + $this->scripts[] = $param_config['type']; | |
| 313 | 645 | break; |
| 314 | 646 | case 'number': |
| 315 | 647 | $number_attr = []; |
| 316 | 648 | if (isset($param_config['min'])) { |
| @@ -370,8 +702,272 @@ | ||
| 370 | 702 | case 'checkbox': |
| 371 | 703 | // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status |
| 372 | 704 | $html .= '<input type="hidden" name="' . $this->inputName . '[' . $param_name . ']" value="0" />'; |
| 373 | 705 | $html .= VikBooking::getVboApplication()->printYesNoButtons($this->inputName . '['.$param_name.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($this->settings[$param_name]) ? (int)$this->settings[$param_name] : (int)$default_paramv), 1, 0); |
| 706 | + break; | |
| 707 | + case 'calendar': | |
| 708 | + $e_options = isset($param_config['options']) && is_array($param_config['options']) ? $param_config['options'] : []; | |
| 709 | + $e_id = isset($e_options['id']) ? $e_options['id'] : $this->inputName . '_' . $param_name; | |
| 710 | + $html .= VikBooking::getVboApplication()->getCalendar($this->settings[$param_name] ?? $default_paramv, $this->inputName . '['.$param_name.']', $e_id, $e_options['df'] ?? null, $e_options['attributes'] ?? []); | |
| 711 | + break; | |
| 712 | + case 'file_upload': | |
| 713 | + /** | |
| 714 | + * File upload (AJAX) field, for single or multiple files uploading. | |
| 715 | + * | |
| 716 | + * @since 1.18.3 (J) - 1.8.3 (WP) | |
| 717 | + */ | |
| 718 | + $element_id = 'vik-fileupload-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name); | |
| 719 | + $element_nm = $this->inputName . '[' . $param_name . ']'; | |
| 720 | + $multiple = ''; | |
| 721 | + if ($param_config['multiple'] ?? null) { | |
| 722 | + $multiple = 'multiple'; | |
| 723 | + $element_nm .= '[]'; | |
| 724 | + } | |
| 725 | + | |
| 726 | + // site root URI | |
| 727 | + $site_uri = JUri::root(); | |
| 728 | + | |
| 729 | + // CSRF token for safe AJAX requests | |
| 730 | + $csrf = addslashes(JSession::getFormToken()); | |
| 731 | + | |
| 732 | + // default file icon class | |
| 733 | + $file_icon_class = VikBookingIcons::i('file'); | |
| 734 | + | |
| 735 | + // JSON upload options | |
| 736 | + $upload_options = [ | |
| 737 | + 'element_id' => $element_id, | |
| 738 | + 'csrf_token' => $csrf, | |
| 739 | + 'field_name' => 'vbo_files', | |
| 740 | + 'param_name' => $element_nm, | |
| 741 | + 'upload_url' => VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=service.upload'), | |
| 742 | + 'allowed_types' => (string) ($param_config['allowed_types'] ?? ''), | |
| 743 | + 'safe_file_name' => (int) ($param_config['safe_file_name'] ?? 0), | |
| 744 | + 'return_type' => (string) ($param_config['return_type'] ?? 'path'), | |
| 745 | + 'file_icon_class' => $file_icon_class, | |
| 746 | + 'loading_icon_class' => VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'), | |
| 747 | + ]; | |
| 748 | + $json_upload_options = json_encode($upload_options); | |
| 749 | + | |
| 750 | + // upload or drag & drop text | |
| 751 | + $help_text = sprintf('<a href="JavaScript: void(0);">%s</a> %s', JText::translate('VBOMANUALUPLOAD'), JText::translate('VBODROPFILES')); | |
| 752 | + | |
| 753 | + // uploaded files | |
| 754 | + $uploaded_files = array_values(array_filter((array) ($this->settings[$param_name] ?? null))); | |
| 755 | + $uploaded_html = ''; | |
| 756 | + foreach ($uploaded_files as $uploaded_file) { | |
| 757 | + if (strpos($uploaded_file, VBO_ADMIN_PATH) !== false && !is_file($uploaded_file)) { | |
| 758 | + // file value is an internal path, but it no longer exists | |
| 759 | + continue; | |
| 760 | + } | |
| 761 | + $uploaded_file_val = htmlspecialchars((string) $uploaded_file, ENT_QUOTES, 'UTF-8'); | |
| 762 | + $uploaded_file_name = basename((string) $uploaded_file); | |
| 763 | + $uploaded_file_cont = $uploaded_file_name; | |
| 764 | + if (strpos($uploaded_file, $site_uri) !== false) { | |
| 765 | + // make it a link | |
| 766 | + $uploaded_file_cont = '<a href="' . $uploaded_file . '" target="_blank">' . $uploaded_file_name . '</a>'; | |
| 767 | + } | |
| 768 | + // render current file | |
| 769 | + $uploaded_html .= <<<HTML | |
| 770 | +<div class="file-elem"> | |
| 771 | + <div class="file-elem-inner"> | |
| 772 | + <div class="file-summary"> | |
| 773 | + <i class="{$file_icon_class}"></i> | |
| 774 | + <div class="filename">{$uploaded_file_cont}</div> | |
| 775 | + <input type="hidden" name="{$element_nm}" value="{$uploaded_file_val}" data-type="file_upload" /> | |
| 776 | + </div> | |
| 777 | + </div> | |
| 778 | +</div> | |
| 779 | +HTML; | |
| 780 | + } | |
| 781 | + | |
| 782 | + if (!$uploaded_files) { | |
| 783 | + // display an empty input hidden element to let any conditional rule work | |
| 784 | + $uploaded_html = <<<HTML | |
| 785 | +<input type="hidden" name="{$element_nm}" value="" data-type="file_upload" data-empty="1" /> | |
| 786 | +HTML; | |
| 787 | + } | |
| 788 | + | |
| 789 | + // visible element | |
| 790 | + $html .= <<<HTML | |
| 791 | +<div class="vbo-param-file-upload-wrap vbo-dropfiles-target"> | |
| 792 | + <div class="vbo-uploaded-files">{$uploaded_html}</div> | |
| 793 | + <div class="vbo-param-file-upload-loading"></div> | |
| 794 | + <div class="lead">{$help_text}</div> | |
| 795 | + <input type="file" id="{$element_id}" data-upload="{$multiple}" hidden {$multiple}/> | |
| 796 | +</div> | |
| 797 | +<script> | |
| 798 | + function vboParamFieldRenderUploads(result, options) { | |
| 799 | + if (!options?.inputElement) { | |
| 800 | + throw new Error('Missing target'); | |
| 801 | + } | |
| 802 | + | |
| 803 | + if (!result?.processed) { | |
| 804 | + throw new Error('No files were processed'); | |
| 805 | + } | |
| 806 | + | |
| 807 | + if (!result?.paths || !result.paths.length) { | |
| 808 | + alert('No valid files were uploaded'); | |
| 809 | + return; | |
| 810 | + } | |
| 811 | + | |
| 812 | + // define the default file-uploaded icon element class list | |
| 813 | + let fileIconClassList = []; | |
| 814 | + if (options?.file_icon_class) { | |
| 815 | + fileIconClassList = options.file_icon_class.split(' '); | |
| 816 | + } | |
| 817 | + | |
| 818 | + // target the current list of files uploaded and make it empty | |
| 819 | + const filesPool = options.inputElement.closest('.vbo-param-file-upload-wrap').querySelector('.vbo-uploaded-files'); | |
| 820 | + filesPool.innerHTML = ''; | |
| 821 | + | |
| 822 | + // iterate over each file uploaded | |
| 823 | + result.fileNames.forEach((name, index) => { | |
| 824 | + // build uploaded file nodes | |
| 825 | + let fileNode = document.createElement('div'); | |
| 826 | + fileNode.classList.add('file-elem'); | |
| 827 | + let fileInner = document.createElement('div'); | |
| 828 | + fileInner.classList.add('file-elem-inner'); | |
| 829 | + let fileSummary = document.createElement('div'); | |
| 830 | + fileSummary.classList.add('file-summary'); | |
| 831 | + let fileIcon = document.createElement('i'); | |
| 832 | + if (fileIconClassList.length) { | |
| 833 | + fileIcon.classList.add(...fileIconClassList); | |
| 834 | + } | |
| 835 | + let fileName = document.createElement('div'); | |
| 836 | + fileName.classList.add('filename'); | |
| 837 | + fileName.innerText = name; | |
| 838 | + let fileInput = document.createElement('input'); | |
| 839 | + fileInput.setAttribute('type', 'hidden'); | |
| 840 | + fileInput.setAttribute('name', options?.param_name); | |
| 841 | + if (options?.return_type == 'url') { | |
| 842 | + fileInput.value = result.urls[index] || name; | |
| 843 | + // make the file name element a link | |
| 844 | + fileName.innerText = ''; | |
| 845 | + let fileLink = document.createElement('a'); | |
| 846 | + fileLink.setAttribute('href', fileInput.value); | |
| 847 | + fileLink.setAttribute('target', '_blank'); | |
| 848 | + fileLink.innerText = name; | |
| 849 | + fileName.append(fileLink); | |
| 850 | + } else if (options?.return_type == 'name') { | |
| 851 | + fileInput.value = name; | |
| 852 | + } else { | |
| 853 | + fileInput.value = result.paths[index] || name; | |
| 854 | + } | |
| 855 | + | |
| 856 | + // append nodes to files pool | |
| 857 | + fileSummary.append(fileIcon, fileName, fileInput); | |
| 858 | + fileInner.append(fileSummary); | |
| 859 | + fileNode.append(fileInner); | |
| 860 | + filesPool.append(fileNode); | |
| 861 | + }); | |
| 862 | + } | |
| 863 | + | |
| 864 | + async function vboParamFieldUploadFiles(files, options) { | |
| 865 | + const fieldBaseName = options?.field_name; | |
| 866 | + const formData = new FormData(); | |
| 867 | + for (let i = 0; i < files.length; i++) { | |
| 868 | + formData.append(fieldBaseName + '[]', files[i]); | |
| 869 | + } | |
| 870 | + | |
| 871 | + if (options?.allowed_types) { | |
| 872 | + // comma separated string of allowed file extension types | |
| 873 | + formData.append('allowed_types', options.allowed_types); | |
| 874 | + } | |
| 875 | + | |
| 876 | + if (options?.safe_file_name) { | |
| 877 | + // whether to keep the original file name or randomize it | |
| 878 | + formData.append('safe_file_name', options.safe_file_name); | |
| 879 | + } | |
| 880 | + | |
| 881 | + // define the default upload-loading icon element class list | |
| 882 | + let loadingIconClassList = []; | |
| 883 | + let loadingElement = null; | |
| 884 | + if (options?.loading_icon_class) { | |
| 885 | + loadingIconClassList = options.loading_icon_class.split(' '); | |
| 886 | + } | |
| 887 | + | |
| 888 | + if (loadingIconClassList.length && options?.inputElement) { | |
| 889 | + // build loading icon element | |
| 890 | + loadingElement = document.createElement('i'); | |
| 891 | + loadingElement.classList.add(...loadingIconClassList); | |
| 892 | + // append loading element | |
| 893 | + options | |
| 894 | + .inputElement | |
| 895 | + .closest('.vbo-param-file-upload-wrap') | |
| 896 | + .querySelector('.vbo-param-file-upload-loading') | |
| 897 | + .append(loadingElement); | |
| 898 | + } | |
| 899 | + | |
| 900 | + try { | |
| 901 | + const response = await fetch(options?.upload_url, { | |
| 902 | + method: 'POST', | |
| 903 | + headers: { | |
| 904 | + 'X-CSRF-Token': options?.csrf_token, | |
| 905 | + }, | |
| 906 | + body: formData, | |
| 907 | + }); | |
| 908 | + | |
| 909 | + const result = await response.json().catch(() => null); | |
| 910 | + | |
| 911 | + if (response.ok) { | |
| 912 | + // render files uploaded | |
| 913 | + vboParamFieldRenderUploads(result, options); | |
| 914 | + } else { | |
| 915 | + alert('Upload failed: ' + response.statusText); | |
| 916 | + } | |
| 917 | + } catch (error) { | |
| 918 | + console.error('Upload error:', error); | |
| 919 | + alert('An error occurred during upload.'); | |
| 920 | + } | |
| 921 | + | |
| 922 | + if (options?.inputElement) { | |
| 923 | + // reset file input element value to allow additional uploads | |
| 924 | + options.inputElement.value = ''; | |
| 925 | + } | |
| 926 | + | |
| 927 | + if (loadingElement) { | |
| 928 | + // remove loading animation | |
| 929 | + loadingElement.remove(); | |
| 930 | + } | |
| 931 | + } | |
| 932 | + | |
| 933 | + function vboParamFieldUploadSetup(options) { | |
| 934 | + // target elements | |
| 935 | + const fileInput = document.getElementById(options?.element_id); | |
| 936 | + const dropTarget = fileInput.closest('.vbo-param-file-upload-wrap'); | |
| 937 | + | |
| 938 | + // open file dialog by simulating the click on hidden file input | |
| 939 | + dropTarget.addEventListener('click', () => fileInput.click()); | |
| 940 | + | |
| 941 | + // drop target drag and drop events | |
| 942 | + dropTarget.addEventListener('dragover', e => { | |
| 943 | + e.preventDefault(); | |
| 944 | + dropTarget.classList.add('drag-over', 'drag-enter'); | |
| 945 | + }); | |
| 946 | + dropTarget.addEventListener('dragleave', () => { | |
| 947 | + dropTarget.classList.remove('drag-over', 'drag-enter'); | |
| 948 | + }); | |
| 949 | + dropTarget.addEventListener('drop', async e => { | |
| 950 | + e.preventDefault(); | |
| 951 | + dropTarget.classList.remove('drag-over', 'drag-enter'); | |
| 952 | + const files = e.dataTransfer.files; | |
| 953 | + if (files.length) { | |
| 954 | + await vboParamFieldUploadFiles(files, Object.assign({}, options, {inputElement: fileInput})); | |
| 955 | + } | |
| 956 | + }); | |
| 957 | + | |
| 958 | + // input file element change event | |
| 959 | + fileInput.addEventListener('change', async e => { | |
| 960 | + if (e.target.files.length) { | |
| 961 | + await vboParamFieldUploadFiles(e.target.files, Object.assign({}, options, {inputElement: fileInput})); | |
| 962 | + } | |
| 963 | + }); | |
| 964 | + } | |
| 965 | + | |
| 966 | + // configure field | |
| 967 | + vboParamFieldUploadSetup({$json_upload_options}); | |
| 968 | +</script> | |
| 969 | +HTML; | |
| 374 | 970 | break; |
| 375 | 971 | default: |
| 376 | 972 | $html .= '<input type="text" name="' . $this->inputName . '[' . $param_name . ']" value="'.(isset($this->settings[$param_name]) ? JHtml::fetch('esc_attr', $this->settings[$param_name]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"' . $inp_attr . '/>'; |
| 377 | 973 | break; |