PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / helpers / src / params / rendering.php

rendering.php in VikBooking Hotel Booking Engine & PMS 1.8.6, at admin/helpers/src/params/rendering.php

974 lines 45.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2024 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Helper class to render specific param types.
15 *
16 * @since 1.16.9 (J) - 1.6.9 (WP)
17 */
18 final class VBOParamsRendering
19 {
20 /**
21 * @var array
22 */
23 private $params = [];
24
25 /**
26 * @var array
27 */
28 private $settings = [];
29
30 /**
31 * @var array
32 */
33 private $scripts = [];
34
35 /**
36 * @var array
37 */
38 private $assets = [];
39
40 /**
41 * @var string
42 */
43 private $inputName = 'vboparams';
44
45 /**
46 * @var int
47 */
48 private static $instance_counter = -1;
49
50 /**
51 * Class constructor is protected.
52 *
53 * @param array $params The form params to bind.
54 * @param array $settings The form settings to bind.
55 *
56 * @see getInstance()
57 */
58 private function __construct(array $params, array $settings)
59 {
60 // bind values
61 $this->params = $params;
62 $this->settings = $settings;
63
64 // increase instance counter
65 static::$instance_counter++;
66 }
67
68 /**
69 * Proxy for immediately accessing the object and bind data.
70 *
71 * @param array $params The form params to bind.
72 * @param array $settings The form settings to bind.
73 *
74 * @return VBOParamsRendering
75 */
76 public static function getInstance(array $params = [], array $settings = [])
77 {
78 return new static($params, $settings);
79 }
80
81 /**
82 * Sets the name to be used for rendering the param fields.
83 *
84 * @param string $name The name to use.
85 *
86 * @return self
87 */
88 public function setInputName($name)
89 {
90 $this->inputName = (string) $name;
91
92 return $this;
93 }
94
95 /**
96 * Renders the injected form params and returns the HTML code.
97 *
98 * @param bool $load_assets Whether to load the necessary assets.
99 *
100 * @return string
101 */
102 public function getHtml($load_assets = true)
103 {
104 if (!$this->params) {
105 return '';
106 }
107
108 // build the HTML params string
109 $html = '';
110
111 // list of conditional field rules
112 $js_conditional_fields = [];
113
114 // scan all field params
115 foreach ($this->params as $param_name => $param_config) {
116 if (empty($param_name)) {
117 continue;
118 }
119
120 $labelparts = explode('//', (isset($param_config['label']) ? $param_config['label'] : ''));
121 $label = $labelparts[0];
122 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
123 if (!empty($param_config['help'])) {
124 $labelhelp = $param_config['help'];
125 }
126
127 $nested_style = (isset($param_config['nested']) && $param_config['nested']);
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];
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 }
180 }
181
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;"' : '') . '>';
183 if (strlen($label) && (!isset($param_config['hidden']) || $param_config['hidden'] != true)) {
184 $html .= '<div class="vbo-param-label">' . $label . '</div>';
185 }
186 $html .= '<div class="vbo-param-setting"' . ($param_config['type'] === 'custom' ? ' data-custom="' . $this->inputName . '[' . $param_name . ']' . '"' : '') . '>';
187
188 // render field
189 $html .= $this->getField($param_name, $param_config);
190
191 // check for assets to be loaded, only once to obtain individual setups
192 if ($load_assets) {
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 }
200 }
201
202 if ($labelhelp) {
203 $html .= '<span class="vbo-param-setting-comment">' . $labelhelp . '</span>';
204 }
205
206 $html .= '</div>';
207 $html .= '</div>';
208 }
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
290 });
291 </script>
292 HTML;
293 }
294
295 // JS helper functions
296 $html .= $this->getScripts();
297
298 return $html;
299 }
300
301 /**
302 * Builds the requested script assets, if any.
303 *
304 * @param bool $load_once True to unset the assets after loading.
305 *
306 * @return array
307 *
308 * @since 1.18.0 (J) - 1.8.0 (WP)
309 */
310 public function buildScriptAssets($load_once = false)
311 {
312 $scripts = [];
313
314 foreach ($this->assets as $asset_type => $asset_elements) {
315 if ($asset_type === 'select2') {
316 // build list of selectors
317 $ids_list = implode(', ', array_map(function($el) {
318 return "#{$el}";
319 }, $asset_elements));
320
321 // check for asset options
322 $asset_options = $this->assets['select2_options'] ?? null;
323 $asset_options_str = $asset_options ? json_encode($asset_options) : '';
324
325 // always attempt to load assets
326 VikBooking::getVboApplication()->loadSelect2();
327
328 // build and push script
329 $scripts[] =
330 <<<JAVASCRIPT
331 jQuery(function() {
332 jQuery('$ids_list').select2($asset_options_str);
333 });
334 JAVASCRIPT;
335 }
336
337 if ($load_once) {
338 unset($this->assets[$asset_type]);
339 }
340 }
341
342 return $scripts;
343 }
344
345 /**
346 * Loads the requested assets, if any.
347 *
348 * @param bool $load_once True to unset the assets after loading.
349 *
350 * @return void
351 */
352 public function loadAssets($load_once = false)
353 {
354 $doc = JFactory::getDocument();
355
356 foreach ($this->buildScriptAssets($load_once) as $script) {
357 $doc->addScriptDeclaration($script);
358 }
359 }
360
361 /**
362 * Gets the necessary script tags.
363 *
364 * @return string
365 */
366 public function getScripts()
367 {
368 $html = '';
369
370 if (in_array('password', $this->scripts)) {
371 // toggle the password fields
372 $html .= "\n" . '<script>' . "\n";
373 $html .= 'function vboParamTogglePwd(elem) {' . "\n";
374 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
375 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
376 $html .= ' var inp_type = inp.attr("type");' . "\n";
377 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
378 $html .= '}' . "\n";
379 $html .= "\n" . '</script>' . "\n";
380 }
381
382 return $html;
383 }
384
385 /**
386 * Renders the given param name according to config.
387 * Eventually populates the assets and scripts to be loaded.
388 *
389 * @param string $param_name The param name.
390 * @param array $param_config The param configuration.
391 *
392 * @return string
393 */
394 public function getField($param_name, $param_config)
395 {
396 $html = '';
397
398 $inp_attr = '';
399 if (isset($param_config['attributes']) && is_array($param_config['attributes'])) {
400 foreach ($param_config['attributes'] as $inpk => $inpv) {
401 $inp_attr .= $inpk . '="' . $inpv . '" ';
402 }
403 $inp_attr = ' ' . rtrim($inp_attr);
404 }
405
406 $default_paramv = $param_config['default'] ?? null;
407
408 switch ($param_config['type']) {
409 case 'custom':
410 $html .= $param_config['html'];
411 break;
412 case 'select':
413 $options = isset($param_config['options']) && is_array($param_config['options']) ? $param_config['options'] : [];
414 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
415 $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
416 $set_attr = true;
417 if (isset($param_config['attributes']) && is_array($param_config['attributes']) && isset($param_config['attributes']['id'])) {
418 $element_id = $param_config['attributes']['id'];
419 $set_attr = false;
420 }
421 if (isset($param_config['assets']) && $param_config['assets']) {
422 if (!isset($this->assets['select2'])) {
423 $this->assets['select2'] = [];
424 }
425 $this->assets['select2'][] = $element_id;
426 $this->assets['select2_options'] = $param_config['asset_options'] ?? null;
427 }
428 if (isset($param_config['multiple']) && $param_config['multiple']) {
429 $html .= '<select name="' . $this->inputName . '[' . $param_name . '][]" multiple="multiple"' . $inp_attr . ($set_attr ? ' id="' . $element_id . '"' : '') . '>' . "\n";
430 } else {
431 $html .= '<select name="' . $this->inputName . '[' . $param_name . ']"' . $inp_attr . ($set_attr ? ' id="' . $element_id . '"' : '') . '>' . "\n";
432 }
433 foreach ($options as $optind => $optval) {
434 // support nested array values for the option-group tags
435 $group = null;
436 $sel_opts = [$optind => $optval];
437 if (is_array($optval)) {
438 $group = $optind;
439 $sel_opts = $optval;
440 }
441 if ($group) {
442 $html .= '<optgroup label="' . JHtml::fetch('esc_attr', JText::translate($group)) . '">' . "\n";
443 }
444 foreach ($sel_opts as $optkey => $poption) {
445 $checkval = $is_assoc ? $optkey : $poption;
446 $selected = false;
447 if (isset($this->settings[$param_name])) {
448 if (is_array($this->settings[$param_name])) {
449 $selected = in_array($checkval, $this->settings[$param_name]);
450 } else {
451 $selected = ($checkval == $this->settings[$param_name]);
452 }
453 } elseif (isset($default_paramv)) {
454 if (is_array($default_paramv)) {
455 $selected = in_array($checkval, $default_paramv);
456 } else {
457 $selected = ($checkval == $default_paramv);
458 }
459 }
460 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>' . "\n";
461 }
462 if ($group) {
463 $html .= '</optgroup>' . "\n";
464 }
465 }
466 $html .= '</select>' . "\n";
467 break;
468 case 'listings':
469 // build attributes list
470 $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
471 $elements_attr = [
472 'name' => $this->inputName . '[' . $param_name . ']',
473 ];
474 if ($param_config['multiple'] ?? null) {
475 $elements_attr['multiple'] = 'multiple';
476 $elements_attr['name'] .= '[]';
477 }
478 $custom_attr = (array) ($param_config['attributes'] ?? []);
479 unset($custom_attr['id'], $custom_attr['name']);
480 $elements_attr = array_merge($elements_attr, $custom_attr);
481
482 $wrapped = false;
483 $style_selection = false;
484 if ($param_config['inline'] ?? true) {
485 // wrap the select within an additional div
486 $html .= '<div class="' . (($param_config['multiple'] ?? null) ? 'vbo-multiselect-inline-elems-wrap' : 'vbo-singleselect-inline-elems-wrap') . '">';
487 $wrapped = true;
488 $style_selection = (bool) ($param_config['multiple'] ?? null);
489 } elseif ($param_config['wrapdivcls'] ?? null) {
490 // wrap the select within a custom div
491 $html .= '<div class="' . $param_config['wrapdivcls'] . '">';
492 $wrapped = true;
493 }
494
495 // obtain the necessary HTML code for rendering
496 $html .= VikBooking::getVboApplication()->renderElementsDropDown([
497 'id' => $element_id,
498 'elements' => 'listings',
499 'placeholder' => ($param_config['asset_options']['placeholder'] ?? null),
500 'allow_clear' => ($param_config['asset_options']['allowClear'] ?? $param_config['asset_options']['allow_clear'] ?? null),
501 'attributes' => $elements_attr,
502 'selected_value' => (is_scalar($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_scalar($default_paramv ?? null) ? $default_paramv : null)),
503 'selected_values' => (is_array($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_array($default_paramv ?? null) ? $default_paramv : null)),
504 'style_selection' => $style_selection,
505 ]);
506
507 if ($wrapped) {
508 // close the select div wrapper
509 $html .= '</div>';
510 }
511 break;
512 case 'elements':
513 // build attributes list
514 $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
515 $elements_attr = [
516 'name' => $this->inputName . '[' . $param_name . ']',
517 ];
518 if ($param_config['multiple'] ?? null) {
519 $elements_attr['multiple'] = 'multiple';
520 $elements_attr['name'] .= '[]';
521 }
522 $custom_attr = (array) ($param_config['attributes'] ?? []);
523 unset($custom_attr['id'], $custom_attr['name']);
524 $elements_attr = array_merge($elements_attr, $custom_attr);
525
526 $wrapped = false;
527 $style_selection = false;
528 if ($param_config['inline'] ?? true) {
529 // wrap the select within an additional div
530 $html .= '<div class="' . (($param_config['multiple'] ?? null) ? 'vbo-multiselect-inline-elems-wrap' : 'vbo-singleselect-inline-elems-wrap') . '">';
531 $wrapped = true;
532 $style_selection = (bool) ($param_config['multiple'] ?? null);
533 } elseif ($param_config['wrapdivcls'] ?? null) {
534 // wrap the select within a custom div
535 $html .= '<div class="' . $param_config['wrapdivcls'] . '">';
536 $wrapped = true;
537 }
538
539 // obtain the necessary HTML code for rendering
540 $html .= VikBooking::getVboApplication()->renderElementsDropDown([
541 'id' => $element_id,
542 'placeholder' => ($param_config['asset_options']['placeholder'] ?? null),
543 'allow_clear' => ($param_config['asset_options']['allowClear'] ?? $param_config['asset_options']['allow_clear'] ?? null),
544 'attributes' => $elements_attr,
545 'element_def_img_uri' => ($param_config['element_def_img_uri'] ?? ''),
546 'style_selection' => ($param_config['style_selection'] ?? $style_selection),
547 'selected_value' => (is_scalar($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_scalar($default_paramv ?? null) ? $default_paramv : null)),
548 'selected_values' => (is_array($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_array($default_paramv ?? null) ? $default_paramv : null)),
549 ], (array) ($param_config['elements'] ?? []), (array) ($param_config['groups'] ?? []));
550
551 if ($wrapped) {
552 // close the select div wrapper
553 $html .= '</div>';
554 }
555 break;
556 case 'tags':
557 // build attributes list
558 $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
559 $elements_attr = [
560 'name' => $this->inputName . '[' . $param_name . ']',
561 ];
562 if ($param_config['multiple'] ?? null) {
563 $elements_attr['multiple'] = 'multiple';
564 $elements_attr['name'] .= '[]';
565 }
566 $custom_attr = (array) ($param_config['attributes'] ?? []);
567 unset($custom_attr['id'], $custom_attr['name']);
568 $elements_attr = array_merge($elements_attr, $custom_attr);
569
570 $wrapped = false;
571 $style_selection = (bool) ($param_config['style_selection'] ?? null);
572 if ($param_config['inline'] ?? true) {
573 // wrap the select within an additional div
574 $html .= '<div class="' . (($param_config['multiple'] ?? null) ? 'vbo-multiselect-inline-elems-wrap' : 'vbo-singleselect-inline-elems-wrap') . '">';
575 $wrapped = true;
576 $style_selection = (bool) ($param_config['multiple'] ?? null);
577 } elseif ($param_config['wrapdivcls'] ?? null) {
578 // wrap the select within a custom div
579 $html .= '<div class="' . $param_config['wrapdivcls'] . '">';
580 $wrapped = true;
581 }
582
583 // obtain the necessary HTML code for rendering
584 $html .= VikBooking::getVboApplication()->renderTagsDropDown([
585 'id' => $element_id,
586 'placeholder' => ($param_config['asset_options']['placeholder'] ?? null),
587 'allow_clear' => ($param_config['asset_options']['allowClear'] ?? $param_config['asset_options']['allow_clear'] ?? null),
588 'attributes' => $elements_attr,
589 'selected_value' => (is_scalar($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_scalar($default_paramv ?? null) ? $default_paramv : null)),
590 'selected_values' => (is_array($this->settings[$param_name] ?? null) ? $this->settings[$param_name] : (is_array($default_paramv ?? null) ? $default_paramv : null)),
591 'style_selection' => $style_selection,
592 ], (array) ($param_config['tags'] ?? []), (array) ($param_config['groups'] ?? []));
593
594 if ($wrapped) {
595 // close the select div wrapper
596 $html .= '</div>';
597 }
598 break;
599 case 'datetime':
600 // build attributes list
601 $element_id = 'vik-dtp-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
602 $elements_attr = [
603 'name' => $this->inputName . '[' . $param_name . ']',
604 'value' => $this->settings[$param_name] ?? $default_paramv ?: '',
605 ];
606 $custom_attr = (array) ($param_config['attributes'] ?? []);
607 unset($custom_attr['id'], $custom_attr['name'], $custom_attr['value']);
608 $elements_attr = array_merge($elements_attr, $custom_attr);
609
610 // obtain the necessary HTML code for rendering
611 $html .= VikBooking::getVboApplication()->renderDateTimePicker([
612 'id' => $element_id,
613 'attributes' => $elements_attr,
614 ]);
615 break;
616 case 'time':
617 // build attributes list
618 $element_id = 'vik-tp-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
619 $elements_attr = [
620 'name' => $this->inputName . '[' . $param_name . ']',
621 'value' => $this->settings[$param_name] ?? $default_paramv ?: '',
622 ];
623 $custom_attr = (array) ($param_config['attributes'] ?? []);
624 unset($custom_attr['id'], $custom_attr['name'], $custom_attr['value']);
625 $elements_attr = array_merge($elements_attr, $custom_attr);
626
627 // obtain the necessary HTML code for rendering
628 $html .= VikBooking::getVboApplication()->renderTimePicker([
629 'id' => $element_id,
630 'attributes' => $elements_attr,
631 ]);
632 break;
633 case 'password':
634 $html .= '<div class="btn-wrapper input-append">';
635 $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 . '/>';
636 $html .= '<button type="button" class="btn btn-primary" onclick="vboParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
637 $html .= '</div>';
638 // set flag for JS helper
639 $this->scripts[] = $param_config['type'];
640 break;
641 case 'number':
642 $number_attr = [];
643 if (isset($param_config['min'])) {
644 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $param_config['min']) . '"';
645 }
646 if (isset($param_config['max'])) {
647 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $param_config['max']) . '"';
648 }
649 if (isset($param_config['step'])) {
650 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $param_config['step']) . '"';
651 }
652 $html .= '<input type="number" 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)).'" ' . implode(' ', $number_attr) . $inp_attr . '/>';
653 break;
654 case 'textarea':
655 $html .= '<textarea name="' . $this->inputName . '[' . $param_name . ']"' . $inp_attr . '>'.(isset($this->settings[$param_name]) ? JHtml::fetch('esc_textarea', $this->settings[$param_name]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
656 break;
657 case 'visual_html':
658 $tarea_cont = isset($this->settings[$param_name]) ? JHtml::fetch('esc_textarea', $this->settings[$param_name]) : JHtml::fetch('esc_textarea', $default_paramv);
659 $tarea_attr = isset($param_config['attributes']) && is_array($param_config['attributes']) ? $param_config['attributes'] : [];
660 $editor_opts = isset($param_config['editor_opts']) && is_array($param_config['editor_opts']) ? $param_config['editor_opts'] : [];
661 $editor_btns = isset($param_config['editor_btns']) && is_array($param_config['editor_btns']) ? $param_config['editor_btns'] : [];
662 $html .= VikBooking::getVboApplication()->renderVisualEditor($this->inputName . '[' . $param_name . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
663 break;
664 case 'codemirror':
665 $editor = JEditor::getInstance('codemirror');
666 $e_options = isset($param_config['options']) && is_array($param_config['options']) ? $param_config['options'] : [];
667 $e_name = $this->inputName . '[' . $param_name . ']';
668 $e_value = isset($this->settings[$param_name]) ? $this->settings[$param_name] : $default_paramv;
669 $e_width = isset($e_options['width']) ? $e_options['width'] : '100%';
670 $e_height = isset($e_options['height']) ? $e_options['height'] : 300;
671 $e_col = isset($e_options['col']) ? $e_options['col'] : 70;
672 $e_row = isset($e_options['row']) ? $e_options['row'] : 20;
673 $e_buttons = isset($e_options['buttons']) ? (bool)$e_options['buttons'] : true;
674 $e_id = isset($e_options['id']) ? $e_options['id'] : $this->inputName . '_' . $param_name;
675 $e_params = isset($e_options['params']) && is_array($e_options['params']) ? $e_options['params'] : [];
676 if (interface_exists('Throwable')) {
677 /**
678 * With PHP >= 7 supporting throwable exceptions for Fatal Errors
679 * we try to avoid issues with third party plugins that make use
680 * of the WP native function get_current_screen().
681 *
682 * @wponly
683 */
684 try {
685 $html .= $editor->display($e_name, $e_value, $e_width, $e_height, $e_col, $e_row, $e_buttons, $e_id, $e_asset = null, $e_autor = null, $e_params);
686 } catch (Throwable $t) {
687 $html .= $t->getMessage() . ' in ' . $t->getFile() . ':' . $t->getLine() . '<br/>';
688 $html .= '<textarea name="' . $this->inputName . '[' . $param_name . ']"' . $inp_attr . '>' . (isset($this->settings[$param_name]) ? JHtml::fetch('esc_textarea', $this->settings[$param_name]) : JHtml::fetch('esc_textarea', $default_paramv)) . '</textarea>';
689 }
690 } else {
691 $html .= $editor->display($e_name, $e_value, $e_width, $e_height, $e_col, $e_row, $e_buttons, $e_id, $e_asset = null, $e_autor = null, $e_params);
692 }
693 break;
694 case 'hidden':
695 $html .= '<input type="hidden" 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)).'"' . $inp_attr . '/>';
696 break;
697 case 'checkbox':
698 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
699 $html .= '<input type="hidden" name="' . $this->inputName . '[' . $param_name . ']" value="0" />';
700 $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);
701 break;
702 case 'calendar':
703 $e_options = isset($param_config['options']) && is_array($param_config['options']) ? $param_config['options'] : [];
704 $e_id = isset($e_options['id']) ? $e_options['id'] : $this->inputName . '_' . $param_name;
705 $html .= VikBooking::getVboApplication()->getCalendar($this->settings[$param_name] ?? $default_paramv, $this->inputName . '['.$param_name.']', $e_id, $e_options['df'] ?? null, $e_options['attributes'] ?? []);
706 break;
707 case 'file_upload':
708 /**
709 * File upload (AJAX) field, for single or multiple files uploading.
710 *
711 * @since 1.18.3 (J) - 1.8.3 (WP)
712 */
713 $element_id = 'vik-fileupload-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
714 $element_nm = $this->inputName . '[' . $param_name . ']';
715 $multiple = '';
716 if ($param_config['multiple'] ?? null) {
717 $multiple = 'multiple';
718 $element_nm .= '[]';
719 }
720
721 // site root URI
722 $site_uri = JUri::root();
723
724 // CSRF token for safe AJAX requests
725 $csrf = addslashes(JSession::getFormToken());
726
727 // default file icon class
728 $file_icon_class = VikBookingIcons::i('file');
729
730 // JSON upload options
731 $upload_options = [
732 'element_id' => $element_id,
733 'csrf_token' => $csrf,
734 'field_name' => 'vbo_files',
735 'param_name' => $element_nm,
736 'upload_url' => VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=service.upload'),
737 'allowed_types' => (string) ($param_config['allowed_types'] ?? ''),
738 'safe_file_name' => (int) ($param_config['safe_file_name'] ?? 0),
739 'return_type' => (string) ($param_config['return_type'] ?? 'path'),
740 'file_icon_class' => $file_icon_class,
741 'loading_icon_class' => VikBookingIcons::i('circle-notch', 'fa-spin fa-fw'),
742 ];
743 $json_upload_options = json_encode($upload_options);
744
745 // upload or drag & drop text
746 $help_text = sprintf('<a href="JavaScript: void(0);">%s</a> %s', JText::translate('VBOMANUALUPLOAD'), JText::translate('VBODROPFILES'));
747
748 // uploaded files
749 $uploaded_files = array_values(array_filter((array) ($this->settings[$param_name] ?? null)));
750 $uploaded_html = '';
751 foreach ($uploaded_files as $uploaded_file) {
752 if (strpos($uploaded_file, VBO_ADMIN_PATH) !== false && !is_file($uploaded_file)) {
753 // file value is an internal path, but it no longer exists
754 continue;
755 }
756 $uploaded_file_val = htmlspecialchars((string) $uploaded_file, ENT_QUOTES, 'UTF-8');
757 $uploaded_file_name = basename((string) $uploaded_file);
758 $uploaded_file_cont = $uploaded_file_name;
759 if (strpos($uploaded_file, $site_uri) !== false) {
760 // make it a link
761 $uploaded_file_cont = '<a href="' . $uploaded_file . '" target="_blank">' . $uploaded_file_name . '</a>';
762 }
763 // render current file
764 $uploaded_html .= <<<HTML
765 <div class="file-elem">
766 <div class="file-elem-inner">
767 <div class="file-summary">
768 <i class="{$file_icon_class}"></i>
769 <div class="filename">{$uploaded_file_cont}</div>
770 <input type="hidden" name="{$element_nm}" value="{$uploaded_file_val}" data-type="file_upload" />
771 </div>
772 </div>
773 </div>
774 HTML;
775 }
776
777 if (!$uploaded_files) {
778 // display an empty input hidden element to let any conditional rule work
779 $uploaded_html = <<<HTML
780 <input type="hidden" name="{$element_nm}" value="" data-type="file_upload" data-empty="1" />
781 HTML;
782 }
783
784 // visible element
785 $html .= <<<HTML
786 <div class="vbo-param-file-upload-wrap vbo-dropfiles-target">
787 <div class="vbo-uploaded-files">{$uploaded_html}</div>
788 <div class="vbo-param-file-upload-loading"></div>
789 <div class="lead">{$help_text}</div>
790 <input type="file" id="{$element_id}" data-upload="{$multiple}" hidden {$multiple}/>
791 </div>
792 <script>
793 function vboParamFieldRenderUploads(result, options) {
794 if (!options?.inputElement) {
795 throw new Error('Missing target');
796 }
797
798 if (!result?.processed) {
799 throw new Error('No files were processed');
800 }
801
802 if (!result?.paths || !result.paths.length) {
803 alert('No valid files were uploaded');
804 return;
805 }
806
807 // define the default file-uploaded icon element class list
808 let fileIconClassList = [];
809 if (options?.file_icon_class) {
810 fileIconClassList = options.file_icon_class.split(' ');
811 }
812
813 // target the current list of files uploaded and make it empty
814 const filesPool = options.inputElement.closest('.vbo-param-file-upload-wrap').querySelector('.vbo-uploaded-files');
815 filesPool.innerHTML = '';
816
817 // iterate over each file uploaded
818 result.fileNames.forEach((name, index) => {
819 // build uploaded file nodes
820 let fileNode = document.createElement('div');
821 fileNode.classList.add('file-elem');
822 let fileInner = document.createElement('div');
823 fileInner.classList.add('file-elem-inner');
824 let fileSummary = document.createElement('div');
825 fileSummary.classList.add('file-summary');
826 let fileIcon = document.createElement('i');
827 if (fileIconClassList.length) {
828 fileIcon.classList.add(...fileIconClassList);
829 }
830 let fileName = document.createElement('div');
831 fileName.classList.add('filename');
832 fileName.innerText = name;
833 let fileInput = document.createElement('input');
834 fileInput.setAttribute('type', 'hidden');
835 fileInput.setAttribute('name', options?.param_name);
836 if (options?.return_type == 'url') {
837 fileInput.value = result.urls[index] || name;
838 // make the file name element a link
839 fileName.innerText = '';
840 let fileLink = document.createElement('a');
841 fileLink.setAttribute('href', fileInput.value);
842 fileLink.setAttribute('target', '_blank');
843 fileLink.innerText = name;
844 fileName.append(fileLink);
845 } else if (options?.return_type == 'name') {
846 fileInput.value = name;
847 } else {
848 fileInput.value = result.paths[index] || name;
849 }
850
851 // append nodes to files pool
852 fileSummary.append(fileIcon, fileName, fileInput);
853 fileInner.append(fileSummary);
854 fileNode.append(fileInner);
855 filesPool.append(fileNode);
856 });
857 }
858
859 async function vboParamFieldUploadFiles(files, options) {
860 const fieldBaseName = options?.field_name;
861 const formData = new FormData();
862 for (let i = 0; i < files.length; i++) {
863 formData.append(fieldBaseName + '[]', files[i]);
864 }
865
866 if (options?.allowed_types) {
867 // comma separated string of allowed file extension types
868 formData.append('allowed_types', options.allowed_types);
869 }
870
871 if (options?.safe_file_name) {
872 // whether to keep the original file name or randomize it
873 formData.append('safe_file_name', options.safe_file_name);
874 }
875
876 // define the default upload-loading icon element class list
877 let loadingIconClassList = [];
878 let loadingElement = null;
879 if (options?.loading_icon_class) {
880 loadingIconClassList = options.loading_icon_class.split(' ');
881 }
882
883 if (loadingIconClassList.length && options?.inputElement) {
884 // build loading icon element
885 loadingElement = document.createElement('i');
886 loadingElement.classList.add(...loadingIconClassList);
887 // append loading element
888 options
889 .inputElement
890 .closest('.vbo-param-file-upload-wrap')
891 .querySelector('.vbo-param-file-upload-loading')
892 .append(loadingElement);
893 }
894
895 try {
896 const response = await fetch(options?.upload_url, {
897 method: 'POST',
898 headers: {
899 'X-CSRF-Token': options?.csrf_token,
900 },
901 body: formData,
902 });
903
904 const result = await response.json().catch(() => null);
905
906 if (response.ok) {
907 // render files uploaded
908 vboParamFieldRenderUploads(result, options);
909 } else {
910 alert('Upload failed: ' + response.statusText);
911 }
912 } catch (error) {
913 console.error('Upload error:', error);
914 alert('An error occurred during upload.');
915 }
916
917 if (options?.inputElement) {
918 // reset file input element value to allow additional uploads
919 options.inputElement.value = '';
920 }
921
922 if (loadingElement) {
923 // remove loading animation
924 loadingElement.remove();
925 }
926 }
927
928 function vboParamFieldUploadSetup(options) {
929 // target elements
930 const fileInput = document.getElementById(options?.element_id);
931 const dropTarget = fileInput.closest('.vbo-param-file-upload-wrap');
932
933 // open file dialog by simulating the click on hidden file input
934 dropTarget.addEventListener('click', () => fileInput.click());
935
936 // drop target drag and drop events
937 dropTarget.addEventListener('dragover', e => {
938 e.preventDefault();
939 dropTarget.classList.add('drag-over', 'drag-enter');
940 });
941 dropTarget.addEventListener('dragleave', () => {
942 dropTarget.classList.remove('drag-over', 'drag-enter');
943 });
944 dropTarget.addEventListener('drop', async e => {
945 e.preventDefault();
946 dropTarget.classList.remove('drag-over', 'drag-enter');
947 const files = e.dataTransfer.files;
948 if (files.length) {
949 await vboParamFieldUploadFiles(files, Object.assign({}, options, {inputElement: fileInput}));
950 }
951 });
952
953 // input file element change event
954 fileInput.addEventListener('change', async e => {
955 if (e.target.files.length) {
956 await vboParamFieldUploadFiles(e.target.files, Object.assign({}, options, {inputElement: fileInput}));
957 }
958 });
959 }
960
961 // configure field
962 vboParamFieldUploadSetup({$json_upload_options});
963 </script>
964 HTML;
965 break;
966 default:
967 $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 . '/>';
968 break;
969 }
970
971 return $html;
972 }
973 }
974