PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 trunk, at admin/helpers/src/params/rendering.php

979 lines 45.7 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 // dispatch delayed change event
290 setTimeout(() => {
291 parent_field.dispatchEvent(new Event('change'));
292 }, 100);
293 });
294
295 });
296 </script>
297 HTML;
298 }
299
300 // JS helper functions
301 $html .= $this->getScripts();
302
303 return $html;
304 }
305
306 /**
307 * Builds the requested script assets, if any.
308 *
309 * @param bool $load_once True to unset the assets after loading.
310 *
311 * @return array
312 *
313 * @since 1.18.0 (J) - 1.8.0 (WP)
314 */
315 public function buildScriptAssets($load_once = false)
316 {
317 $scripts = [];
318
319 foreach ($this->assets as $asset_type => $asset_elements) {
320 if ($asset_type === 'select2') {
321 // build list of selectors
322 $ids_list = implode(', ', array_map(function($el) {
323 return "#{$el}";
324 }, $asset_elements));
325
326 // check for asset options
327 $asset_options = $this->assets['select2_options'] ?? null;
328 $asset_options_str = $asset_options ? json_encode($asset_options) : '';
329
330 // always attempt to load assets
331 VikBooking::getVboApplication()->loadSelect2();
332
333 // build and push script
334 $scripts[] =
335 <<<JAVASCRIPT
336 jQuery(function() {
337 jQuery('$ids_list').select2($asset_options_str);
338 });
339 JAVASCRIPT;
340 }
341
342 if ($load_once) {
343 unset($this->assets[$asset_type]);
344 }
345 }
346
347 return $scripts;
348 }
349
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 /**
367 * Gets the necessary script tags.
368 *
369 * @return string
370 */
371 public function getScripts()
372 {
373 $html = '';
374
375 if (in_array('password', $this->scripts)) {
376 // toggle the password fields
377 $html .= "\n" . '<script>' . "\n";
378 $html .= 'function vboParamTogglePwd(elem) {' . "\n";
379 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
380 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
381 $html .= ' var inp_type = inp.attr("type");' . "\n";
382 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
383 $html .= '}' . "\n";
384 $html .= "\n" . '</script>' . "\n";
385 }
386
387 return $html;
388 }
389
390 /**
391 * Renders the given param name according to config.
392 * Eventually populates the assets and scripts to be loaded.
393 *
394 * @param string $param_name The param name.
395 * @param array $param_config The param configuration.
396 *
397 * @return string
398 */
399 public function getField($param_name, $param_config)
400 {
401 $html = '';
402
403 $inp_attr = '';
404 if (isset($param_config['attributes']) && is_array($param_config['attributes'])) {
405 foreach ($param_config['attributes'] as $inpk => $inpv) {
406 $inp_attr .= $inpk . '="' . $inpv . '" ';
407 }
408 $inp_attr = ' ' . rtrim($inp_attr);
409 }
410
411 $default_paramv = $param_config['default'] ?? null;
412
413 switch ($param_config['type']) {
414 case 'custom':
415 $html .= $param_config['html'];
416 break;
417 case 'select':
418 $options = isset($param_config['options']) && is_array($param_config['options']) ? $param_config['options'] : [];
419 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
420 $element_id = 'vik-select-' . static::$instance_counter . '-' . preg_replace("/[^A-Z0-9]+/i", '', $param_name);
421 $set_attr = true;
422 if (isset($param_config['attributes']) && is_array($param_config['attributes']) && isset($param_config['attributes']['id'])) {
423 $element_id = $param_config['attributes']['id'];
424 $set_attr = false;
425 }
426 if (isset($param_config['assets']) && $param_config['assets']) {
427 if (!isset($this->assets['select2'])) {
428 $this->assets['select2'] = [];
429 }
430 $this->assets['select2'][] = $element_id;
431 $this->assets['select2_options'] = $param_config['asset_options'] ?? null;
432 }
433 if (isset($param_config['multiple']) && $param_config['multiple']) {
434 $html .= '<select name="' . $this->inputName . '[' . $param_name . '][]" multiple="multiple"' . $inp_attr . ($set_attr ? ' id="' . $element_id . '"' : '') . '>' . "\n";
435 } else {
436 $html .= '<select name="' . $this->inputName . '[' . $param_name . ']"' . $inp_attr . ($set_attr ? ' id="' . $element_id . '"' : '') . '>' . "\n";
437 }
438 foreach ($options as $optind => $optval) {
439 // support nested array values for the option-group tags
440 $group = null;
441 $sel_opts = [$optind => $optval];
442 if (is_array($optval)) {
443 $group = $optind;
444 $sel_opts = $optval;
445 }
446 if ($group) {
447 $html .= '<optgroup label="' . JHtml::fetch('esc_attr', JText::translate($group)) . '">' . "\n";
448 }
449 foreach ($sel_opts as $optkey => $poption) {
450 $checkval = $is_assoc ? $optkey : $poption;
451 $selected = false;
452 if (isset($this->settings[$param_name])) {
453 if (is_array($this->settings[$param_name])) {
454 $selected = in_array($checkval, $this->settings[$param_name]);
455 } else {
456 $selected = ($checkval == $this->settings[$param_name]);
457 }
458 } elseif (isset($default_paramv)) {
459 if (is_array($default_paramv)) {
460 $selected = in_array($checkval, $default_paramv);
461 } else {
462 $selected = ($checkval == $default_paramv);
463 }
464 }
465 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>' . "\n";
466 }
467 if ($group) {
468 $html .= '</optgroup>' . "\n";
469 }
470 }
471 $html .= '</select>' . "\n";
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;
638 case 'password':
639 $html .= '<div class="btn-wrapper input-append">';
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 . '/>';
641 $html .= '<button type="button" class="btn btn-primary" onclick="vboParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
642 $html .= '</div>';
643 // set flag for JS helper
644 $this->scripts[] = $param_config['type'];
645 break;
646 case 'number':
647 $number_attr = [];
648 if (isset($param_config['min'])) {
649 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $param_config['min']) . '"';
650 }
651 if (isset($param_config['max'])) {
652 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $param_config['max']) . '"';
653 }
654 if (isset($param_config['step'])) {
655 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $param_config['step']) . '"';
656 }
657 $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 . '/>';
658 break;
659 case 'textarea':
660 $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>';
661 break;
662 case 'visual_html':
663 $tarea_cont = isset($this->settings[$param_name]) ? JHtml::fetch('esc_textarea', $this->settings[$param_name]) : JHtml::fetch('esc_textarea', $default_paramv);
664 $tarea_attr = isset($param_config['attributes']) && is_array($param_config['attributes']) ? $param_config['attributes'] : [];
665 $editor_opts = isset($param_config['editor_opts']) && is_array($param_config['editor_opts']) ? $param_config['editor_opts'] : [];
666 $editor_btns = isset($param_config['editor_btns']) && is_array($param_config['editor_btns']) ? $param_config['editor_btns'] : [];
667 $html .= VikBooking::getVboApplication()->renderVisualEditor($this->inputName . '[' . $param_name . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
668 break;
669 case 'codemirror':
670 $editor = JEditor::getInstance('codemirror');
671 $e_options = isset($param_config['options']) && is_array($param_config['options']) ? $param_config['options'] : [];
672 $e_name = $this->inputName . '[' . $param_name . ']';
673 $e_value = isset($this->settings[$param_name]) ? $this->settings[$param_name] : $default_paramv;
674 $e_width = isset($e_options['width']) ? $e_options['width'] : '100%';
675 $e_height = isset($e_options['height']) ? $e_options['height'] : 300;
676 $e_col = isset($e_options['col']) ? $e_options['col'] : 70;
677 $e_row = isset($e_options['row']) ? $e_options['row'] : 20;
678 $e_buttons = isset($e_options['buttons']) ? (bool)$e_options['buttons'] : true;
679 $e_id = isset($e_options['id']) ? $e_options['id'] : $this->inputName . '_' . $param_name;
680 $e_params = isset($e_options['params']) && is_array($e_options['params']) ? $e_options['params'] : [];
681 if (interface_exists('Throwable')) {
682 /**
683 * With PHP >= 7 supporting throwable exceptions for Fatal Errors
684 * we try to avoid issues with third party plugins that make use
685 * of the WP native function get_current_screen().
686 *
687 * @wponly
688 */
689 try {
690 $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);
691 } catch (Throwable $t) {
692 $html .= $t->getMessage() . ' in ' . $t->getFile() . ':' . $t->getLine() . '<br/>';
693 $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>';
694 }
695 } else {
696 $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);
697 }
698 break;
699 case 'hidden':
700 $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 . '/>';
701 break;
702 case 'checkbox':
703 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
704 $html .= '<input type="hidden" name="' . $this->inputName . '[' . $param_name . ']" value="0" />';
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;
970 break;
971 default:
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 . '/>';
973 break;
974 }
975
976 return $html;
977 }
978 }
979