PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
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
vikbooking / admin / helpers / jv_helper.php

jv_helper.php in VikBooking Hotel Booking Engine & PMS 1.8.7, at admin/helpers/jv_helper.php

3,070 lines 97.1 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 - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. 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 * Extends native application functions.
15 *
16 * @wponly the class extends VikApplication and uses different vars.
17 * @since 1.0
18 * @see VikApplication
19 */
20 class VboApplication extends VikApplication
21 {
22 /**
23 * Additional commands container for any methods.
24 *
25 * @var array
26 */
27 private $commands;
28
29 /**
30 * This method loads an additional CSS file (if available)
31 * for the current CMS, and CMS version.
32 *
33 * @return void
34 **/
35 public function normalizeBackendStyles()
36 {
37 $document = JFactory::getDocument();
38
39 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'wp.css')) {
40 $document->addStyleSheet(VBO_ADMIN_URI . 'helpers/' . 'wp.css', ['version' => VIKBOOKING_SOFTWARE_VERSION], ['id' => 'vbo-wp-style']);
41 }
42 }
43
44 /**
45 * Includes a script URI.
46 *
47 * @param string $uri The script URI.
48 *
49 * @return void
50 */
51 public function addScript($uri)
52 {
53 JHtml::fetch('script', $uri);
54 }
55
56 /**
57 * Sets additional commands for any methods. Like raise an error if the recipient email address is empty.
58 * Returns this object for chainability.
59 */
60 public function setCommand($key, $value)
61 {
62 if (!empty($key)) {
63 $this->commands[$key] = $value;
64 }
65 return $this;
66 }
67
68 public function sendMail($from_address, $from_name, $to, $reply_address, $subject, $hmess, $is_html = true, $encoding = 'base64', $attachment = null)
69 {
70 if (!is_array($to) && strpos($to, ',') !== false) {
71 $all_recipients = explode(',', $to);
72 foreach ($all_recipients as $k => $v) {
73 if (empty($v)) {
74 unset($all_recipients[$k]);
75 }
76 }
77 if (count($all_recipients) > 0) {
78 $to = $all_recipients;
79 }
80 }
81
82 if (empty($to)) {
83 //Prevent Joomla Exceptions that would stop the script execution
84 if (isset($this->commands['print_errors'])) {
85 VikError::raiseWarning('', 'The recipient email address is empty. Email message could not be sent. Please check your configuration.');
86 }
87 return false;
88 }
89
90 if ($from_name == $from_address) {
91 $mainframe = JFactory::getApplication();
92 $attempt_fromn = $mainframe->get('fromname', '');
93 if (!empty($attempt_fromn)) {
94 $from_name = $attempt_fromn;
95 }
96 }
97
98 /**
99 * Conditional text rules may set extra recipients or attachments.
100 *
101 * @since 1.16.0 (J) - 1.6.0 (WP)
102 */
103 $extra_admin_recipients = VikBooking::addAdminEmailRecipient(null);
104 $bcc_addresses = VikBooking::addAdminEmailRecipient(null, $bcc = true);
105 $extra_attachments = VikBooking::addEmailAttachment(null);
106 if ($extra_admin_recipients) {
107 // cast a possible string to array
108 $to = (array) $to;
109 // merge additional recipients
110 $to = array_merge($to, $extra_admin_recipients);
111 }
112 if ($extra_attachments) {
113 $attachment = $attachment ? (array) $attachment : [];
114 $attachment = array_merge($attachment, $extra_attachments);
115 }
116
117 /**
118 * We let the internal library process the email sending depending on the platform.
119 * This will allow us to perform the required manipulation of the content, if needed.
120 *
121 * @since 1.15.2 (J) - 1.5.5 (WP)
122 */
123 $mail_data = new VBOMailWrapper([
124 'sender' => [$from_address, $from_name],
125 'recipient' => $to,
126 'bcc' => $bcc_addresses,
127 'reply' => $reply_address,
128 'subject' => $subject,
129 'content' => $hmess,
130 'attachments' => $attachment,
131 ]);
132
133 // unset queues for the next email sending operation
134 VikBooking::addAdminEmailRecipient(null, false, $reset = true);
135 VikBooking::addEmailAttachment(null, $reset = true);
136
137 // dispatch the email sending command
138 return VBOFactory::getPlatform()->getMailer()->send($mail_data);
139 }
140
141 /**
142 * @param $arr_values array
143 * @param $current_key string
144 * @param $empty_value string (J3.x only)
145 * @param $default
146 * @param $input_name string
147 * @param $record_id = '' string
148 */
149 public function getDropDown($arr_values, $current_key, $empty_value, $default, $input_name, $record_id = '')
150 {
151 $dropdown = '';
152 $dropdown .= '<select name="'.$input_name.'" onchange="document.adminForm.submit();">'."\n";
153 $dropdown .= '<option value="">'.$default.'</option>'."\n";
154 $list = "\n";
155 foreach ($arr_values as $k => $v) {
156 $dropdown .= '<option value="'.$k.'"'.($k == $current_key ? ' selected="selected"' : '').'>'.$v.'</option>'."\n";
157 }
158 $dropdown .= '</select>'."\n";
159
160 return $dropdown;
161 }
162
163 public function loadSelect2()
164 {
165 static $st_loaded = null;
166
167 if ($st_loaded) {
168 // loaded flag
169 return;
170 }
171
172 // cache loaded flag
173 $st_loaded = 1;
174
175 // load JS + CSS
176 $document = JFactory::getDocument();
177 $document->addStyleSheet(VBO_ADMIN_URI.'resources/select2.min.css');
178 $this->addScript(VBO_ADMIN_URI.'resources/select2.min.js');
179 }
180
181 /**
182 * Returns the HTML code to render a regular dropdown
183 * menu styled through the jQuery plugin Select2.
184 *
185 * @param $arr_values array
186 * @param $current_key string
187 * @param $input_name string
188 * @param $placeholder string used when the select has no selected option (it's empty)
189 * @param $empty_name [string] the name of the option to set an empty value to the field (<option>$empty_name</option>)
190 * @param $empty_val [string] the value of the option to set an empty value to the field (<option>$empty_val</option>)
191 * @param $onchange [string] javascript code for the onchange attribute
192 * @param $idattr [string] the identifier attribute of the select
193 *
194 * @return string
195 */
196 public function getNiceSelect($arr_values, $current_key, $input_name, $placeholder, $empty_name = '', $empty_val = '', $onchange = 'document.adminForm.submit();', $idattr = '')
197 {
198 //load JS + CSS
199 $this->loadSelect2();
200
201 //attribute
202 $idattr = empty($idattr) ? rand(1, 999) : $idattr;
203
204 //select
205 $dropdown = '<select id="'.$idattr.'" name="'.$input_name.'"'.(!empty($onchange) ? ' onchange="'.$onchange.'"' : '').'>'."\n";
206 if (!empty($placeholder) && empty($current_key)) {
207 //in order for the placeholder value to appear, there must be a blank <option> as the first option in the select
208 $dropdown .= '<option></option>'."\n";
209 } else {
210 //unset the placeholder to not pass it to the select2 object, or the empty value will not be displayed
211 $placeholder = '';
212 }
213 if (strlen($empty_name) || strlen($empty_val)) {
214 $dropdown .= '<option value="'.$empty_val.'">'.$empty_name.'</option>'."\n";
215 }
216 foreach ($arr_values as $k => $v) {
217 $dropdown .= '<option value="'.$k.'"'.($k == $current_key ? ' selected="selected"' : '').'>'.$v.'</option>'."\n";
218 }
219 $dropdown .= '</select>'."\n";
220
221 //js code
222 $dropdown .= '<script type="text/javascript">'."\n";
223 $dropdown .= 'jQuery(function() {'."\n";
224 $dropdown .= ' jQuery("#'.$idattr.'").select2('.(!empty($placeholder) ? '{placeholder: "'.addslashes($placeholder).'"}' : '').');'."\n";
225 $dropdown .= '});'."\n";
226 $dropdown .= '</script>'."\n";
227
228 return $dropdown;
229 }
230
231 /**
232 * Adds the script declaration to render the Bootstrap JModal window.
233 * The suffix can be passed to generate other JS functions.
234 * Optionally pass JavaScript code for the 'show' and 'hide' events.
235 * For compatibility with the Joomla framework, this method should be
236 * echoed although it does not return anything on WordPress.
237 *
238 * @param $suffix string
239 * @param $hide_js string
240 * @param $show_js string
241 *
242 * @return void should still be echoed for compatibility with J.
243 */
244 public function getJmodalScript($suffix = '', $hide_js = '', $show_js = '')
245 {
246 static $loaded = [];
247
248 $doc = JFactory::getDocument();
249
250 if (!isset($loaded[$suffix]))
251 {
252 $doc->addScriptDeclaration(
253 <<<JS
254 function vboOpenJModal$suffix(id, modal_url, new_title) {
255
256 var on_hide = null;
257
258 if ("$hide_js") {
259 on_hide = function() {
260 $hide_js
261 }
262 }
263
264 var on_show = null;
265
266 if ("$show_js") {
267 on_show = function() {
268 $show_js
269 }
270 }
271
272 wpOpenJModal(id, modal_url, on_show, on_hide);
273
274 if (new_title) {
275 jQuery('#jmodal-' + id + ' .modal-header h3').text(new_title);
276 }
277
278 return false;
279 }
280 JS
281 );
282
283 $loaded[$suffix] = 1;
284 }
285 }
286
287 /**
288 * Returns a safe sub-string string with the requested length, by
289 * avoiding errors for those errors not supporting multi-byte strings.
290 *
291 * @param string $text the text to apply the substr onto.
292 * @param string $len the length of the sub-string to take.
293 *
294 * @return string the portion of the string.
295 *
296 * @since 1.15.0 (J) - 1.5.0 (WP)
297 */
298 public function safeSubstr($text, $len = 3)
299 {
300 $mb_supported = function_exists('mb_substr');
301
302 if ($len < 1) {
303 return $text;
304 }
305
306 return $mb_supported ? mb_substr($text, 0, $len, 'UTF-8') : substr($text, 0, $len);
307 }
308
309 /**
310 * Prepares the document to allow JS functions to format currency values.
311 *
312 * @param array $options Optional list of currency settings.
313 *
314 * @return void
315 *
316 * @since 1.18.3 (J) - 1.8.3 (WP)
317 */
318 public function prepareJavaScriptCurrency(array $options = [])
319 {
320 static $currencyjs_loaded = null;
321
322 if ($currencyjs_loaded) {
323 // loaded flag
324 return;
325 }
326
327 // cache loaded flag
328 $currencyjs_loaded = 1;
329
330 // get currency formatting options
331 list($currency_digits, $currency_decimals, $currency_thousands) = explode(':', VikBooking::getNumberFormatData());
332
333 // default configuration
334 $config = [
335 'symbol' => VikBooking::getCurrencySymb() ?: '$',
336 'position' => VikBooking::getCurrencyPosition(),
337 'digits' => (int) $currency_digits,
338 'decimals' => $currency_decimals ?: '.',
339 'thousands' => $currency_thousands ?: ',',
340 'noDecimals' => 1,
341 ];
342
343 // merge settings and encode them in JSON
344 $config = json_encode(array_merge($config, $options));
345
346 // build script declaration
347 $decl = <<<JAVASCRIPT
348 VBOCore.DOMLoaded(() => {
349 VBOCore.getCurrency($config);
350 });
351 JAVASCRIPT;
352
353 // add script declaration to document
354 JFactory::getDocument()->addScriptDeclaration($decl);
355 }
356
357 /**
358 * Renders a dual slider element composed of two native range input
359 * elements to pick a minimum and a maximum value.
360 *
361 * @param array $options Optional list of currency settings.
362 *
363 * @return string The HTML string necessary to render the dual slider element.
364 *
365 * @since 1.18.3 (J) - 1.8.3 (WP)
366 */
367 public function renderDualSlider(array $options = [])
368 {
369 static $dual_slider_instances = -1;
370
371 // increase instance counter
372 $dual_slider_instances++;
373
374 // extra container class and suffix
375 $extra_class = $options['class'] ?? '';
376 $extra_class = $extra_class ? ' ' . $extra_class : $extra_class;
377 $suffix_elem = preg_replace('/[^a-z0-9\-_]+/i', '', (string) ($options['suffix'] ?? ''));
378
379 if (empty($options['suffix'])) {
380 // the suffix is required
381 $options['suffix'] = uniqid('ds_');
382 }
383
384 // minimum and maximum range values
385 $min_range = floor((float) ($options['min'] ?? 0));
386 $max_range = ceil((float) ($options['max'] ?? 100));
387 $max_range = $max_range <= $min_range ? ($min_range + 1) : $max_range;
388
389 // default range values
390 $min_default = (float) ($options['min_def'] ?? 0);
391 $max_default = (float) ($options['max_def'] ?? 100);
392 $max_default = $max_default < $min_default ? $min_default : $max_default;
393
394 // range steps
395 $range_diff = $max_range - $min_range;
396 if (empty($options['step']) || ($options['step'] ?? '') === 'auto') {
397 // calculate the best step value
398 if ($range_diff < 50) {
399 $step_default = 1;
400 } elseif ($range_diff < 500) {
401 $step_default = 5;
402 } elseif ($range_diff < 2000) {
403 $step_default = 10;
404 } else {
405 $step_default = 50;
406 }
407 } else {
408 $step_default = (float) ($options['step'] ?? 5);
409 }
410 $step_min = (float) ($options['step_min'] ?? $step_default);
411 $step_max = (float) ($options['step_max'] ?? $step_default);
412
413 if (empty($options['fixed_range'])) {
414 // ensure ranges allow to fullfil the first or last step for both ranges
415 if ($remainder = ($min_range % $step_min)) {
416 $min_range -= $remainder;
417 }
418 if ($remainder = ($max_range % $step_max)) {
419 $max_range += $step_max - $remainder;
420 }
421 $min_range = $min_range < 0 ? 0 : $min_range;
422 $max_range = $max_range < 0 ? 0 : $max_range;
423
424 // ensure the min/max default values can fulfill the step
425 if ($remainder = ($min_default % $step_min)) {
426 // i.e. browser behavior: step 5 - 422 goes to 420 - while 423 goes to 425
427 if ($remainder > ($step_min / 2)) {
428 $min_default -= $remainder;
429 }
430 }
431 if ($remainder = ($max_default % $step_max)) {
432 // i.e. browser behavior: step 5 - 822 goes to 820 - while 823 goes to 825
433 if ($remainder < ($step_max / 2)) {
434 $max_default += $step_max - $remainder;
435 }
436 }
437 }
438
439 // input name attributes
440 $min_name = $options['min_name'] ?? '';
441 $max_name = $options['max_name'] ?? '';
442 $min_name = $min_name ? 'name="' . $min_name . '"' : $min_name;
443 $max_name = $max_name ? 'name="' . $max_name . '"' : $max_name;
444
445 // current range and value HTML/text templates
446 $value_tpl = ((string) ($options['value_format'] ?? '')) ?: '%d - %d';
447 $value_data = sprintf($value_tpl, $min_default, $max_default);
448 $value_html = <<<HTML
449 <span class="vbo-dual-slider-range-value" data-format="{$value_tpl}">{$value_data}</span>
450 HTML;
451 $range_tpl = (string) ($options['range_tpl'] ?? '');
452 $range_html = $range_tpl ? sprintf($range_tpl, $value_html) : '';
453 if ($range_html) {
454 // enclose within fixed HTML container
455 $range_html = <<<HTML
456 <div class="vbo-dual-slider-range-current">{$range_html}</div>
457 HTML;
458 }
459
460 // build script content
461 $jscript = <<<HTML
462 <script>
463 function vboUpdateSliderTrack(trackSuffix) {
464 const dualSlider = document.getElementById('vbo-dual-slider-' + trackSuffix);
465 const minRange = dualSlider.querySelector('input[type="range"].vbo-dual-slider-range-min');
466 const maxRange = dualSlider.querySelector('input[type="range"].vbo-dual-slider-range-max');
467 const track = dualSlider.querySelector('.vbo-dual-slider-track');
468 const rangeValue = dualSlider.querySelector('.vbo-dual-slider-range-value');
469
470 let min = parseInt(minRange.value);
471 let max = parseInt(maxRange.value);
472 if (min > max) {
473 [minRange.value, maxRange.value] = [max, min];
474 }
475
476 let rangeMin = parseInt(minRange.min);
477 let rangeMax = parseInt(minRange.max);
478 let percent1 = ((minRange.value - rangeMin) / (rangeMax - rangeMin)) * 100;
479 let percent2 = ((maxRange.value - rangeMin) / (rangeMax - rangeMin)) * 100;
480
481 track.style.left = percent1 + '%';
482 track.style.width = (percent2 - percent1) + '%';
483
484 if (rangeValue) {
485 let rangeValueFormat = rangeValue.getAttribute('data-format') || '%d - %d';
486 let rangeValueString = rangeValueFormat.replace('%d', minRange.value).replace('%d', maxRange.value);
487 rangeValue.textContent = rangeValueString;
488 }
489 }
490 </script>
491 HTML;
492
493 if ($dual_slider_instances > 0) {
494 // restart the JS script content to avoid repeating the same function
495 $jscript = '';
496 }
497
498 // keep building script content
499 $jscript .= <<<HTML
500 <script>
501 var dualSlider = document.getElementById('vbo-dual-slider-{$suffix_elem}');
502 dualSlider
503 .querySelector('input[type="range"].vbo-dual-slider-range-min')
504 .addEventListener('input', () => {
505 vboUpdateSliderTrack('{$suffix_elem}');
506 });
507 dualSlider
508 .querySelector('input[type="range"].vbo-dual-slider-range-max')
509 .addEventListener('input', () => {
510 vboUpdateSliderTrack('{$suffix_elem}');
511 });
512 vboUpdateSliderTrack('{$suffix_elem}');
513 </script>
514 HTML;
515
516 // build final HTML string
517 $html = <<<HTML
518 <div class="vbo-dual-slider-container{$extra_class}" id="vbo-dual-slider-{$suffix_elem}">
519 <div class="vbo-dual-slider-wrap">
520 <div class="vbo-dual-slider-track vbo-pref-background"></div>
521 <input type="range" class="vbo-dual-slider-range vbo-dual-slider-range-min" min="{$min_range}" max="{$max_range}" value="{$min_default}" step="{$step_min}" {$min_name}/>
522 <input type="range" class="vbo-dual-slider-range vbo-dual-slider-range-max" min="{$min_range}" max="{$max_range}" value="{$max_default}" step="{$step_max}" {$max_name}/>
523 </div>
524 {$range_html}
525 </div>
526 {$jscript}
527 HTML;
528
529 // return the HTML string to be displayed
530 return $html;
531 }
532
533 /**
534 * Renders a date-time locale input element to pick a date and time.
535 *
536 * @param array $options Associative list of element options.
537 *
538 * @return string The HTML string necessary to render the date-time picker.
539 *
540 * @since 1.18.0 (J) - 1.8.0 (WP)
541 */
542 public function renderDateTimePicker(array $options = [])
543 {
544 if (!($options['id'] ?? null)) {
545 // the ID attribute is mandatory
546 $options['id'] = uniqid('dtp_');
547 }
548
549 // ensure attributes are set
550 if (!($options['attributes'] ?? [])) {
551 $options['attributes'] = [];
552 }
553
554 // attributes name, value, min and max can also be specified outside the "attributes" key
555 if (($options['name'] ?? null) && !($options['attributes']['name'] ?? null)) {
556 // resort the attribute inside the apposite key
557 $options['attributes']['name'] = $options['name'];
558 }
559 if (($options['value'] ?? null) && !($options['attributes']['value'] ?? null)) {
560 // resort the attribute inside the apposite key
561 $options['attributes']['value'] = $options['value'];
562 }
563 if (($options['min'] ?? null) && !($options['attributes']['min'] ?? null)) {
564 // resort the attribute inside the apposite key
565 $options['attributes']['min'] = $options['min'];
566 }
567 if (($options['max'] ?? null) && !($options['attributes']['max'] ?? null)) {
568 // resort the attribute inside the apposite key
569 $options['attributes']['max'] = $options['max'];
570 }
571
572 // check for "min" attribute, required to hide seconds from the time-picker
573 if (!($options['attributes']['min'] ?? null)) {
574 // default to 10 years in the past
575 $options['attributes']['min'] = JFactory::getDate('-10 years')->format('Y-m-d\TH:i');
576 }
577
578 // build attributes list
579 $attributes = array_merge([
580 'id' => $options['id'],
581 ], $options['attributes']);
582
583 // build attributes string
584 $attr_str = implode(' ', array_map(function($name, $value) {
585 return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
586 }, array_keys($attributes), array_values($attributes)));
587
588 // build HTML string
589 $html = <<<HTML
590 <input type="datetime-local" {$attr_str} />
591 HTML;
592
593 // return the HTML string to be displayed
594 return $html;
595 }
596
597 /**
598 * Renders a time input element to pick a time.
599 *
600 * @param array $options Associative list of element options.
601 *
602 * @return string The HTML string necessary to render the time picker.
603 *
604 * @since 1.18.4 (J) - 1.8.4 (WP)
605 */
606 public function renderTimePicker(array $options = [])
607 {
608 if (!($options['id'] ?? null)) {
609 // the ID attribute is mandatory
610 $options['id'] = uniqid('tp_');
611 }
612
613 // ensure attributes are set
614 if (!($options['attributes'] ?? [])) {
615 $options['attributes'] = [];
616 }
617
618 // attributes name, value, min and max can also be specified outside the "attributes" key
619 if (($options['name'] ?? null) && !($options['attributes']['name'] ?? null)) {
620 // resort the attribute inside the apposite key
621 $options['attributes']['name'] = $options['name'];
622 }
623 if (($options['value'] ?? null) && !($options['attributes']['value'] ?? null)) {
624 // resort the attribute inside the apposite key
625 $options['attributes']['value'] = $options['value'];
626 }
627 if (($options['min'] ?? null) && !($options['attributes']['min'] ?? null)) {
628 // resort the attribute inside the apposite key
629 $options['attributes']['min'] = $options['min'];
630 }
631 if (($options['max'] ?? null) && !($options['attributes']['max'] ?? null)) {
632 // resort the attribute inside the apposite key
633 $options['attributes']['max'] = $options['max'];
634 }
635 if (($options['step'] ?? null) && !($options['attributes']['step'] ?? null)) {
636 // resort the attribute inside the apposite key
637 $options['attributes']['step'] = $options['step'];
638 }
639
640 // build attributes list
641 $attributes = array_merge([
642 'id' => $options['id'],
643 ], $options['attributes']);
644
645 // build attributes string
646 $attr_str = implode(' ', array_map(function($name, $value) {
647 return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
648 }, array_keys($attributes), array_values($attributes)));
649
650 // build HTML string
651 $html = <<<HTML
652 <input type="time" {$attr_str} />
653 HTML;
654
655 // return the HTML string to be displayed
656 return $html;
657 }
658
659 /**
660 * Renders a select2 component to display existing tags or to add new ones.
661 *
662 * @param array $options Associative list of dropdown options.
663 * @param array $elements Associative list of element records.
664 * @param array $groups Optional list of element groups to source.
665 *
666 * @return string The HTML string necessary to render the dropdown.
667 *
668 * @since 1.18.0 (J) - 1.8.0 (WP)
669 */
670 public function renderTagsDropDown(array $options = [], array $elements = [], array $groups = [])
671 {
672 // load select2 assets
673 $this->loadSelect2();
674
675 if (!($options['id'] ?? null)) {
676 // the ID attribute is mandatory
677 $options['id'] = uniqid('tdd_');
678 }
679
680 // build attributes list
681 $attributes = array_merge([
682 'id' => $options['id'],
683 ], ($options['attributes'] ?? []));
684
685 // build attributes string
686 $attr_str = implode(' ', array_map(function($name, $value) {
687 return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
688 }, array_keys($attributes), array_values($attributes)));
689
690 // build data sources
691 $data_sources = [];
692
693 foreach ($elements as $element) {
694 if (is_object($element)) {
695 $element = (array) $element;
696 }
697
698 if (empty($element['id'])) {
699 continue;
700 }
701
702 // build element data source
703 $data_source = [
704 'id' => $element['id'],
705 'text' => $element['name'] ?? $element['id'],
706 'color' => $element['color'] ?? null,
707 'hex' => $element['hex'] ?? null,
708 ];
709
710 // check for option selected status
711 if (($options['selected_value'] ?? null) && $options['selected_value'] == $element['id']) {
712 $data_source['selected'] = true;
713 } elseif (is_array($options['selected_values'] ?? null) && in_array($element['id'], $options['selected_values'])) {
714 $data_source['selected'] = true;
715 }
716
717 // check for option disabled status
718 if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $element['id']) {
719 $data_source['disabled'] = true;
720 } elseif (is_array($options['disabled_values'] ?? null) && in_array($element['id'], $options['disabled_values'])) {
721 $data_source['disabled'] = true;
722 }
723
724 // push element data source
725 $data_sources[] = $data_source;
726 }
727
728 // append groups to source as data elements
729 foreach ($groups as $group) {
730 if (is_object($group)) {
731 // always cast to array
732 $group = (array) $group;
733 }
734
735 if (!is_array($group) || empty($group['text']) || empty($group['elements'])) {
736 continue;
737 }
738
739 // filter out invalid group elements
740 $group['elements'] = array_filter((array) $group['elements'], function($group_element) {
741 return is_array($group_element) && isset($group_element['id']) && isset($group_element['text']);
742 });
743
744 // check for option selected status
745 if (($options['selected_value'] ?? null) || (is_array($options['selected_values'] ?? null) && $options['selected_values'])) {
746 foreach ($group['elements'] as $k => $element) {
747 if (($options['selected_value'] ?? null)) {
748 if ($options['selected_value'] == $element['id']) {
749 $group['elements'][$k]['selected'] = true;
750 }
751 } else {
752 if (in_array($element['id'], $options['selected_values'])) {
753 $group['elements'][$k]['selected'] = true;
754 }
755 }
756 }
757 }
758
759 // check for option disabled status
760 if (($options['disabled_value'] ?? null) || (is_array($options['disabled_values'] ?? null) && $options['disabled_values'])) {
761 foreach ($group['elements'] as $k => $element) {
762 if (($options['disabled_value'] ?? null)) {
763 if ($options['disabled_value'] == $element['id']) {
764 $group['elements'][$k]['disabled'] = true;
765 }
766 } else {
767 if (in_array($element['id'], $options['disabled_values'])) {
768 $group['elements'][$k]['disabled'] = true;
769 }
770 }
771 }
772 }
773
774 // push group element data source
775 $data_sources[] = [
776 'text' => $group['text'],
777 'children' => $group['elements'],
778 ];
779 }
780
781 // data sources JSON encoded string
782 $data_sources_str = json_encode($data_sources);
783
784 // empty option tag
785 $empty_option = '';
786 if (!($options['attributes']['multiple'] ?? null)) {
787 $empty_option = '<option></option>';
788 }
789
790 // clearing allowed
791 $clearable = (bool) ($options['allow_clear'] ?? 1);
792 $clearable_str = $clearable ? 'true' : 'false';
793
794 // tags allowed (for entering custom values)
795 $taggable = (bool) ($options['allow_tags'] ?? 1);
796 $taggable_str = $taggable ? 'true' : 'false';
797
798 // placeholder text
799 $placeholder = json_encode($options['placeholder'] ?? '');
800
801 // select2 width
802 $sel2width = $options['width'] ?? 'resolve';
803
804 // supported tag colors
805 $colors = json_encode((array) ($options['colors'] ?? []));
806
807 // build HTML string
808 $html = <<<HTML
809 <select {$attr_str}>{$empty_option}</select>
810 HTML;
811
812 // build script declaration
813 $js_decl = <<<JAVASCRIPT
814 jQuery(function() {
815 const supportedColors = $colors;
816 let remainingColors = supportedColors.slice();
817
818 if (remainingColors.length) {
819 // internally rearrange tags by ID to preserve the linear color scheme supported by default
820 const existingTags = {$data_sources_str}.sort((a, b) => parseInt(a.id) - parseInt(b.id));
821
822 // iterate all the existing tags
823 existingTags.forEach((tag) => {
824 let index = remainingColors.indexOf(tag.color);
825 if (index != -1) {
826 // remove the tag color from the remaining ones
827 remainingColors.splice(index, 1);
828
829 if (remainingColors.length == 0) {
830 // no more remaining colors, reset array
831 remainingColors = supportedColors.slice();
832 }
833 }
834 });
835 }
836
837 jQuery('select#{$options['id']}').select2({
838 width: '$sel2width',
839 allowClear: $clearable_str,
840 data: $data_sources_str,
841 placeholder: $placeholder,
842 tags: $taggable_str,
843 createTag: function (params) {
844 const term = (params.term || '').replace(/:/g, '').trim();
845
846 if (term === '') {
847 return null;
848 }
849
850 // temporarily assign the first available color
851 const color = remainingColors[0];
852
853 return {
854 id: term + ':' + color,
855 text: term,
856 color: color,
857 newTag: true,
858 };
859 },
860 templateResult: (element) => {
861 if (!element.id) {
862 return element.text;
863 }
864
865 let tag_class = '';
866 let tag_style = '';
867 if (element?.color) {
868 tag_class = element.color;
869 } else if (element?.hex) {
870 tag_style = 'background-color: ' + element.hex + ';';
871 } else {
872 tag_class = (element.id + '').toLowerCase().replace(/[^a-z0-9]/ig, '');
873 }
874 return jQuery('<span class="vbo-sel2-selectable-tag"><span class="vbo-sel2-selectable-tag-color vbo-colortag-circle' + (tag_class ? ' ' + tag_class : '') + '"' + (tag_style ? ' style="' + tag_style + '"' : '') + '></span><span class="vbo-sel2-selectable-tag-name">' + element.text + '</span></span>');
875 },
876 templateSelection: (element) => {
877 if (!element.id) {
878 return element.text;
879 }
880
881 let tag_elem = jQuery('<span></span>')
882 .addClass('vbo-sel2-selected-tag')
883 .text(element.text);
884
885 if (element.newTag) {
886 // we can understand here whether a new tag has been officially submitted
887 element.newTag = false;
888
889 // permanently detach the last color assigned
890 remainingColors.shift();
891
892 if (remainingColors.length == 0) {
893 // no more remaining colors, reset array
894 remainingColors = supportedColors.slice();
895 }
896 }
897
898 if (element?.color) {
899 tag_elem.addClass(element.color);
900 } else if (element?.hex) {
901 tag_elem.css('background-color', element.hex);
902 } else {
903 tag_elem.addClass((element.id + '').toLowerCase().replace(/[^a-z0-9]/ig, ''));
904 }
905
906 return tag_elem;
907 },
908 });
909 });
910 JAVASCRIPT;
911
912 if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
913 // concatenate script to HTML string when doing an AJAX request
914 $html .= "\n" . '<script>' . $js_decl . '</script>';
915 } else {
916 // add script declaration to document
917 JFactory::getDocument()->addScriptDeclaration($js_decl);
918 }
919
920 // return the HTML string to be displayed
921 return $html;
922 }
923
924 /**
925 * Renders a select2 component to display elements with thumbnails.
926 *
927 * @param array $options Associative list of dropdown options.
928 * @param array $elements Associative list of element records.
929 * @param array $groups Optional list of element groups to source.
930 *
931 * @return string The HTML string necessary to render the dropdown.
932 *
933 * @since 1.17.5 (J) - 1.7.5 (WP)
934 * @since 1.18.7 (J) - 1.8.7 (WP) added support to elements "listings" with sub-units.
935 */
936 public function renderElementsDropDown(array $options = [], array $elements = [], array $groups = [])
937 {
938 if (!$elements && ($options['elements'] ?? '') == 'listings') {
939 // load listing records
940 $filter_listing_ids = (array) ($options['element_ids'] ?? []);
941 $elements = array_values(VikBooking::getAvailabilityInstance(true)->loadRooms($filter_listing_ids, 0, true));
942
943 if (($options['subunits'] ?? [])) {
944 // check if any of the listings involved is using a room-type (hotel) inventory
945 $hotelInventoryListingIds = array_column(array_filter($elements, function($roomRecord) {
946 return ($roomRecord['units'] ?? 0) > 1;
947 }), 'id');
948
949 // determine the value format for the sub-unit IDs
950 $subunitValueFormat = $options['subunits']['value_format'] ?? '%d-%d';
951
952 foreach ($hotelInventoryListingIds as $hotelInventoryListingId) {
953 foreach ($elements as $elIndex => $element) {
954 if ($element['id'] != $hotelInventoryListingId) {
955 // single-unit (or previous sub-unit) listing identified
956 continue;
957 }
958
959 // clone first element for sub-unit
960 $subunitEl = $element;
961
962 if (!($options['subunits']['entire_listing'] ?? true)) {
963 // the entire listing should no longer be included
964 unset($elements[$elIndex]);
965 $elIndex -= 1;
966 } else {
967 // modify the "entire listing" name ("All" by default)
968 $nameSuffix = (string) ($options['subunits']['listing_suffix'] ?? sprintf(' (%s)', strtolower(JText::translate('VBNEWCOUPONEIGHT'))));
969 $elements[$elIndex]['name'] .= $nameSuffix;
970 }
971
972 // handle sub-unit listing element to insert
973 $counter = 1;
974 for ($u = 1; $u <= $element['units']; $u++) {
975 // set sub-unit ID and name
976 $subunitEl['id'] = sprintf($subunitValueFormat, $hotelInventoryListingId, $u);
977 $subunitEl['name'] = $element['name'] . sprintf(' #%d', $u);
978 $subunitEl['_nested'] = true;
979
980 // insert the sub-unit element array next to the parent listing
981 array_splice($elements, $elIndex + $counter, 0, [$subunitEl]);
982
983 // increase counter
984 $counter++;
985 }
986 }
987 }
988 }
989 }
990
991 if (!$elements && !$groups) {
992 // abort
993 return '';
994 }
995
996 // load select2 assets
997 $this->loadSelect2();
998
999 if (!($options['id'] ?? null)) {
1000 // the ID attribute is mandatory
1001 $options['id'] = uniqid('ldd_');
1002 }
1003
1004 if ($options['attributes']['multiple'] ?? null) {
1005 // inject custom data attribute to prevent flashing via CSS during JS mutation
1006 $options['attributes']['data-will-mutate'] = 1;
1007 }
1008
1009 // build attributes list
1010 $attributes = array_merge([
1011 'id' => $options['id'],
1012 ], ($options['attributes'] ?? []));
1013
1014 // build attributes string
1015 $attr_str = implode(' ', array_map(function($name, $value) {
1016 return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
1017 }, array_keys($attributes), array_values($attributes)));
1018
1019 // build data sources
1020 $data_sources = [];
1021 $base_img_path = implode(DIRECTORY_SEPARATOR, [VBO_SITE_PATH, 'resources', 'uploads']) . DIRECTORY_SEPARATOR;
1022 $base_img_uri = VBO_SITE_URI . 'resources/uploads/';
1023
1024 foreach ($elements as $element) {
1025 if (is_object($element)) {
1026 $element = (array) $element;
1027 }
1028
1029 if (empty($element['id'])) {
1030 continue;
1031 }
1032
1033 // element image
1034 $element_img_uri = null;
1035
1036 if (($options['elements'] ?? '') == 'listings') {
1037 // check for listing mini-thumbnail
1038 if (!empty($element['img']) && is_file($base_img_path . 'mini_' . $element['img'])) {
1039 $element_img_uri = $base_img_uri . 'mini_' . $element['img'];
1040 }
1041 }
1042
1043 if (!$element_img_uri && ($options['element_def_img_uri'] ?? '')) {
1044 // use the provided default element image
1045 $element_img_uri = $options['element_def_img_uri'];
1046 } elseif (!$element_img_uri && ($element['img_uri'] ?? '')) {
1047 // use the provided element image URI
1048 $element_img_uri = $element['img_uri'];
1049 }
1050
1051 // build element data source
1052 $data_source = [
1053 'id' => $element['id'],
1054 'text' => $element['name'] ?? $element['id'],
1055 'img' => $element_img_uri,
1056 ];
1057
1058 if (!$element_img_uri && !empty($element['html'])) {
1059 // set element HTML (icon) content
1060 $data_source['html'] = $element['html'];
1061 }
1062
1063 if (!empty($element['_nested'])) {
1064 // set element nested flag
1065 $data_source['_nested'] = 1;
1066 }
1067
1068 // check for option selected status
1069 if (($options['selected_value'] ?? null) && $options['selected_value'] == $element['id']) {
1070 $data_source['selected'] = true;
1071 } elseif (is_array($options['selected_values'] ?? null) && in_array($element['id'], $options['selected_values'])) {
1072 $data_source['selected'] = true;
1073 }
1074
1075 // check for option disabled status
1076 if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $element['id']) {
1077 $data_source['disabled'] = true;
1078 } elseif (is_array($options['disabled_values'] ?? null) && in_array($element['id'], $options['disabled_values'])) {
1079 $data_source['disabled'] = true;
1080 }
1081
1082 // push element data source
1083 $data_sources[] = $data_source;
1084 }
1085
1086 // check for listing category groups
1087 if (($options['elements'] ?? '') == 'listings' && ($options['load_categories'] ?? null)) {
1088 // load listing categories
1089 $categories = VikBooking::getAvailabilityInstance(true)->loadRoomCategories();
1090
1091 if (count($categories) > 1) {
1092 // turn the category IDs into negative
1093 $categories = array_map(function($cat) {
1094 return [
1095 'id' => ($cat['id'] - ($cat['id'] * 2)),
1096 'text' => $cat['name'],
1097 ];
1098 }, $categories);
1099
1100 // active or disabled listing categories
1101 foreach ($categories as &$category) {
1102 // check for option selected status
1103 if (($options['selected_value'] ?? null) && $options['selected_value'] == $category['id']) {
1104 $category['selected'] = true;
1105 } elseif (is_array($options['selected_values'] ?? null) && in_array($category['id'], $options['selected_values'])) {
1106 $category['selected'] = true;
1107 }
1108
1109 // check for option disabled status
1110 if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $category['id']) {
1111 $category['disabled'] = true;
1112 } elseif (is_array($options['disabled_values'] ?? null) && in_array($category['id'], $options['disabled_values'])) {
1113 $category['disabled'] = true;
1114 }
1115 }
1116 unset($category);
1117
1118 // push listing category groups
1119 $groups[] = [
1120 'text' => $options['categories_lbl'] ?? 'Filter by category',
1121 'elements' => $categories,
1122 ];
1123 }
1124 }
1125
1126 // append groups to source as data elements
1127 foreach ($groups as $group) {
1128 if (is_object($group)) {
1129 // always cast to array
1130 $group = (array) $group;
1131 }
1132
1133 if (!is_array($group) || empty($group['text']) || empty($group['elements'])) {
1134 continue;
1135 }
1136
1137 // filter out invalid group elements
1138 $group['elements'] = array_filter((array) $group['elements'], function($group_element) {
1139 return is_array($group_element) && isset($group_element['id']) && isset($group_element['text']);
1140 });
1141
1142 // check for option selected status
1143 if (($options['selected_value'] ?? null) || (is_array($options['selected_values'] ?? null) && $options['selected_values'])) {
1144 foreach ($group['elements'] as $k => $element) {
1145 if (($options['selected_value'] ?? null)) {
1146 if ($options['selected_value'] == $element['id']) {
1147 $group['elements'][$k]['selected'] = true;
1148 }
1149 } else {
1150 if (in_array($element['id'], $options['selected_values'])) {
1151 $group['elements'][$k]['selected'] = true;
1152 }
1153 }
1154 }
1155 }
1156
1157 // check for option disabled status
1158 if (($options['disabled_value'] ?? null) || (is_array($options['disabled_values'] ?? null) && $options['disabled_values'])) {
1159 foreach ($group['elements'] as $k => $element) {
1160 if (($options['disabled_value'] ?? null)) {
1161 if ($options['disabled_value'] == $element['id']) {
1162 $group['elements'][$k]['disabled'] = true;
1163 }
1164 } else {
1165 if (in_array($element['id'], $options['disabled_values'])) {
1166 $group['elements'][$k]['disabled'] = true;
1167 }
1168 }
1169 }
1170 }
1171
1172 // push group element data source
1173 $data_sources[] = [
1174 'text' => $group['text'],
1175 'children' => $group['elements'],
1176 ];
1177 }
1178
1179 // data sources JSON encoded string
1180 $data_sources_str = json_encode($data_sources);
1181
1182 // empty option tag
1183 $empty_option = '';
1184 if (!($options['attributes']['multiple'] ?? null)) {
1185 $empty_option = '<option></option>';
1186 }
1187
1188 // clearing allowed
1189 $clearable = (bool) ($options['allow_clear'] ?? 1);
1190 $clearable_str = $clearable ? 'true' : 'false';
1191
1192 // placeholder text
1193 $placeholder = json_encode($options['placeholder'] ?? '');
1194
1195 // select2 width
1196 $sel2width = $options['width'] ?? 'resolve';
1197
1198 // build HTML string
1199 $html = <<<HTML
1200 <select {$attr_str}>{$empty_option}</select>
1201 HTML;
1202
1203 // template selection function
1204 $selectionFn = '';
1205 if ($options['style_selection'] ?? null) {
1206 $defaultSelectionIcn = $options['default_selection_icon'] ?? '';
1207 $selectionFn = <<<JAVASCRIPT
1208 templateSelection: (element) => {
1209 if (!element.id) {
1210 return element.text;
1211 }
1212 let sel_elem = jQuery('<span></span>')
1213 .addClass('vbo-sel2-selected-tag')
1214 .text(element.text);
1215 if (element.img) {
1216 let avatar_elem = jQuery('<img/>')
1217 .addClass('vbo-sel2-selected-tag-avatar')
1218 .attr('src', element.img);
1219 sel_elem.prepend(avatar_elem);
1220 } else if ('$defaultSelectionIcn') {
1221 let icn_elem = jQuery('<i></i>')
1222 .addClass('$defaultSelectionIcn')
1223 .addClass('vbo-sel2-selected-tag-avatar');
1224 sel_elem.prepend(icn_elem);
1225 }
1226 return sel_elem;
1227 }
1228 JAVASCRIPT;
1229 }
1230
1231 // build script declaration
1232 $js_decl = <<<JAVASCRIPT
1233 jQuery(function() {
1234 jQuery('select#{$options['id']}').select2({
1235 width: '$sel2width',
1236 allowClear: $clearable_str,
1237 data: $data_sources_str,
1238 placeholder: $placeholder,
1239 templateResult: (element) => {
1240 let isNested = element?._nested;
1241 if (element.img) {
1242 return jQuery('<span class="vbo-sel2-element-img' + (isNested ? ' vbo-sel2-element-nested' : '') + '"><img src="' + element.img + '" /> <span>' + element.text + '</span></span>');
1243 }
1244 if (element.html) {
1245 return jQuery('<span class="vbo-sel2-element-img' + (isNested ? ' vbo-sel2-element-nested' : '') + '">' + element.html + ' <span>' + element.text + '</span></span>');
1246 }
1247 if (isNested) {
1248 return jQuery('<span class="vbo-sel2-element-nested"><span>' + element.text + '</span></span>');
1249 }
1250 return element.text;
1251 },
1252 $selectionFn
1253 });
1254 });
1255 JAVASCRIPT;
1256
1257 if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
1258 // concatenate script to HTML string when doing an AJAX request
1259 $html .= "\n" . '<script>' . $js_decl . '</script>';
1260 } else {
1261 // add script declaration to document
1262 JFactory::getDocument()->addScriptDeclaration($js_decl);
1263 }
1264
1265 // return the HTML string to be displayed
1266 return $html;
1267 }
1268
1269 /**
1270 * Renders a select2 component to display searchable elements with thumbnails.
1271 *
1272 * @param array $options Associative list of dropdown and search options.
1273 *
1274 * @return string The HTML string necessary to render the dropdown.
1275 *
1276 * @since 1.18.0 (J) - 1.8.0 (WP)
1277 */
1278 public function renderSearchElementsDropDown(array $options = [])
1279 {
1280 if (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'bookings') {
1281 // search bookings endpoint
1282 $options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.bookings_search');
1283 } elseif (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'customers') {
1284 // search customers endpoint
1285 $options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.customer_elements_search');
1286 }
1287
1288 if (empty($options['endpoint'])) {
1289 // abort
1290 return '';
1291 }
1292
1293 // set AJAX endpoint for searching
1294 $endpoint = $options['endpoint'];
1295
1296 if ($options['load_assets'] ?? true) {
1297 // load select2 assets
1298 $this->loadSelect2();
1299 }
1300
1301 if (!($options['id'] ?? null)) {
1302 // the ID attribute is mandatory
1303 $options['id'] = uniqid('ldd_');
1304 }
1305
1306 // build attributes list
1307 $attributes = array_merge([
1308 'id' => $options['id'],
1309 ], ($options['attributes'] ?? []));
1310
1311 // build attributes string
1312 $attr_str = implode(' ', array_map(function($name, $value) {
1313 return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
1314 }, array_keys($attributes), array_values($attributes)));
1315
1316 // build data sources
1317 $data_sources = [];
1318
1319 // check for default option(s) selected status
1320 $selected_options = [];
1321 if (is_array($options['selected_values'] ?? null) && $options['selected_values']) {
1322 $selected_options = $options['selected_values'];
1323 } elseif (($options['selected_value'] ?? null)) {
1324 $selected_options[] = $options['selected_value'];
1325 }
1326 foreach ($selected_options as $sel_option) {
1327 if (is_array($sel_option) || is_object($sel_option)) {
1328 // element object expected
1329 $sel_option = (array) $sel_option;
1330 if (isset($sel_option['id']) && isset($sel_option['text'])) {
1331 // push valid data source
1332 $sel_option['selected'] = true;
1333 $data_sources[] = $sel_option;
1334 }
1335 } else {
1336 // selected ID expected, push a data source object with limited information
1337 $data_sources[] = [
1338 'id' => $sel_option,
1339 'text' => $sel_option,
1340 'selected' => true,
1341 ];
1342 }
1343 }
1344
1345 // data sources JSON encoded string
1346 $data_sources_str = json_encode($data_sources);
1347
1348 // empty option tag
1349 $empty_option = '';
1350 if (!($options['attributes']['multiple'] ?? null)) {
1351 $empty_option = '<option></option>';
1352 }
1353
1354 // clearing allowed
1355 $clearable = (bool) ($options['allow_clear'] ?? 1);
1356 $clearable_str = $clearable ? 'true' : 'false';
1357
1358 // placeholder text
1359 $placeholder = json_encode($options['placeholder'] ?? '');
1360
1361 // minimum input length for searching
1362 $min_inp_len = 1;
1363
1364 // search language definitions
1365 $lang_error_loading = json_encode($options['language']['error'] ?? JText::translate('VBO_ERR_LOAD_RESULTS'));
1366 $lang_no_results = json_encode($options['language']['noresults'] ?? JText::translate('VBO_NO_RECORDS_FOUND'));
1367 $lang_searching = json_encode($options['language']['searching'] ?? JText::translate('VBO_SEARCHING'));
1368 // this language definition is NOT quoted, because it is not JSON-encoded
1369 $lang_inptooshort = $min_inp_len > 1 && ($options['language']['inptooshort'] ?? '') ? $options['language']['inptooshort'] : '';
1370
1371 // selection/result with ID
1372 $selection_with_id = ($options['selected_id'] ?? null) ? 'true' : 'false';
1373
1374 // selection extra class
1375 $selection_class = $options['selection_class'] ?? '';
1376
1377 // selection click open widget
1378 $selection_click_widget = $options['selection_click_widget'] ?? '';
1379
1380 // selection dispatch event
1381 $selection_event = $options['selection_event'] ?? '';
1382
1383 // select2 width
1384 $sel2width = $options['width'] ?? 'resolve';
1385
1386 // build HTML string
1387 $html = <<<HTML
1388 <select {$attr_str}>{$empty_option}</select>
1389 HTML;
1390
1391 // template selection function
1392 $selectionFn = '';
1393 if ($options['style_selection'] ?? null) {
1394 $defaultSelectionIcn = $options['default_selection_icon'] ?? '';
1395 $selectionFn = <<<JAVASCRIPT
1396 templateSelection: (element) => {
1397 if (!element.id) {
1398 return element.text;
1399 }
1400 let sel_elem = jQuery('<span></span>')
1401 .addClass(('vbo-sel2-selected-search-elem $selection_class').trim())
1402 .text(element.text + ($selection_with_id ? ' #' + element.id : ''));
1403 if (element.img) {
1404 let avatar_elem = jQuery('<img/>')
1405 .addClass('vbo-sel2-selected-search-elem-avatar')
1406 .attr('src', element.img);
1407 if (element.img_title) {
1408 avatar_elem.attr('title', element.img_title);
1409 }
1410 sel_elem.prepend(avatar_elem);
1411 } else if (element.icon_class) {
1412 let icn_wrap = jQuery('<span></span>')
1413 .addClass('vbo-sel2-selected-search-elem-avatar');
1414 let icn_elem = jQuery('<i></i>')
1415 .addClass(element.icon_class);
1416 icn_wrap.append(icn_elem);
1417 sel_elem.prepend(icn_wrap);
1418 } else if ('$defaultSelectionIcn') {
1419 let icn_elem = jQuery('<i></i>')
1420 .addClass('$defaultSelectionIcn')
1421 .addClass('vbo-sel2-selected-search-elem-avatar');
1422 sel_elem.prepend(icn_elem);
1423 }
1424 if ("$selection_click_widget" == 'booking_details') {
1425 sel_elem.on('click', () => {
1426 VBOCore.handleDisplayWidgetNotification({
1427 widget_id: 'booking_details',
1428 }, {
1429 booking_id: element.id,
1430 modal_options: {
1431 suffix: 'vbo-booking-details-inner',
1432 body_prepend: false,
1433 enlargeable: false,
1434 minimizeable: false,
1435 },
1436 });
1437 });
1438 }
1439 return sel_elem;
1440 }
1441 JAVASCRIPT;
1442 }
1443
1444 // build script declaration
1445 $js_decl = <<<JAVASCRIPT
1446 jQuery(function() {
1447 jQuery('select#{$options['id']}').select2({
1448 width: '$sel2width',
1449 allowClear: $clearable_str,
1450 data: $data_sources_str,
1451 placeholder: $placeholder,
1452 minimumInputLength: $min_inp_len,
1453 language: {
1454 errorLoading: () => {
1455 return $lang_error_loading;
1456 },
1457 noResults: () => {
1458 return $lang_no_results;
1459 },
1460 searching: () => {
1461 return $lang_searching;
1462 },
1463 inputTooShort: () => {
1464 return "$lang_inptooshort";
1465 },
1466 },
1467 ajax: {
1468 delay: 350,
1469 url: "$endpoint",
1470 dataType: 'json',
1471 },
1472 templateResult: (element) => {
1473 if (!element.id) {
1474 return element.text;
1475 }
1476 let search_elem = jQuery('<span></span>')
1477 .addClass('vbo-sel2-search-elem');
1478
1479 let search_avatar = jQuery('<span></span>')
1480 .addClass('vbo-sel2-search-elem-avatar');
1481
1482 let elem_name = jQuery('<span></span>')
1483 .addClass('vbo-sel2-search-elem-name')
1484 .text(element.text + ($selection_with_id ? ' #' + element.id : ''));
1485
1486 if (element.img) {
1487 search_avatar.append('<img src="' + element.img + '" ' + (element.img_title ? 'title="' + element.img_title + '" ' : '') + '/>');
1488 } else if (element.icon_class) {
1489 search_avatar.append('<i class="' + element.icon_class + '"></i>');
1490 }
1491
1492 search_avatar.append(elem_name);
1493 search_elem.append(search_avatar);
1494
1495 return search_elem;
1496 },
1497 $selectionFn
1498 });
1499 if ("$selection_event") {
1500 jQuery('select#{$options['id']}').on('select2:select', (e) => {
1501 let element = e?.params?.data || e;
1502 VBOCore.emitEvent("$selection_event", {
1503 element: element,
1504 });
1505 });
1506 }
1507 });
1508 JAVASCRIPT;
1509
1510 if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
1511 // concatenate script to HTML string when doing an AJAX request
1512 $html .= "\n" . '<script>' . $js_decl . '</script>';
1513 } else {
1514 // add script declaration to document
1515 JFactory::getDocument()->addScriptDeclaration($js_decl);
1516 }
1517
1518 // return the HTML string to be displayed
1519 return $html;
1520 }
1521
1522 /**
1523 * Loads the assets for setting up the VBOCore JS in the site section.
1524 *
1525 * @param array $options Associative list of loading options.
1526 *
1527 * @return void
1528 *
1529 * @since 1.17.4 (J) - 1.7.4 (WP)
1530 */
1531 public function loadCoreJS(array $options = [])
1532 {
1533 static $corejs_loaded = null;
1534
1535 if ($corejs_loaded) {
1536 // loaded flag
1537 return;
1538 }
1539
1540 // cache loaded flag
1541 $corejs_loaded = 1;
1542
1543 // add script
1544 $this->addScript(VBO_ADMIN_URI . 'resources/vbocore.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
1545
1546 if ($options) {
1547 $core_options = json_encode((object) $options, JSON_PRETTY_PRINT);
1548
1549 // add script declaration to document
1550 JFactory::getDocument()->addScriptDeclaration(
1551 <<<JS
1552 jQuery(function() {
1553 VBOCore.setOptions($core_options);
1554 });
1555 JS
1556 );
1557 }
1558 }
1559
1560 /**
1561 * Loads the assets for rendering the signature pad.
1562 *
1563 * @param array $options Associative list of loading options.
1564 *
1565 * @return void
1566 *
1567 * @since 1.17.4 (J) - 1.7.4 (WP)
1568 */
1569 public function loadSignaturePad(array $options = [])
1570 {
1571 static $signpad_loaded = null;
1572
1573 if ($signpad_loaded) {
1574 // loaded flag
1575 return;
1576 }
1577
1578 // cache loaded flag
1579 $signpad_loaded = 1;
1580
1581 // add script
1582 $this->addScript(VBO_SITE_URI . 'resources/signature_pad.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
1583 }
1584
1585 /**
1586 * Loads the assets solely needed to render the DRP calendar.
1587 *
1588 * @param array $options Associative list of loading options.
1589 *
1590 * @return void
1591 *
1592 * @since 1.17.3 (J) - 1.7.3 (WP)
1593 */
1594 public function loadDatesRangePicker(array $options = [])
1595 {
1596 static $drp_loaded = null;
1597
1598 if ($drp_loaded) {
1599 // loaded flag
1600 return;
1601 }
1602
1603 // cache loaded flag
1604 $drp_loaded = 1;
1605
1606 // add DRP script
1607 $this->addScript(VBO_SITE_URI . 'resources/datesrangepicker.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
1608
1609 // load JS lang defs
1610 JText::script('VBPICKUPROOM');
1611 JText::script('VBRETURNROOM');
1612 JText::script('VBO_MIN_STAY_NIGHTS');
1613 JText::script('VBO_CLEAR_DATES');
1614 JText::script('VBO_CLOSE');
1615 }
1616
1617 /**
1618 * Loads the necessary JS and CSS assets to render the jQuery UI Datepicker calendar.
1619 * It is also possible to load the assets for the DatesRangePicker extension.
1620 *
1621 * @param array $options Associative list of loading options.
1622 *
1623 * @return void
1624 *
1625 * @since 1.1.0
1626 * @since 1.15.0 (J) - 1.5.0 (WP) the lang definitions work for both front and back -ends.
1627 * @since 1.17.3 (J) - 1.7.3 (WP) added support for the DatesRangePicker extension.
1628 */
1629 public function loadDatePicker(array $options = [])
1630 {
1631 static $datepicker_loaded = null;
1632
1633 if ($datepicker_loaded) {
1634 // loaded flag
1635 return;
1636 }
1637
1638 $document = JFactory::getDocument();
1639 $document->addStyleSheet(VBO_SITE_URI . 'resources/jquery-ui.min.css');
1640
1641 JHtml::fetch('jquery.framework', true, true);
1642 $this->addScript(VBO_SITE_URI . 'resources/jquery-ui.min.js');
1643
1644 if (!strcasecmp(($options['type'] ?? ''), 'dates_range')) {
1645 // load DRP calendar assets
1646 $this->loadDatesRangePicker($options);
1647 }
1648
1649 $vbo_df = VikBooking::getDateFormat();
1650 $juidf = $vbo_df == "%d/%m/%Y" ? 'dd/mm/yy' : ($vbo_df == "%m/%d/%Y" ? 'mm/dd/yy' : 'yy/mm/dd');
1651
1652 $is_rtl_lan = false;
1653 $now_lang = JFactory::getLanguage();
1654 if (method_exists($now_lang, 'isRtl')) {
1655 $is_rtl_lan = $now_lang->isRtl();
1656 }
1657
1658 // list of week day names for "short" and "min" versions
1659 $day_names_short_list = [
1660 JText::translate('SUN'),
1661 JText::translate('MON'),
1662 JText::translate('TUE'),
1663 JText::translate('WED'),
1664 JText::translate('THU'),
1665 JText::translate('FRI'),
1666 JText::translate('SAT'),
1667 ];
1668 $day_names_min_list = [];
1669 foreach ($day_names_short_list as $wdn) {
1670 $day_names_min_list[] = $this->safeSubstr($wdn, 2);
1671 }
1672
1673 // ensure this language does not produce conflicting week-days "min"
1674 $day_names_min_list = array_unique($day_names_min_list);
1675 if (count($day_names_min_list) != count($day_names_short_list)) {
1676 // fallback onto the "short" week-days list to avoid conflicts with this language
1677 $day_names_min_list = $day_names_short_list;
1678 }
1679
1680 // build default regional values for datepicker
1681 $vbo_dp_regional_vals = [
1682 'closeText' => JText::translate('VBJQCALDONE'),
1683 'prevText' => JText::translate('VBJQCALPREV'),
1684 'nextText' => JText::translate('VBJQCALNEXT'),
1685 'currentText' => JText::translate('VBJQCALTODAY'),
1686 'monthNames' => [
1687 JText::translate('JANUARY'),
1688 JText::translate('FEBRUARY'),
1689 JText::translate('MARCH'),
1690 JText::translate('APRIL'),
1691 JText::translate('MAY'),
1692 JText::translate('JUNE'),
1693 JText::translate('JULY'),
1694 JText::translate('AUGUST'),
1695 JText::translate('SEPTEMBER'),
1696 JText::translate('OCTOBER'),
1697 JText::translate('NOVEMBER'),
1698 JText::translate('DECEMBER'),
1699 ],
1700 'monthNamesShort' => [
1701 JText::translate('JANUARY_SHORT'),
1702 JText::translate('FEBRUARY_SHORT'),
1703 JText::translate('MARCH_SHORT'),
1704 JText::translate('APRIL_SHORT'),
1705 JText::translate('MAY_SHORT'),
1706 JText::translate('JUNE_SHORT'),
1707 JText::translate('JULY_SHORT'),
1708 JText::translate('AUGUST_SHORT'),
1709 JText::translate('SEPTEMBER_SHORT'),
1710 JText::translate('OCTOBER_SHORT'),
1711 JText::translate('NOVEMBER_SHORT'),
1712 JText::translate('DECEMBER_SHORT'),
1713 ],
1714 'dayNames' => [
1715 JText::translate('SUNDAY'),
1716 JText::translate('MONDAY'),
1717 JText::translate('TUESDAY'),
1718 JText::translate('WEDNESDAY'),
1719 JText::translate('THURSDAY'),
1720 JText::translate('FRIDAY'),
1721 JText::translate('SATURDAY'),
1722 ],
1723 'dayNamesShort' => $day_names_short_list,
1724 'dayNamesMin' => $day_names_min_list,
1725 'weekHeader' => JText::translate('VBJQCALWKHEADER'),
1726 'dateFormat' => $juidf,
1727 'firstDay' => VikBooking::getFirstWeekDay(),
1728 'isRTL' => $is_rtl_lan,
1729 'showMonthAfterYear' => false,
1730 'yearSuffix' => '',
1731 ];
1732
1733 $ldecl = '
1734 jQuery(function($) {' . "\n" . '
1735 $.datepicker.regional["vikbooking"] = ' . json_encode($vbo_dp_regional_vals) . ';
1736 $.datepicker.setDefaults($.datepicker.regional["vikbooking"]);' . "\n" . '
1737 });';
1738
1739 /**
1740 * Trigger event to allow third party plugins to overwrite the JS declaration for the datepicker.
1741 *
1742 * @since 1.16.0 (J) - 1.6.0 (WP)
1743 */
1744 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeDeclareDatepickerRegionalVikBooking', [$is_rtl_lan, $now_lang->getTag(), &$ldecl]);
1745
1746 // add script declaration
1747 $document->addScriptDeclaration($ldecl);
1748
1749 // cache loaded flag
1750 $datepicker_loaded = 1;
1751 }
1752
1753 /**
1754 * Loads the CMS's native datepicker calendar.
1755 *
1756 * @since 1.10
1757 */
1758 public function getCalendar($val, $name, $id = null, $df = null, array $attributes = array())
1759 {
1760 if ($df === null)
1761 {
1762 $df = VikBooking::getDateFormat();
1763 }
1764
1765 return parent::calendar($val, $name, $id, $df, $attributes);
1766 }
1767
1768 /**
1769 * Returns a masked e-mail address. The e-mail are masked using
1770 * a technique to encode the bytes in hexadecimal representation.
1771 * The chunk of the masked e-mail will be also encoded to be HTML readable.
1772 *
1773 * @param string $email The e-mail to mask.
1774 * @param boolean $reverse True to reverse the e-mail address.
1775 * Only if the e-mail is not contained into an attribute.
1776 *
1777 * @return string The masked e-mail address.
1778 */
1779 public function maskMail($email, $reverse = false)
1780 {
1781 if ($reverse)
1782 {
1783 // reverse the e-mail address
1784 $email = strrev($email);
1785 }
1786
1787 // converts the e-mail address from bin to hex
1788 $email = bin2hex($email);
1789 // append ;&#x sequence after every chunk of the masked e-mail
1790 $email = chunk_split($email, 2, ";&#x");
1791 // prepend &#x sequence before the address and trim the ending sequence
1792 $email = "&#x" . substr($email, 0, -3);
1793
1794 return $email;
1795 }
1796
1797 /**
1798 * Returns a safemail tag to avoid the bots spoof a plain address.
1799 *
1800 * @param string $email The e-mail address to mask.
1801 * @param boolean $mail_to True if the address should be wrapped
1802 * within a "mailto" link.
1803 *
1804 * @return string The HTML tag containing the masked address.
1805 *
1806 * @uses maskMail()
1807 */
1808 public function safeMailTag($email, $mail_to = false)
1809 {
1810 // include the CSS declaration to reverse the text contained in the <safemail> tags
1811 JFactory::getDocument()->addStyleDeclaration('safemail {direction: rtl;unicode-bidi: bidi-override;}');
1812
1813 // mask the reversed e-mail address
1814 $masked = $this->maskMail($email, true);
1815
1816 // include the address into a custom <safemail> tag
1817 $tag = "<safemail>$masked</safemail>";
1818
1819 if ($mail_to)
1820 {
1821 // mask the address for mailto command (do not use reverse)
1822 $mailto = $this->maskMail($email);
1823
1824 // wrap the safemail tag within a mailto link
1825 $tag = "<a href=\"mailto:$mailto\" class=\"mailto\">$tag</a>";
1826 }
1827
1828 return $tag;
1829 }
1830
1831 /**
1832 * Loads and echoes the script necessary to render the Fancybox
1833 * plugin for jQuery to open images or iframes within a modal box.
1834 * This resolves conflicts with some Bootstrap or Joomla (4) versions
1835 * that do not support the old-native CSS class .modal with "behavior.modal".
1836 * Mainly made to open pictures in a modal box, so the default "type" is set to "image".
1837 * By passing a custom $opts string, the "type" property could be set to "iframe", but
1838 * in this case it's better to use the other method of this class (Jmodal).
1839 * The base jQuery library should be already loaded when using this method.
1840 *
1841 * @param string $selector The jQuery selector to trigger Fancybox.
1842 * @param string $opts The options object for the Fancybox setup.
1843 * @param boolean $reloadfunc If true, an additional function is included in the script
1844 * to apply again Fancybox to newly added images to the DOM (via Ajax).
1845 *
1846 * @return void
1847 *
1848 * @uses addScript()
1849 */
1850 public function prepareModalBox($selector = '.vbomodal', $opts = '', $reloadfunc = false)
1851 {
1852 if (empty($opts)) {
1853 $opts = '{
1854 "helpers": {
1855 "overlay": {
1856 "locked": false
1857 }
1858 },
1859 "width": "70%",
1860 "height": "75%",
1861 "autoScale": true,
1862 "transitionIn": "none",
1863 "transitionOut": "none",
1864 "padding": 0,
1865 "type": "image"
1866 }';
1867 }
1868 $document = JFactory::getDocument();
1869 $document->addStyleSheet(VBO_SITE_URI.'resources/jquery.fancybox.css');
1870 $this->addScript(VBO_SITE_URI.'resources/jquery.fancybox.js');
1871
1872 $reloadjs = '
1873 function reloadFancybox() {
1874 jQuery("'.$selector.'").fancybox('.$opts.');
1875 }
1876 ';
1877 $js = '
1878 <script type="text/javascript">
1879 jQuery(function() {
1880 jQuery("'.$selector.'").fancybox('.$opts.');
1881 });'.($reloadfunc ? $reloadjs : '').'
1882 </script>';
1883
1884 echo $js;
1885 }
1886
1887 /**
1888 * Method used to handle the reCAPTCHA events.
1889 *
1890 * @param string $event The reCAPTCHA event to trigger.
1891 * Here's the list of the accepted events:
1892 * - display Returns the HTML used to
1893 * display the reCAPTCHA input.
1894 * - check Validates the POST data to make sure
1895 * the reCAPTCHA input was checked.
1896 * @param array $options A configuration array.
1897 *
1898 * @return mixed The event response.
1899 *
1900 * @since 1.2.3
1901 * @wponly the Joomla integration differs
1902 */
1903 public function reCaptcha($event = 'display', array $options = array())
1904 {
1905 $response = null;
1906 // an optional configuration array (just leave empty)
1907 $options = array();
1908 // trigger reCAPTCHA display event to fill $response var
1909 do_action_ref_array('vik_recaptcha_' . $event, array(&$response, $options));
1910 // display reCAPTCHA by echoing it (empty in case reCAPTCHA is not available)
1911 return $response;
1912 }
1913
1914 /**
1915 * Checks if the com_user captcha is configured.
1916 * In case the parameter is set to global, the default one
1917 * will be retrieved.
1918 *
1919 * @param string $plugin The plugin name to check ('recaptcha' by default).
1920 *
1921 * @return boolean True if configured, otherwise false.
1922 *
1923 * @since 1.2.3
1924 * @wponly the Joomla integration differs
1925 */
1926 public function isCaptcha($plugin = 'recaptcha')
1927 {
1928 return apply_filters('vik_' . $plugin . '_on', false);
1929 }
1930
1931 /**
1932 * Checks if the global captcha is configured.
1933 *
1934 * @param string $plugin The plugin name to check ('recaptcha' by default).
1935 *
1936 * @return boolean True if configured, otherwise false.
1937 *
1938 * @since 1.2.3
1939 */
1940 public function isGlobalCaptcha($plugin = 'recaptcha')
1941 {
1942 return $this->isCaptcha($plugin);
1943 }
1944
1945 /**
1946 * Method used to obtain a WordPress media form field.
1947 *
1948 * @return string The media in HTML.
1949 *
1950 * @since 1.3.0
1951 */
1952 public function getMediaField($name, $value = null, array $data = array())
1953 {
1954 // check if WordPress is installed
1955 if (VBOPlatformDetection::isWordPress())
1956 {
1957 add_action('admin_enqueue_scripts', function() {
1958 wp_enqueue_media();
1959 });
1960
1961 // import form field class
1962 JLoader::import('adapter.form.field');
1963
1964 // create XML field manifest
1965 $xml = "<field name=\"$name\" type=\"media\" modowner=\"vikbooking\" />";
1966
1967 // instantiate field
1968 $field = JFormField::getInstance(simplexml_load_string($xml));
1969
1970 // overwrite name and value within data
1971 $data['name'] = $name;
1972 $data['value'] = $value;
1973
1974 // inject display data within field instance
1975 foreach ($data as $k => $v)
1976 {
1977 $field->bind($v, $k);
1978 }
1979
1980 // render field
1981 return $field->render();
1982 }
1983
1984 // fallback to Joomla
1985
1986 // init media field
1987 $field = new JFormFieldMedia(null, $value);
1988 // setup an empty form as placeholder
1989 $field->setForm(new JForm('vikbooking.media'));
1990
1991 // force field attributes
1992 $data['name'] = $name;
1993 $data['value'] = $value;
1994
1995 if (empty($data['previewWidth']))
1996 {
1997 // there is no preview width, set a defualt value
1998 // to make the image visible within the popover
1999 $data['previewWidth'] = 480;
2000 }
2001
2002 // render the field
2003 return $field->render('joomla.form.field.media', $data);
2004 }
2005
2006 /**
2007 * Displays a multi-state toggle switch element with unlimited buttons.
2008 * Custom values, contents, labels, attributes and JS events can be attached
2009 * to each button. VCM will use this same method.
2010 *
2011 * @param string $name the input name equal for all radio buttons.
2012 * @param string $value the current input field value to be pre-selected.
2013 * @param array $values list of radio buttons with each value.
2014 * @param array $labels list of contents for each button trigger.
2015 * @param array $attrs list of associative array attributes for each button.
2016 * @param array $wrap list of associative array attributes for the wrapper.
2017 *
2018 * @return string the necessary HTML to render the multi-state toggle switch.
2019 *
2020 * @since 1.15.0 (J) - 1.5.0 (WP)
2021 */
2022 public function multiStateToggleSwitchField($name, $value, $values = array(), $labels = array(), $attrs = array(), $wrap = array())
2023 {
2024 static $tooltip_js_declared = null;
2025
2026 // whether tooltip for titles is needed
2027 $needs_tooltip = false;
2028
2029 // HTML container
2030 $multi_state_switch = '';
2031
2032 if (!is_array($values) || !count($values)) {
2033 // values must be set or we don't know what buttons to display
2034 return $multi_state_switch;
2035 }
2036
2037 // build default classes for the tri-state toggle switch (with 3 buttons)
2038 $def_tristate_cls = array(
2039 'vik-multiswitch-radiobtn-on',
2040 'vik-multiswitch-radiobtn-def',
2041 'vik-multiswitch-radiobtn-off',
2042 );
2043
2044 // start wrapper
2045 $multi_state_switch .= "\n" . '<div class="vik-multiswitch-wrap' . (isset($wrap['class']) ? (' ' . $wrap['class']) : '') . '">' . "\n";
2046
2047 foreach ($values as $btn_k => $btn_val) {
2048 // build default classes for button label
2049 $btn_classes = array('vik-multiswitch-radiobtn');
2050 if (isset($def_tristate_cls[$btn_k])) {
2051 // push default class for a 3-state toggle switch
2052 array_push($btn_classes, $def_tristate_cls[$btn_k]);
2053 }
2054 // check if additional custom classes have been defined for this button
2055 if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['label_class']) && !empty($attrs[$btn_k]['label_class'])) {
2056 if (is_array($attrs[$btn_k]['label_class'])) {
2057 // list of additional classes for this button
2058 $btn_classes = array_merge($btn_classes, $attrs[$btn_k]['label_class']);
2059 } elseif (is_string($attrs[$btn_k]['label_class'])) {
2060 // multiple classes should be space-separated
2061 array_push($btn_classes, $attrs[$btn_k]['label_class']);
2062 }
2063 }
2064
2065 // check title as first thing, even though this is passed along with the labels
2066 $label_title = '';
2067 if (isset($labels[$btn_k]) && !is_scalar($labels[$btn_k]) && isset($labels[$btn_k]['title'])) {
2068 $needs_tooltip = true;
2069 $label_title = ' title="' . addslashes(htmlentities($labels[$btn_k]['title'])) . '"';
2070 }
2071
2072 // start button label
2073 $multi_state_switch .= "\t" . '<label class="' . implode(' ', $btn_classes) . '"' . $label_title . '>' . "\n";
2074
2075 // check button input radio
2076 $radio_attributes = array();
2077 if (($value !== null && $value == $btn_val) || ($value === null && $btn_k === 0)) {
2078 // this radio button must be checked (pre-selected)
2079 $radio_attributes['checked'] = true;
2080 }
2081 // check if custom attributes were specified for this input
2082 if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['input'])) {
2083 // must be an associative array with key = attribute name, value = attribute value
2084 foreach ($attrs[$btn_k]['input'] as $attr_name => $attr_val) {
2085 // javascript events could be attached like 'onchange'=>'myCallback(this.value)'
2086 $radio_attributes[$attr_name] = $attr_val;
2087 }
2088 }
2089 $radio_attr_string = '';
2090 foreach ($radio_attributes as $attr_name => $attr_val) {
2091 if ($attr_val === true) {
2092 // short-attribute name, like "checked"
2093 $radio_attr_string .= $attr_name . ' ';
2094 continue;
2095 }
2096 $radio_attr_string .= $attr_name . '="' . $attr_val . '" ';
2097 }
2098 $multi_state_switch .= "\t\t" . '<input type="radio" name="' . $name . '" value="' . $btn_val . '" ' . $radio_attr_string . '/>' . "\n";
2099
2100 // add button trigger
2101 $multi_state_switch .= "\t\t" . '<span class="vik-multiswitch-trigger"></span>' . "\n";
2102
2103 // check button label text
2104 if (isset($labels[$btn_k])) {
2105 /**
2106 * By default, the buttons of the toggle switch use an animation,
2107 * which requires an absolute positioning of the "label-text".
2108 * For this reason, there cannot be a minimum width for these texts
2109 * and so the content should fit the default width. Usually, using
2110 * a font-awesome icon is the best content. For using literal texts,
2111 * like "Dark", "Light" etc.. the class "vik-multiswitch-noanimation"
2112 * should be passed to the button label text.
2113 */
2114 $label_txt = '';
2115 $label_class = '';
2116 if (!is_scalar($labels[$btn_k])) {
2117 // with an associative array we accept value, title and custom classes
2118 if (isset($labels[$btn_k]['value'])) {
2119 $label_txt = $labels[$btn_k]['value'];
2120 }
2121 if (isset($labels[$btn_k]['class'])) {
2122 $label_class = ' ' . ltrim($labels[$btn_k]['class']);
2123 }
2124 } else {
2125 // just a string, maybe with text or HTML mixed content
2126 $label_txt = $labels[$btn_k];
2127 }
2128 if (strlen($label_txt)) {
2129 // append button label text only if some text has been defined
2130 $multi_state_switch .= "\t\t" . '<span class="vik-multiswitch-txt' . $label_class . '">' . $label_txt . '</span>' . "\n";
2131 }
2132 }
2133
2134 // end button label
2135 $multi_state_switch .= "\t" . '</label>' . "\n";
2136 }
2137
2138 // end wrapper
2139 $multi_state_switch .= '</div>' . "\n";
2140
2141 // check tooltip JS rendering
2142 if (!$tooltip_js_declared && $needs_tooltip) {
2143 // turn static flag on
2144 $tooltip_js_declared = 1;
2145
2146 // add script declaration for JS rendering of tooltips
2147 $doc = JFactory::getDocument();
2148 $doc->addScriptDeclaration(
2149 <<<JS
2150 jQuery(function() {
2151 if (typeof jQuery.fn.tooltip === 'function') {
2152 jQuery('.vik-multiswitch-wrap label').tooltip();
2153 }
2154 });
2155 JS
2156 );
2157 }
2158
2159 return $multi_state_switch;
2160 }
2161
2162 /**
2163 * Returns a list of supported fonts for the third-party visual editor (Quill).
2164 *
2165 * @param bool $short_names whether to return short font names.
2166 *
2167 * @return array list of font family names or short names.
2168 *
2169 * @since 1.15.0 (J) - 1.5.0 (WP)
2170 */
2171 public function getVisualEditorFonts($short_names = false)
2172 {
2173 // supported fonts
2174 $font_families = ['Sans Serif', 'Arial', 'Courier', 'Garamond', 'Tahoma', 'Times New Roman', 'Verdana', 'Inconsolata', 'Sailec Light', 'Monospace'];
2175
2176 if (!$short_names) {
2177 // return the regular names to be displayed
2178 return $font_families;
2179 }
2180
2181 // return the "short" names of the supported fonts
2182 return array_map(function($font) {
2183 return str_replace(' ', '-', strtolower($font));
2184 }, $font_families);
2185 }
2186
2187 /**
2188 * Loads the necessary language definitions for the third-party visual editor (Quill).
2189 *
2190 * @return void
2191 *
2192 * @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
2193 */
2194 public function loadVisualEditorDefinitions()
2195 {
2196 static $loaded = null;
2197
2198 if ($loaded) {
2199 return;
2200 }
2201
2202 $loaded = 1;
2203
2204 // load language definitions for JS
2205 JText::script('VBO_CONT_WRAPPER');
2206 JText::script('VBO_CONT_WRAPPER_HELP');
2207 JText::script('VBO_GEN_CONTENT');
2208 JText::script('VBO_AI_LABEL_DEF');
2209 JText::script('VBO_AITOOL_WRITER_DEF_PROMPT');
2210 JText::script('VBANNULLA');
2211 }
2212
2213 /**
2214 * Loads the necessary assets for the third-party visual editor (Quill).
2215 *
2216 * @return void
2217 *
2218 * @since 1.15.0 (J) - 1.5.0 (WP)
2219 * @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
2220 */
2221 public function loadVisualEditorAssets()
2222 {
2223 static $loaded = null;
2224
2225 if ($loaded) {
2226 return;
2227 }
2228
2229 $loaded = 1;
2230
2231 // access the document
2232 $doc = JFactory::getDocument();
2233
2234 // load JS langs
2235 $this->loadVisualEditorDefinitions();
2236
2237 // build the list of font families
2238 $font_families = $this->getVisualEditorFonts();
2239 $font_shortfam = $this->getVisualEditorFonts(true);
2240 $js_font_names = json_encode($font_shortfam);
2241
2242 // build inline CSS styles
2243 $css_font_decl = '';
2244 foreach ($font_families as $k => $font_name) {
2245 $font_val = $font_shortfam[$k];
2246 $css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="' . $font_val . '"]::before,';
2247 $css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="' . $font_val . '"]::before {' . "\n";
2248 $css_font_decl .= 'content: "' . $font_name . '";' . "\n";
2249 $css_font_decl .= 'font-family: "' . $font_name . '";' . "\n";
2250 $css_font_decl .= '}' . "\n";
2251 }
2252
2253 $css_font_decl .= '
2254 .ql-picker.ql-specialtags .ql-picker-label {
2255 padding-right: 18px;
2256 }
2257 .ql-picker.ql-specialtags .ql-picker-label:before {
2258 content: "' . htmlspecialchars(JText::translate('VBO_CONDTEXT_TKN')) . '";
2259 }
2260 .ql-formats .ql-genai {
2261 width: auto !important;
2262 font-weight: bold;
2263 }
2264 ';
2265
2266 /**
2267 * Cache the pre-configuration CSS onto a file to allow AJAX requests
2268 * to properly load the asset inline within the response. Before this
2269 * change the styles used to be added as an inline style declaration.
2270 *
2271 * @since 1.16.7 (J) - 1.6.7 (WP)
2272 * @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
2273 */
2274 $cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
2275 $cached_preconfig_css_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css']);
2276 $cached_preconfig_css_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css';
2277 $cached_preconfig_css_ok = is_file($cached_preconfig_css_path);
2278
2279 if (!$cached_preconfig_css_ok) {
2280 // attempt to create the file
2281 $cached_preconfig_css_ok = JFile::write($cached_preconfig_css_path, $css_font_decl);
2282 }
2283
2284 if ($cached_preconfig_css_ok) {
2285 // load cached script file
2286 $doc->addStyleSheet($cached_preconfig_css_uri);
2287 } else {
2288 // revert to append CSS style declaration to document
2289 $doc->addStyleDeclaration($css_font_decl);
2290 }
2291
2292 // append theme CSS to document
2293 $doc->addStyleSheet(VBO_ADMIN_URI . 'resources/quill/quill.snow.css');
2294
2295 // append JS assets to document
2296 $this->addScript(VBO_ADMIN_URI . 'resources/quill/quill.js');
2297 $this->addScript(VBO_ADMIN_URI . 'resources/quill/quill-image-resize.min.js');
2298 $this->addScript(VBO_ADMIN_URI . 'resources/quill/vik-content-builder.js');
2299
2300 // icon for mail wrapper
2301 $mail_wrapper_icn = '<i class="' . VikBookingIcons::i('minus-square') . '" title="' . htmlspecialchars(JText::translate('VBO_INSERT_CONT_WRAPPER')) . '"></i>';
2302
2303 // icon for mail preview
2304 $mail_preview_icn = '<i class="' . VikBookingIcons::i('eye') . '" title="' . htmlspecialchars(JText::translate('VBOPREVIEW')) . '"></i>';
2305
2306 // icon for property logo (home icon)
2307 $mail_homelogo_icn = '<i class="' . VikBookingIcons::i('hotel') . '" title="' . htmlspecialchars(JText::translate('VBCONFIGFOURLOGO'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401) . '"></i>';
2308
2309 // text for generating content through AI
2310 $mail_genai_icn = htmlspecialchars(JText::translate('VBO_AI_LABEL_DEF'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
2311
2312 // build script pre-configuration string
2313 $quill_preconfig_script =
2314 <<<JS
2315 // Quill pre-configuration
2316 (function() {
2317 // configure Quill to use inline styles rather than classes
2318
2319 var AlignClass = Quill.import('attributors/class/align');
2320 Quill.register(AlignClass, true);
2321
2322 var BackgroundClass = Quill.import('attributors/class/background');
2323 Quill.register(BackgroundClass, true);
2324
2325 var ColorClass = Quill.import('attributors/class/color');
2326 Quill.register(ColorClass, true);
2327
2328 var FontClass = Quill.import('attributors/class/font');
2329 Quill.register(FontClass, true);
2330
2331 var SizeClass = Quill.import('attributors/class/size');
2332 Quill.register(SizeClass, true);
2333
2334 var AlignStyle = Quill.import('attributors/style/align');
2335 Quill.register(AlignStyle, true);
2336
2337 var BackgroundStyle = Quill.import('attributors/style/background');
2338 Quill.register(BackgroundStyle, true);
2339
2340 var ColorStyle = Quill.import('attributors/style/color');
2341 Quill.register(ColorStyle, true);
2342
2343 var SizeStyle = Quill.import('attributors/style/size');
2344 Quill.register(SizeStyle, true);
2345
2346 var FontStyle = Quill.import('attributors/style/font');
2347 Quill.register(FontStyle, true);
2348
2349 // set additional fonts
2350 var Font = Quill.import('formats/font');
2351 Font.whitelist = $js_font_names;
2352 Quill.register(Font, true);
2353
2354 // register custom Blot for special tags
2355 var Inline = Quill.import('blots/inline');
2356 class Specialtag extends Inline {
2357 static create(value) {
2358 var node = super.create(value);
2359 if (value) {
2360 node.setAttribute('class', value);
2361 }
2362 return node;
2363 }
2364
2365 static formats(domNode) {
2366 return domNode.getAttribute("class");
2367 }
2368
2369 format(name, value) {
2370 if (name !== this.statics.blotName || !value) {
2371 return super.format(name, value);
2372 }
2373 if (value) {
2374 this.domNode.setAttribute('class', value);
2375 }
2376 }
2377 }
2378 Specialtag.blotName = 'specialtag';
2379 Specialtag.tagName = 'strong';
2380 Quill.register(Specialtag);
2381
2382 // register bold tag names in the proper order to avoid conflicts
2383 var Bold = Quill.import('formats/bold');
2384 Bold.tagName = ['B', 'STRONG'];
2385 Quill.register(Bold, true);
2386
2387 // register custom Blot for mail-wrapper
2388 var BlockEmbed = Quill.import('blots/block/embed');
2389 class MailWrapper extends BlockEmbed { }
2390 MailWrapper.blotName = 'mailwrapper';
2391 MailWrapper.className = 'vbo-editor-hl-mailwrapper';
2392 MailWrapper.tagName = 'hr';
2393 Quill.register(MailWrapper);
2394
2395 // register custom Blot for preview
2396 class Preview extends Inline { }
2397 Preview.blotName = 'preview';
2398 Preview.tagName = 'span';
2399 Quill.register(Preview);
2400
2401 // register custom icons for mail-wrapper and preview
2402 var icons = Quill.import('ui/icons');
2403 icons['mailwrapper'] = '$mail_wrapper_icn';
2404 icons['preview'] = '$mail_preview_icn';
2405 icons['homelogo'] = '$mail_homelogo_icn';
2406 icons['genai'] = '$mail_genai_icn';
2407 })();
2408 JS
2409 ;
2410
2411 /**
2412 * Cache the pre-configuration script onto a file to allow AJAX requests
2413 * to properly load the script inline within the response. Before this
2414 * change the script used to be added as an inline script declaration.
2415 *
2416 * @since 1.16.7 (J) - 1.6.7 (WP)
2417 * @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
2418 */
2419 $cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
2420 $cached_preconfig_script_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.js']);
2421 $cached_preconfig_script_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.js';
2422 $cached_preconfig_script_ok = is_file($cached_preconfig_script_path);
2423
2424 if (!$cached_preconfig_script_ok) {
2425 // attempt to create the file
2426 $cached_preconfig_script_ok = JFile::write($cached_preconfig_script_path, $quill_preconfig_script);
2427 }
2428
2429 if ($cached_preconfig_script_ok) {
2430 // load cached script file
2431 $this->addScript($cached_preconfig_script_uri);
2432 } else {
2433 // revert to append JS script declaration to document
2434 $doc->addScriptDeclaration($quill_preconfig_script);
2435 }
2436
2437 /**
2438 * Load Context Menu assets.
2439 *
2440 * @since 1.17.6 (J) - 1.7.6 (WP)
2441 * @since 1.18.0 (J) - 1.8.0 (WP) loaded only if not during an AJAX request.
2442 */
2443 if ((VBOPlatformDetection::isWordPress() && !wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
2444 $this->loadContextMenuAssets();
2445 }
2446 }
2447
2448 /**
2449 * Renders a third-party visual editor (Quill).
2450 *
2451 * @param string $name the input name of the textarea field.
2452 * @param string $value the current value of the textarea field/editor.
2453 * @param array $attrs list of associative array attributes for the textarea.
2454 * @param array $opts associative array of options for the editor.
2455 * @param array $btns associative array of custom buttons for the editor (special tags).
2456 *
2457 * @return string the necessary HTML to render the visual editor.
2458 *
2459 * @since 1.15.0 (J) - 1.5.0 (WP)
2460 * @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
2461 */
2462 public function renderVisualEditor($name, $value, array $attrs = [], array $opts = [], array $btns = [])
2463 {
2464 if (empty($name)) {
2465 return '';
2466 }
2467
2468 // build the AJAX endpoints for uploading files, to preview the message and more
2469 $upload_endpoint = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=upload_media_file');
2470 $ajax_preview_mess = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.preview_visual_editor');
2471 $ajax_logo_url = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.get_default_logo');
2472
2473 // the HTML to build
2474 $editor = "\n";
2475
2476 // static editor counter
2477 static $editor_counter = 0;
2478
2479 // increase counter for this instance
2480 $editor_counter++;
2481
2482 // build id attributes for text and visual editors
2483 $editor_id = 'vik-contentbuilder-editor-' . $editor_counter;
2484 if (!isset($attrs['id'])) {
2485 $attrs['id'] = 'vik-contentbuilder-tarea-' . $editor_counter;
2486 }
2487
2488 // labels for modes and editor
2489 $text_html_lbl = JText::translate('VBO_MODE_TEXTHTML');
2490 $visual_mode_lbl = JText::translate('VBO_MODE_VISUAL');
2491
2492 // allowed modes to display the visual editor
2493 $allowed_modes = [
2494 'text' => $text_html_lbl,
2495 'visual' => $visual_mode_lbl,
2496 'modal-visual' => $visual_mode_lbl,
2497 ];
2498
2499 // define the default buttons to display for mode switching
2500 $modes = [
2501 'text' => $text_html_lbl,
2502 'modal-visual' => $visual_mode_lbl,
2503 ];
2504
2505 // overwrite modes to display
2506 if (isset($opts['modes']) && is_array($opts['modes']) && $opts['modes']) {
2507 // check if the given array is associative, hence inclusive of texts
2508 if (array_keys($opts['modes']) != range(0, (count($opts['modes']) - 1))) {
2509 // replace modes
2510 $modes = $opts['modes'];
2511 } else {
2512 // only the allowed keys must have been passed
2513 $new_modes = [];
2514 foreach ($opts['modes'] as $mode_type) {
2515 if (!isset($allowed_modes[$mode_type])) {
2516 continue;
2517 }
2518 $new_modes[$mode_type] = $allowed_modes[$mode_type];
2519 }
2520 $modes = $new_modes ?: $modes;
2521 }
2522 }
2523
2524 // resolve conflicts for both visual modes
2525 if (($modes['visual'] ?? '') && ($modes['modal-visual'] ?? '') && $modes['visual'] == $modes['modal-visual']) {
2526 $modes['modal-visual'] .= ' <i class="' . VikBookingIcons::i('window-restore') . '"></i>';
2527 }
2528
2529 // ensure the text mode is set
2530 if (!isset($modes['text'])) {
2531 $modes['text'] = $allowed_modes['text'];
2532 }
2533
2534 // overwrite default mode
2535 if (isset($opts['def_mode']) && isset($modes[$opts['def_mode']]) && count($modes) > 1) {
2536 $def_mode_val = $modes[$opts['def_mode']];
2537 unset($modes[$opts['def_mode']]);
2538 // sort modes accordingly
2539 $modes = array_merge([$opts['def_mode'] => $def_mode_val], $modes);
2540 // reset the array pointer
2541 reset($modes);
2542 }
2543
2544 // set default mode
2545 $default_mode = key($modes);
2546
2547 // ensure text-area styles are merged or set when not the default mode
2548 if ($default_mode != 'text') {
2549 if (!($attrs['style'] ?? '')) {
2550 // set style attribute to hide the text-area
2551 $attrs['style'] = 'display: none;';
2552 } else {
2553 // append the style instruction to hide the text-area
2554 $attrs['style'] .= ' display: none;';
2555 }
2556 }
2557
2558 // textarea attributes
2559 $ta_attributes = [];
2560 foreach ($attrs as $aname => $aval) {
2561 if ($aname == 'name') {
2562 // skip reserved attribute name
2563 continue;
2564 }
2565 $ta_attributes[] = $aname . '="' . JHtml::fetch('esc_attr', $aval) . '"';
2566 }
2567
2568 // visual editor JS options for list buttons
2569 $js_editor_opts_list_btns = [
2570 // ordered list (ol)
2571 ['list' => 'ordered'],
2572 // un-ordered list (ul)
2573 ['list' => 'bullet'],
2574 ];
2575
2576 if ($opts['list_check'] ?? null) {
2577 // push the un-ordered list (ul) with checked data-attribute, simulating a check-boxes list
2578 $js_editor_opts_list_btns[] = ['list' => 'check'];
2579 }
2580
2581 // build visual editor JS options
2582 $js_editor_opts = [
2583 'snippetsyntax' => true,
2584 'modules' => [
2585 'toolbar' => [
2586 'container' => [
2587 [
2588 [
2589 'font' => $this->getVisualEditorFonts(true)
2590 ],
2591 ],
2592 [
2593 [
2594 'header' => [1, 2, 3, 4, 5, 6, false]
2595 ]
2596 ],
2597 [
2598 'bold',
2599 'italic',
2600 'underline',
2601 'strike',
2602 'blockquote',
2603 ],
2604 [
2605 ['align' => []],
2606 ['indent' => '-1'],
2607 ['indent' => '+1'],
2608 ],
2609 [
2610 [
2611 'color' => []
2612 ],
2613 [
2614 'background' => []
2615 ],
2616 ],
2617 $js_editor_opts_list_btns,
2618 [
2619 'link',
2620 'image',
2621 'homelogo',
2622 ],
2623 [
2624 'mailwrapper',
2625 'preview',
2626 ],
2627 ],
2628 ],
2629 'imageResize' => [
2630 'displaySize' => true,
2631 ],
2632 ],
2633 'theme' => 'snow',
2634 ];
2635
2636 // check for Generative AI support through Vik Channel Manager and E4jConnect
2637 if (class_exists('VikChannelManager') && defined('VikChannelManagerConfig::AI')) {
2638 // add editor button for Gen-AI
2639 $js_editor_opts['modules']['toolbar']['container'][] = ['genai'];
2640 }
2641
2642 // build the list of special tags to be added to the editor
2643 $special_tags_btns = [];
2644 foreach ($btns as $tag_val) {
2645 $special_tags_btns[] = $tag_val;
2646 }
2647
2648 if ($special_tags_btns) {
2649 // add custom buttons to the editor to manage special tags
2650 $js_editor_opts['modules']['toolbar']['container'][] = [
2651 ['specialtags' => $special_tags_btns]
2652 ];
2653 // append CSS inline styles
2654 $editor .= '<style type="text/css">' . "\n";
2655 foreach ($special_tags_btns as $tag_val) {
2656 $editor .= '.ql-picker.ql-specialtags .ql-picker-item[data-value="' . $tag_val . '"]:before {
2657 content: "' . $tag_val . '";
2658 }' . "\n";
2659 }
2660 $editor .= '</style>' . "\n";
2661 }
2662
2663 if ($opts['unset_buttons'] ?? null) {
2664 // unset toolbar container main-level buttons
2665 $opts['unset_buttons'] = (array) $opts['unset_buttons'];
2666 foreach ($js_editor_opts['modules']['toolbar']['container'] as $tc_index => $tc_buttons) {
2667 if (!is_array($tc_buttons)) {
2668 continue;
2669 }
2670 foreach ($opts['unset_buttons'] as $unset_btn) {
2671 if (!is_string($unset_btn)) {
2672 continue;
2673 }
2674 foreach ($tc_buttons as $tc_btn_index => $tc_btn) {
2675 if (is_string($tc_btn) && $tc_btn === $unset_btn) {
2676 // unset toolbar container button
2677 unset($js_editor_opts['modules']['toolbar']['container'][$tc_index][$tc_btn_index]);
2678 }
2679 }
2680 }
2681 if (!$js_editor_opts['modules']['toolbar']['container'][$tc_index]) {
2682 // unset the whole container
2683 unset($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
2684 } else {
2685 // restore numeric (non-associative) list
2686 $js_editor_opts['modules']['toolbar']['container'][$tc_index] = array_values($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
2687 }
2688 }
2689 // restore numeric (non-associative) list
2690 $js_editor_opts['modules']['toolbar']['container'] = array_values($js_editor_opts['modules']['toolbar']['container']);
2691 }
2692
2693 // attept to pretty print a JSON encoded string for the editor options
2694 $editor_opts_str = defined('JSON_PRETTY_PRINT') ? json_encode($js_editor_opts, JSON_PRETTY_PRINT) : json_encode($js_editor_opts);
2695
2696 // safe default value for editor (HTML tags should not be converted to entities)
2697 $safe_value = preg_replace("/(<\/ ?textarea>)+/i", '', $value);
2698
2699 // the HTML to render the visual editor
2700 $html_visual_editor = '<div class="vik-contentbuilder-editor-container" id="' . $editor_id . '"></div>';
2701
2702 // build the actual HTML content
2703 $editor .= '<div class="vik-contentbuilder-wrapper">' . "\n";
2704 if (count($modes) > 1) {
2705 // display buttons to switch mode only if more than one mode available
2706 $editor .= "\t" . '<div class="vik-contentbuilder-switcher"' . (($opts['hide_modes'] ?? null) ? ' style="display: none;"' : '') . '>' . "\n";
2707 foreach ($modes as $key => $val) {
2708 if (!isset($allowed_modes[$key])) {
2709 continue;
2710 }
2711 $editor .= '<button type="button" class="btn vik-contentbuilder-switcher-btn' . ($default_mode == $key ? ' vik-contentbuilder-switcher-btn-active' : '') . '" data-switch="' . $key . '" onclick="VikContentBuilder.switchMode(this);">' . $val . '</button>';
2712 }
2713 $editor .= "\t" . '</div>' . "\n";
2714 }
2715 $editor .= "\t" . '<div class="vik-contentbuilder-inner">' . "\n";
2716 foreach ($modes as $key => $val) {
2717 if ($key == 'text') {
2718 // if text is not the default mode, the text-area will be always hid through the style attribute
2719 $editor .= "\t\t" . '<textarea name="' . $name . '" data-switch="text" ' . implode(' ', $ta_attributes) . '>' . $safe_value . '</textarea>' . "\n";
2720 } elseif ($key == 'visual') {
2721 $editor .= "\t\t" . '<div class="vik-contentbuilder-container" data-switch="visual" style="' . ($default_mode != 'visual' ? 'display: none;' : '') . '">' . "\n";
2722 $editor .= "\t\t\t" . '<div class="vik-contentbuilder-editor-wrap">' . "\n";
2723 $editor .= "\t\t\t\t" . $html_visual_editor . "\n";
2724 $editor .= "\t\t\t" . '</div>' . "\n";
2725 $editor .= "\t\t" . '</div>' . "\n";
2726 } elseif ($key == 'modal-visual') {
2727 $editor .= "\t\t" . '<div class="vik-contentbuilder-modal-container" data-switch="modal-visual" data-container="' . (isset($modes['visual']) ? 'visual' : 'modal-visual') . '" style="display: none;">' . "\n";
2728 $editor .= "\t\t\t" . '<div class="vik-contentbuilder-editor-wrap">' . "\n";
2729 $editor .= "\t\t\t\t" . (!isset($modes['visual']) ? $html_visual_editor : '') . "\n";
2730 $editor .= "\t\t\t" . '</div>' . "\n";
2731 $editor .= "\t\t" . '</div>' . "\n";
2732 }
2733 }
2734 $editor .= "\t" . '</div>' . "\n";
2735 $editor .= '</div>' . "\n";
2736 $editor .= "\n";
2737
2738 // default prompt for Gen-AI
2739 $gen_ai_use_prompt = $opts['gen_ai']['prompt'] ?? null;
2740 if (!$gen_ai_use_prompt && ($opts['gen_ai']['customer'] ?? [])) {
2741 $guest_name = trim(($opts['gen_ai']['customer']['first_name'] ?? '') . ' ' . ($opts['gen_ai']['customer']['last_name'] ?? ''));
2742 if ($guest_name) {
2743 // set proper prompt message with the guest name
2744 $gen_ai_use_prompt = JText::sprintf('VBO_AI_DISC_WRITER_FN_TEXT_GEN_MESS_EXA', $guest_name);
2745 if (!empty($opts['gen_ai']['booking']['lang']) && JFactory::getLanguage()->getTag() != $opts['gen_ai']['booking']['lang']) {
2746 // write the message in the guest language
2747 $guest_lang = $opts['gen_ai']['booking']['lang'];
2748 $known_langs = $this->getKnownLanguages();
2749 if ($known_langs[$guest_lang]['nativeName'] ?? '') {
2750 $guest_lang = $known_langs[$guest_lang]['nativeName'];
2751 }
2752 $gen_ai_use_prompt .= ' ' . JText::sprintf('VBO_AI_GEN_MESS_LANG', $guest_lang);
2753 }
2754 }
2755 }
2756
2757 if (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'cron')) {
2758 // use default prompt for cron messages
2759 $gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_CRON_DEF_PROMPT');
2760 } elseif (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'taskmanager')) {
2761 // use default prompt for the task manager
2762 $gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_TM_DEF_PROMPT');
2763 }
2764
2765 if ($gen_ai_use_prompt && ($opts['gen_ai']['placeholders'] ?? 0) && $btns) {
2766 // add prompt text for using the placeholder tags
2767 $placeholders = array_filter($btns, function($tag) {
2768 // remove conditional text rule tags
2769 return !preg_match('/^\{condition\:\s?.+$/i', $tag);
2770 });
2771 if ($placeholders) {
2772 $gen_ai_use_prompt .= ' ' . JText::translate('VBO_AITOOL_WRITER_USE_PLACEHOLDERS') . "\n" . implode(', ', $placeholders);
2773 }
2774 }
2775
2776 // sanitize default prompt
2777 $gen_ai_use_prompt = json_encode((string) $gen_ai_use_prompt);
2778
2779 // add JS script to HTML content
2780 $toast_icon = VikBookingIcons::i('minus-square');
2781 $envelope_icon = VikBookingIcons::i('envelope');
2782 $booking_icon = VikBookingIcons::i('address-card');
2783 $preview_lbl = htmlspecialchars(JText::translate('VBOPREVIEW'));
2784 $booking_lbl = htmlspecialchars(JText::translate('VBDASHBOOKINGID'));
2785 $editor .= <<<HTML
2786 <script>
2787 jQuery(function() {
2788
2789 const message_preview_fn = (content, bid) => {
2790 let use_bid = bid || (typeof window['vbo_current_bid'] !== 'undefined' ? window['vbo_current_bid'] : null);
2791 VBOCore.doAjax('$ajax_preview_mess', {
2792 content: content,
2793 bid: use_bid,
2794 }, (resp) => {
2795 var pop_win = window.open('', '', 'width=800, height=600, scrollbars=yes');
2796 pop_win.document.body.innerHTML = resp[0];
2797 }, (err) => {
2798 console.log(err);
2799 alert(err.responseText);
2800 });
2801 };
2802
2803 var vbo_toast_mailwrapper = null;
2804
2805 var visual_editor_handlers = {
2806 specialtags: function(tag) {
2807 if (tag) {
2808 var cursorPosition = this.quill.getSelection().index;
2809 this.quill.insertText(cursorPosition, tag, 'specialtag', 'vbo-editor-hl-specialtag');
2810 cursorPosition += tag.length + 1;
2811 this.quill.setSelection(cursorPosition, 'silent');
2812 this.quill.insertText(cursorPosition, ' ');
2813 this.quill.setSelection(cursorPosition + 1, 'silent');
2814 this.quill.deleteText(cursorPosition - 1, 1);
2815 }
2816 },
2817 image: function(clicked) {
2818 var img_handler = new VikContentBuilderImageHandler(this.quill);
2819 img_handler.setEndpoint('$upload_endpoint').present();
2820 },
2821 mailwrapper: function(clicked) {
2822 var range = this.quill.getSelection(true);
2823 this.quill.insertText(range.index, "\\n", 'user');
2824 this.quill.insertEmbed(range.index + 1, 'mailwrapper', true, 'user');
2825 this.quill.setSelection(range.index + 2, 'silent');
2826 if (!vbo_toast_mailwrapper) {
2827 vbo_toast_mailwrapper = 1;
2828 VBOToast.enqueue(new VBOToastMessage({
2829 title: Joomla.JText._('VBO_CONT_WRAPPER'),
2830 body: Joomla.JText._('VBO_CONT_WRAPPER_HELP'),
2831 icon: '$toast_icon',
2832 delay: {
2833 min: 6000,
2834 max: 20000,
2835 tolerance: 4000,
2836 },
2837 action: () => {
2838 VBOToast.dispose(true);
2839 }
2840 }));
2841 }
2842 },
2843 preview: function(clicked) {
2844 let content = this.quill.root.innerHTML;
2845 try {
2846 let preview_btn = this.quill.container.closest('.vik-contentbuilder-editor-wrap').querySelector('button.ql-preview');
2847 jQuery(preview_btn).vboContextMenu({
2848 placement: 'bottom-right',
2849 buttons: [
2850 {
2851 icon: '$envelope_icon',
2852 text: '$preview_lbl',
2853 separator: true,
2854 action: (root, config) => {
2855 message_preview_fn.call(clicked, content);
2856 setTimeout(() => {
2857 jQuery(preview_btn).vboContextMenu('destroy');
2858 }, 500);
2859 },
2860 },
2861 {
2862 icon: '$booking_icon',
2863 text: '$booking_lbl',
2864 action: (root, config) => {
2865 let bid = prompt('$preview_lbl - $booking_lbl');
2866 message_preview_fn.call(clicked, content, bid);
2867 setTimeout(() => {
2868 jQuery(preview_btn).vboContextMenu('destroy');
2869 }, 500);
2870 },
2871 },
2872 ],
2873 });
2874 jQuery(preview_btn).vboContextMenu('show');
2875 } catch(e) {
2876 // fallback on regular preview
2877 console.error(e);
2878 message_preview_fn.call(clicked, content);
2879 }
2880 },
2881 homelogo: function(clicked) {
2882 VBOCore.doAjax('$ajax_logo_url', {}, (resp) => {
2883 try {
2884 this.quill.insertEmbed(this.quill.getSelection().index, 'image', resp.url);
2885 } catch(e) {
2886 alert('Generic logo image error');
2887 }
2888 }, (err) => {
2889 console.log(err);
2890 alert(err.responseText);
2891 });
2892 },
2893 genai: function(clicked) {
2894 let visualEditor = this.quill;
2895 let cursorPosition = visualEditor.getSelection().index;
2896
2897 const vboVisualEditorGenaiGetContentFn = (e) => {
2898 // check if any data was sent within the event
2899 if (e && e.detail?.content) {
2900 // set the generated and picked content to editor
2901 let ai_content = e.detail.content;
2902 if ((e.detail?.type || '') == 'html') {
2903 // convert HTML content into Delta for the Visual Editor
2904 let delta = visualEditor.clipboard.convert(ai_content);
2905 // set (replace) editor HTML content (2nd argument "source" should be "api" so that the "text-change" event will fire)
2906 visualEditor.setContents(delta, 'api');
2907 } else {
2908 // default to plain text
2909 visualEditor.insertText(cursorPosition, ai_content, 'user');
2910 cursorPosition += ai_content.length + 1;
2911 visualEditor.setSelection(cursorPosition, 'silent');
2912 }
2913 }
2914 };
2915
2916 // register event to receive the Gen-AI content picked
2917 document.addEventListener('vbo-ai-tools-writer-content-picked', vboVisualEditorGenaiGetContentFn);
2918
2919 // register listener to un-register the needed events
2920 document.addEventListener('vbo-ai-tools-writer-content-dismissed', function vboVisualEditorGenaiDismissedFn(e) {
2921 // un-register the events asynchronously to avoid unexpected behaviors
2922 setTimeout(() => {
2923 // make sure the same event will not trigger again
2924 e.target.removeEventListener(e.type, vboVisualEditorGenaiDismissedFn);
2925
2926 // unregister the event for getting content data
2927 document.removeEventListener('vbo-ai-tools-writer-content-picked', vboVisualEditorGenaiGetContentFn);
2928 });
2929 });
2930
2931 // default prompt
2932 let writer_prompt = $gen_ai_use_prompt;
2933
2934 // render modal widget
2935 VBOCore.handleDisplayWidgetNotification({
2936 widget_id: 'aitools',
2937 }, {
2938 scope: 'writer',
2939 prompt: {
2940 message: (writer_prompt || Joomla.JText._('VBO_AITOOL_WRITER_DEF_PROMPT')),
2941 submit: 0,
2942 },
2943 modal_options: {
2944 suffix: 'vbo-ai-tools-writer-inner',
2945 title: Joomla.JText._('VBO_GEN_CONTENT') + ' - ' + Joomla.JText._('VBO_AI_LABEL_DEF'),
2946 lock_scroll: false,
2947 enlargeable: false,
2948 minimizeable: false,
2949 dismiss_event: 'vbo-ai-tools-writer-content-picked',
2950 dismissed_event: 'vbo-ai-tools-writer-content-dismissed',
2951 },
2952 });
2953 }
2954 };
2955 var visual_editor_ext_opts = $editor_opts_str;
2956 visual_editor_ext_opts['modules']['toolbar']['handlers'] = visual_editor_handlers;
2957 var visual_editor = new Quill('#$editor_id', visual_editor_ext_opts);
2958 var editor_content = jQuery('textarea#{$attrs['id']}').val();
2959 if (editor_content && editor_content.length) {
2960 if (editor_content.indexOf('<') >= 0) {
2961 // replace special tags
2962 editor_content = editor_content.replace(/([^"']|^)({(?:condition: ?)?[a-z0-9_]{5,64}})([^"']|$)/g, function(match, before, tag, after) {
2963 return before + '<strong class="vbo-editor-hl-specialtag">' + tag + '</strong>' + after;
2964 });
2965 var editor_delta = visual_editor.clipboard.convert(editor_content);
2966 // set editor HTML content
2967 visual_editor.setContents(editor_delta, 'api');
2968 } else {
2969 // set text content
2970 visual_editor.setText(editor_content, 'silent');
2971 }
2972 }
2973 visual_editor.on('text-change', function(delta, source) {
2974 jQuery('textarea#{$attrs['id']}').val(visual_editor.root.innerHTML);
2975 });
2976 jQuery('textarea#{$attrs['id']}').on('change', function() {
2977 var editor_content = jQuery(this).val();
2978 var editor_delta = visual_editor.clipboard.convert(editor_content);
2979 visual_editor.setContents(editor_delta, 'silent');
2980 });
2981 try {
2982 // push editor instance to the pool
2983 VikContentBuilder.pushEditor(visual_editor);
2984 } catch(e) {
2985 console.error('Could not push new visual editor instance', e);
2986 }
2987 setTimeout(() => {
2988 jQuery('.vik-contentbuilder-switcher-btn-active').trigger('click');
2989 });
2990 });
2991 </script>
2992 HTML;
2993
2994 // return the necessary HTML string to be displayed
2995 return $editor;
2996 }
2997
2998 /**
2999 * Loads the necessary assets to render context menus.
3000 *
3001 * @return void
3002 *
3003 * @since 1.16.0 (J) - 1.6.0 (WP)
3004 */
3005 public function loadContextMenuAssets()
3006 {
3007 static $loaded = null;
3008
3009 if ($loaded) {
3010 return;
3011 }
3012
3013 $dark_mode = 'null';
3014
3015 if (JFactory::getApplication()->isClient('administrator')) {
3016 // get appearance preference
3017 $app_pref = VikBooking::getAppearancePref();
3018
3019 if ($app_pref == 'light') {
3020 $dark_mode = 'false';
3021 } elseif ($app_pref == 'dark') {
3022 $dark_mode = 'true';
3023 }
3024 }
3025
3026 $this->addScript(VBO_ADMIN_URI . 'resources/contextmenu.js');
3027
3028 $doc = JFactory::getDocument();
3029
3030 $doc->addStyleSheet(VBO_ADMIN_URI . 'resources/contextmenu.css');
3031 $doc->addScriptDeclaration(
3032 <<<JS
3033 (function($) {
3034 'use strict';
3035
3036 $(function() {
3037 $.vboContextMenu.defaults.darkMode = {$dark_mode};
3038 $.vboContextMenu.defaults.class = 'vbo-dropdown-cxmenu';
3039 });
3040 })(jQuery);
3041 JS
3042 );
3043
3044 $loaded = 1;
3045 }
3046
3047 /**
3048 * Loads the assets necessary to render a phone input field.
3049 *
3050 * @return void
3051 *
3052 * @since 1.16.0 (J) - 1.6.0 (WP)
3053 */
3054 public function loadPhoneInputFieldAssets()
3055 {
3056 static $loaded = null;
3057
3058 if ($loaded) {
3059 return;
3060 }
3061
3062 $loaded = 1;
3063
3064 $document = JFactory::getDocument();
3065
3066 $document->addStyleSheet(VBO_SITE_URI . 'resources/intlTelInput.css');
3067 $document->addScript(VBO_SITE_URI . 'resources/intlTelInput.js');
3068 }
3069 }
3070