PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 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 All 37 releases
← All changes | admin/helpers/src/params/rendering.php +618 -22 1.6.9 → 1.8.16 View file →
@@ -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;