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

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

3,076 lines 97.6 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 // choices name data attribute
1232 $sel2ChoiceName = preg_replace('/[^a-z0-9\-\_]+/i', '', (string) ($options['attributes']['data-choice'] ?? $options['attributes']['name'] ?? ''));
1233
1234 // build script declaration
1235 $js_decl = <<<JAVASCRIPT
1236 jQuery(function() {
1237 jQuery('select#{$options['id']}').select2({
1238 width: '$sel2width',
1239 allowClear: $clearable_str,
1240 data: $data_sources_str,
1241 placeholder: $placeholder,
1242 templateResult: (element) => {
1243 let isNested = element?._nested;
1244 if (element.img) {
1245 return jQuery('<span data-choice="$sel2ChoiceName" class="vbo-sel2-element-img' + (isNested ? ' vbo-sel2-element-nested' : '') + '"><img src="' + element.img + '" /> <span>' + element.text + '</span></span>');
1246 }
1247 if (element.html) {
1248 return jQuery('<span data-choice="$sel2ChoiceName" class="vbo-sel2-element-img' + (isNested ? ' vbo-sel2-element-nested' : '') + '">' + element.html + ' <span>' + element.text + '</span></span>');
1249 }
1250 if (isNested) {
1251 return jQuery('<span data-choice="$sel2ChoiceName" class="vbo-sel2-element-nested"><span>' + element.text + '</span></span>');
1252 }
1253 return element.text;
1254 },
1255 $selectionFn
1256 });
1257 });
1258 JAVASCRIPT;
1259
1260 if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
1261 // concatenate script to HTML string when doing an AJAX request
1262 $html .= "\n" . '<script>' . $js_decl . '</script>';
1263 } else {
1264 // add script declaration to document
1265 JFactory::getDocument()->addScriptDeclaration($js_decl);
1266 }
1267
1268 // return the HTML string to be displayed
1269 return $html;
1270 }
1271
1272 /**
1273 * Renders a select2 component to display searchable elements with thumbnails.
1274 *
1275 * @param array $options Associative list of dropdown and search options.
1276 *
1277 * @return string The HTML string necessary to render the dropdown.
1278 *
1279 * @since 1.18.0 (J) - 1.8.0 (WP)
1280 */
1281 public function renderSearchElementsDropDown(array $options = [])
1282 {
1283 if (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'bookings') {
1284 // search bookings endpoint
1285 $options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.bookings_search');
1286 } elseif (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'customers') {
1287 // search customers endpoint
1288 $options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.customer_elements_search');
1289 }
1290
1291 if (empty($options['endpoint'])) {
1292 // abort
1293 return '';
1294 }
1295
1296 // set AJAX endpoint for searching
1297 $endpoint = $options['endpoint'];
1298
1299 if ($options['load_assets'] ?? true) {
1300 // load select2 assets
1301 $this->loadSelect2();
1302 }
1303
1304 if (!($options['id'] ?? null)) {
1305 // the ID attribute is mandatory
1306 $options['id'] = uniqid('ldd_');
1307 }
1308
1309 // build attributes list
1310 $attributes = array_merge([
1311 'id' => $options['id'],
1312 ], ($options['attributes'] ?? []));
1313
1314 // build attributes string
1315 $attr_str = implode(' ', array_map(function($name, $value) {
1316 return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
1317 }, array_keys($attributes), array_values($attributes)));
1318
1319 // build data sources
1320 $data_sources = [];
1321
1322 // check for default option(s) selected status
1323 $selected_options = [];
1324 if (is_array($options['selected_values'] ?? null) && $options['selected_values']) {
1325 $selected_options = $options['selected_values'];
1326 } elseif (($options['selected_value'] ?? null)) {
1327 $selected_options[] = $options['selected_value'];
1328 }
1329 foreach ($selected_options as $sel_option) {
1330 if (is_array($sel_option) || is_object($sel_option)) {
1331 // element object expected
1332 $sel_option = (array) $sel_option;
1333 if (isset($sel_option['id']) && isset($sel_option['text'])) {
1334 // push valid data source
1335 $sel_option['selected'] = true;
1336 $data_sources[] = $sel_option;
1337 }
1338 } else {
1339 // selected ID expected, push a data source object with limited information
1340 $data_sources[] = [
1341 'id' => $sel_option,
1342 'text' => $sel_option,
1343 'selected' => true,
1344 ];
1345 }
1346 }
1347
1348 // data sources JSON encoded string
1349 $data_sources_str = json_encode($data_sources);
1350
1351 // empty option tag
1352 $empty_option = '';
1353 if (!($options['attributes']['multiple'] ?? null)) {
1354 $empty_option = '<option></option>';
1355 }
1356
1357 // clearing allowed
1358 $clearable = (bool) ($options['allow_clear'] ?? 1);
1359 $clearable_str = $clearable ? 'true' : 'false';
1360
1361 // placeholder text
1362 $placeholder = json_encode($options['placeholder'] ?? '');
1363
1364 // minimum input length for searching
1365 $min_inp_len = 1;
1366
1367 // search language definitions
1368 $lang_error_loading = json_encode($options['language']['error'] ?? JText::translate('VBO_ERR_LOAD_RESULTS'));
1369 $lang_no_results = json_encode($options['language']['noresults'] ?? JText::translate('VBO_NO_RECORDS_FOUND'));
1370 $lang_searching = json_encode($options['language']['searching'] ?? JText::translate('VBO_SEARCHING'));
1371 // this language definition is NOT quoted, because it is not JSON-encoded
1372 $lang_inptooshort = $min_inp_len > 1 && ($options['language']['inptooshort'] ?? '') ? $options['language']['inptooshort'] : '';
1373
1374 // selection/result with ID
1375 $selection_with_id = ($options['selected_id'] ?? null) ? 'true' : 'false';
1376
1377 // selection extra class
1378 $selection_class = $options['selection_class'] ?? '';
1379
1380 // selection click open widget
1381 $selection_click_widget = $options['selection_click_widget'] ?? '';
1382
1383 // selection dispatch event
1384 $selection_event = $options['selection_event'] ?? '';
1385
1386 // select2 width
1387 $sel2width = $options['width'] ?? 'resolve';
1388
1389 // build HTML string
1390 $html = <<<HTML
1391 <select {$attr_str}>{$empty_option}</select>
1392 HTML;
1393
1394 // template selection function
1395 $selectionFn = '';
1396 if ($options['style_selection'] ?? null) {
1397 $defaultSelectionIcn = $options['default_selection_icon'] ?? '';
1398 $selectionFn = <<<JAVASCRIPT
1399 templateSelection: (element) => {
1400 if (!element.id) {
1401 return element.text;
1402 }
1403 let sel_elem = jQuery('<span></span>')
1404 .addClass(('vbo-sel2-selected-search-elem $selection_class').trim())
1405 .text(element.text + ($selection_with_id ? ' #' + element.id : ''));
1406 if (element.img) {
1407 let avatar_elem = jQuery('<img/>')
1408 .addClass('vbo-sel2-selected-search-elem-avatar')
1409 .attr('src', element.img);
1410 if (element.img_title) {
1411 avatar_elem.attr('title', element.img_title);
1412 }
1413 sel_elem.prepend(avatar_elem);
1414 } else if (element.icon_class) {
1415 let icn_wrap = jQuery('<span></span>')
1416 .addClass('vbo-sel2-selected-search-elem-avatar');
1417 let icn_elem = jQuery('<i></i>')
1418 .addClass(element.icon_class);
1419 icn_wrap.append(icn_elem);
1420 sel_elem.prepend(icn_wrap);
1421 } else if ('$defaultSelectionIcn') {
1422 let icn_elem = jQuery('<i></i>')
1423 .addClass('$defaultSelectionIcn')
1424 .addClass('vbo-sel2-selected-search-elem-avatar');
1425 sel_elem.prepend(icn_elem);
1426 }
1427 if ("$selection_click_widget" == 'booking_details') {
1428 sel_elem.on('click', () => {
1429 VBOCore.handleDisplayWidgetNotification({
1430 widget_id: 'booking_details',
1431 }, {
1432 booking_id: element.id,
1433 modal_options: {
1434 suffix: 'vbo-booking-details-inner',
1435 body_prepend: false,
1436 enlargeable: false,
1437 minimizeable: false,
1438 },
1439 });
1440 });
1441 }
1442 return sel_elem;
1443 }
1444 JAVASCRIPT;
1445 }
1446
1447 // build script declaration
1448 $js_decl = <<<JAVASCRIPT
1449 jQuery(function() {
1450 jQuery('select#{$options['id']}').select2({
1451 width: '$sel2width',
1452 allowClear: $clearable_str,
1453 data: $data_sources_str,
1454 placeholder: $placeholder,
1455 minimumInputLength: $min_inp_len,
1456 language: {
1457 errorLoading: () => {
1458 return $lang_error_loading;
1459 },
1460 noResults: () => {
1461 return $lang_no_results;
1462 },
1463 searching: () => {
1464 return $lang_searching;
1465 },
1466 inputTooShort: () => {
1467 return "$lang_inptooshort";
1468 },
1469 },
1470 ajax: {
1471 delay: 350,
1472 url: "$endpoint",
1473 dataType: 'json',
1474 },
1475 templateResult: (element) => {
1476 if (!element.id) {
1477 return element.text;
1478 }
1479 let search_elem = jQuery('<span></span>')
1480 .addClass('vbo-sel2-search-elem');
1481
1482 let search_avatar = jQuery('<span></span>')
1483 .addClass('vbo-sel2-search-elem-avatar');
1484
1485 let elem_name = jQuery('<span></span>')
1486 .addClass('vbo-sel2-search-elem-name')
1487 .text(element.text + ($selection_with_id ? ' #' + element.id : ''));
1488
1489 if (element.img) {
1490 search_avatar.append('<img src="' + element.img + '" ' + (element.img_title ? 'title="' + element.img_title + '" ' : '') + '/>');
1491 } else if (element.icon_class) {
1492 search_avatar.append('<i class="' + element.icon_class + '"></i>');
1493 }
1494
1495 search_avatar.append(elem_name);
1496 search_elem.append(search_avatar);
1497
1498 return search_elem;
1499 },
1500 $selectionFn
1501 });
1502 if ("$selection_event") {
1503 jQuery('select#{$options['id']}').on('select2:select', (e) => {
1504 let element = e?.params?.data || e;
1505 VBOCore.emitEvent("$selection_event", {
1506 element: element,
1507 });
1508 });
1509 }
1510 });
1511 JAVASCRIPT;
1512
1513 if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
1514 // concatenate script to HTML string when doing an AJAX request
1515 $html .= "\n" . '<script>' . $js_decl . '</script>';
1516 } else {
1517 // add script declaration to document
1518 JFactory::getDocument()->addScriptDeclaration($js_decl);
1519 }
1520
1521 // return the HTML string to be displayed
1522 return $html;
1523 }
1524
1525 /**
1526 * Loads the assets for setting up the VBOCore JS in the site section.
1527 *
1528 * @param array $options Associative list of loading options.
1529 *
1530 * @return void
1531 *
1532 * @since 1.17.4 (J) - 1.7.4 (WP)
1533 */
1534 public function loadCoreJS(array $options = [])
1535 {
1536 static $corejs_loaded = null;
1537
1538 if ($corejs_loaded) {
1539 // loaded flag
1540 return;
1541 }
1542
1543 // cache loaded flag
1544 $corejs_loaded = 1;
1545
1546 // add script
1547 $this->addScript(VBO_ADMIN_URI . 'resources/vbocore.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
1548
1549 if ($options) {
1550 $core_options = json_encode((object) $options, JSON_PRETTY_PRINT);
1551
1552 // add script declaration to document
1553 JFactory::getDocument()->addScriptDeclaration(
1554 <<<JS
1555 jQuery(function() {
1556 VBOCore.setOptions($core_options);
1557 });
1558 JS
1559 );
1560 }
1561 }
1562
1563 /**
1564 * Loads the assets for rendering the signature pad.
1565 *
1566 * @param array $options Associative list of loading options.
1567 *
1568 * @return void
1569 *
1570 * @since 1.17.4 (J) - 1.7.4 (WP)
1571 */
1572 public function loadSignaturePad(array $options = [])
1573 {
1574 static $signpad_loaded = null;
1575
1576 if ($signpad_loaded) {
1577 // loaded flag
1578 return;
1579 }
1580
1581 // cache loaded flag
1582 $signpad_loaded = 1;
1583
1584 // add script
1585 $this->addScript(VBO_SITE_URI . 'resources/signature_pad.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
1586 }
1587
1588 /**
1589 * Loads the assets solely needed to render the DRP calendar.
1590 *
1591 * @param array $options Associative list of loading options.
1592 *
1593 * @return void
1594 *
1595 * @since 1.17.3 (J) - 1.7.3 (WP)
1596 */
1597 public function loadDatesRangePicker(array $options = [])
1598 {
1599 static $drp_loaded = null;
1600
1601 if ($drp_loaded) {
1602 // loaded flag
1603 return;
1604 }
1605
1606 // cache loaded flag
1607 $drp_loaded = 1;
1608
1609 // add DRP script
1610 $this->addScript(VBO_SITE_URI . 'resources/datesrangepicker.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
1611
1612 // load JS lang defs
1613 JText::script('VBPICKUPROOM');
1614 JText::script('VBRETURNROOM');
1615 JText::script('VBO_MIN_STAY_NIGHTS');
1616 JText::script('VBO_CLEAR_DATES');
1617 JText::script('VBO_CLOSE');
1618 }
1619
1620 /**
1621 * Loads the necessary JS and CSS assets to render the jQuery UI Datepicker calendar.
1622 * It is also possible to load the assets for the DatesRangePicker extension.
1623 *
1624 * @param array $options Associative list of loading options.
1625 *
1626 * @return void
1627 *
1628 * @since 1.1.0
1629 * @since 1.15.0 (J) - 1.5.0 (WP) the lang definitions work for both front and back -ends.
1630 * @since 1.17.3 (J) - 1.7.3 (WP) added support for the DatesRangePicker extension.
1631 */
1632 public function loadDatePicker(array $options = [])
1633 {
1634 static $datepicker_loaded = null;
1635
1636 if ($datepicker_loaded) {
1637 // loaded flag
1638 return;
1639 }
1640
1641 $document = JFactory::getDocument();
1642 $document->addStyleSheet(VBO_SITE_URI . 'resources/jquery-ui.min.css');
1643
1644 JHtml::fetch('jquery.framework', true, true);
1645 $this->addScript(VBO_SITE_URI . 'resources/jquery-ui.min.js');
1646
1647 if (!strcasecmp(($options['type'] ?? ''), 'dates_range')) {
1648 // load DRP calendar assets
1649 $this->loadDatesRangePicker($options);
1650 }
1651
1652 $vbo_df = VikBooking::getDateFormat();
1653 $juidf = $vbo_df == "%d/%m/%Y" ? 'dd/mm/yy' : ($vbo_df == "%m/%d/%Y" ? 'mm/dd/yy' : 'yy/mm/dd');
1654
1655 $is_rtl_lan = false;
1656 $now_lang = JFactory::getLanguage();
1657 if (method_exists($now_lang, 'isRtl')) {
1658 $is_rtl_lan = $now_lang->isRtl();
1659 }
1660
1661 // list of week day names for "short" and "min" versions
1662 $day_names_short_list = [
1663 JText::translate('SUN'),
1664 JText::translate('MON'),
1665 JText::translate('TUE'),
1666 JText::translate('WED'),
1667 JText::translate('THU'),
1668 JText::translate('FRI'),
1669 JText::translate('SAT'),
1670 ];
1671 $day_names_min_list = [];
1672 foreach ($day_names_short_list as $wdn) {
1673 $day_names_min_list[] = $this->safeSubstr($wdn, 2);
1674 }
1675
1676 // ensure this language does not produce conflicting week-days "min"
1677 $day_names_min_list = array_unique($day_names_min_list);
1678 if (count($day_names_min_list) != count($day_names_short_list)) {
1679 // fallback onto the "short" week-days list to avoid conflicts with this language
1680 $day_names_min_list = $day_names_short_list;
1681 }
1682
1683 // build default regional values for datepicker
1684 $vbo_dp_regional_vals = [
1685 'closeText' => JText::translate('VBJQCALDONE'),
1686 'prevText' => JText::translate('VBJQCALPREV'),
1687 'nextText' => JText::translate('VBJQCALNEXT'),
1688 'currentText' => JText::translate('VBJQCALTODAY'),
1689 'monthNames' => [
1690 JText::translate('JANUARY'),
1691 JText::translate('FEBRUARY'),
1692 JText::translate('MARCH'),
1693 JText::translate('APRIL'),
1694 JText::translate('MAY'),
1695 JText::translate('JUNE'),
1696 JText::translate('JULY'),
1697 JText::translate('AUGUST'),
1698 JText::translate('SEPTEMBER'),
1699 JText::translate('OCTOBER'),
1700 JText::translate('NOVEMBER'),
1701 JText::translate('DECEMBER'),
1702 ],
1703 'monthNamesShort' => [
1704 JText::translate('JANUARY_SHORT'),
1705 JText::translate('FEBRUARY_SHORT'),
1706 JText::translate('MARCH_SHORT'),
1707 JText::translate('APRIL_SHORT'),
1708 JText::translate('MAY_SHORT'),
1709 JText::translate('JUNE_SHORT'),
1710 JText::translate('JULY_SHORT'),
1711 JText::translate('AUGUST_SHORT'),
1712 JText::translate('SEPTEMBER_SHORT'),
1713 JText::translate('OCTOBER_SHORT'),
1714 JText::translate('NOVEMBER_SHORT'),
1715 JText::translate('DECEMBER_SHORT'),
1716 ],
1717 'dayNames' => [
1718 JText::translate('SUNDAY'),
1719 JText::translate('MONDAY'),
1720 JText::translate('TUESDAY'),
1721 JText::translate('WEDNESDAY'),
1722 JText::translate('THURSDAY'),
1723 JText::translate('FRIDAY'),
1724 JText::translate('SATURDAY'),
1725 ],
1726 'dayNamesShort' => $day_names_short_list,
1727 'dayNamesMin' => $day_names_min_list,
1728 'weekHeader' => JText::translate('VBJQCALWKHEADER'),
1729 'dateFormat' => $juidf,
1730 'firstDay' => VikBooking::getFirstWeekDay(),
1731 'isRTL' => $is_rtl_lan,
1732 'showMonthAfterYear' => false,
1733 'yearSuffix' => '',
1734 ];
1735
1736 $ldecl = '
1737 jQuery(function($) {' . "\n" . '
1738 $.datepicker.regional["vikbooking"] = ' . json_encode($vbo_dp_regional_vals) . ';
1739 $.datepicker.setDefaults($.datepicker.regional["vikbooking"]);' . "\n" . '
1740 });';
1741
1742 /**
1743 * Trigger event to allow third party plugins to overwrite the JS declaration for the datepicker.
1744 *
1745 * @since 1.16.0 (J) - 1.6.0 (WP)
1746 */
1747 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeDeclareDatepickerRegionalVikBooking', [$is_rtl_lan, $now_lang->getTag(), &$ldecl]);
1748
1749 // add script declaration
1750 $document->addScriptDeclaration($ldecl);
1751
1752 // cache loaded flag
1753 $datepicker_loaded = 1;
1754 }
1755
1756 /**
1757 * Loads the CMS's native datepicker calendar.
1758 *
1759 * @since 1.10
1760 */
1761 public function getCalendar($val, $name, $id = null, $df = null, array $attributes = array())
1762 {
1763 if ($df === null)
1764 {
1765 $df = VikBooking::getDateFormat();
1766 }
1767
1768 return parent::calendar($val, $name, $id, $df, $attributes);
1769 }
1770
1771 /**
1772 * Returns a masked e-mail address. The e-mail are masked using
1773 * a technique to encode the bytes in hexadecimal representation.
1774 * The chunk of the masked e-mail will be also encoded to be HTML readable.
1775 *
1776 * @param string $email The e-mail to mask.
1777 * @param boolean $reverse True to reverse the e-mail address.
1778 * Only if the e-mail is not contained into an attribute.
1779 *
1780 * @return string The masked e-mail address.
1781 */
1782 public function maskMail($email, $reverse = false)
1783 {
1784 if ($reverse)
1785 {
1786 // reverse the e-mail address
1787 $email = strrev($email);
1788 }
1789
1790 // converts the e-mail address from bin to hex
1791 $email = bin2hex($email);
1792 // append ;&#x sequence after every chunk of the masked e-mail
1793 $email = chunk_split($email, 2, ";&#x");
1794 // prepend &#x sequence before the address and trim the ending sequence
1795 $email = "&#x" . substr($email, 0, -3);
1796
1797 return $email;
1798 }
1799
1800 /**
1801 * Returns a safemail tag to avoid the bots spoof a plain address.
1802 *
1803 * @param string $email The e-mail address to mask.
1804 * @param boolean $mail_to True if the address should be wrapped
1805 * within a "mailto" link.
1806 *
1807 * @return string The HTML tag containing the masked address.
1808 *
1809 * @uses maskMail()
1810 */
1811 public function safeMailTag($email, $mail_to = false)
1812 {
1813 // include the CSS declaration to reverse the text contained in the <safemail> tags
1814 JFactory::getDocument()->addStyleDeclaration('safemail {direction: rtl;unicode-bidi: bidi-override;}');
1815
1816 // mask the reversed e-mail address
1817 $masked = $this->maskMail($email, true);
1818
1819 // include the address into a custom <safemail> tag
1820 $tag = "<safemail>$masked</safemail>";
1821
1822 if ($mail_to)
1823 {
1824 // mask the address for mailto command (do not use reverse)
1825 $mailto = $this->maskMail($email);
1826
1827 // wrap the safemail tag within a mailto link
1828 $tag = "<a href=\"mailto:$mailto\" class=\"mailto\">$tag</a>";
1829 }
1830
1831 return $tag;
1832 }
1833
1834 /**
1835 * Loads and echoes the script necessary to render the Fancybox
1836 * plugin for jQuery to open images or iframes within a modal box.
1837 * This resolves conflicts with some Bootstrap or Joomla (4) versions
1838 * that do not support the old-native CSS class .modal with "behavior.modal".
1839 * Mainly made to open pictures in a modal box, so the default "type" is set to "image".
1840 * By passing a custom $opts string, the "type" property could be set to "iframe", but
1841 * in this case it's better to use the other method of this class (Jmodal).
1842 * The base jQuery library should be already loaded when using this method.
1843 *
1844 * @param string $selector The jQuery selector to trigger Fancybox.
1845 * @param string $opts The options object for the Fancybox setup.
1846 * @param boolean $reloadfunc If true, an additional function is included in the script
1847 * to apply again Fancybox to newly added images to the DOM (via Ajax).
1848 *
1849 * @return void
1850 *
1851 * @uses addScript()
1852 */
1853 public function prepareModalBox($selector = '.vbomodal', $opts = '', $reloadfunc = false)
1854 {
1855 if (empty($opts)) {
1856 $opts = '{
1857 "helpers": {
1858 "overlay": {
1859 "locked": false
1860 }
1861 },
1862 "width": "70%",
1863 "height": "75%",
1864 "autoScale": true,
1865 "transitionIn": "none",
1866 "transitionOut": "none",
1867 "padding": 0,
1868 "type": "image"
1869 }';
1870 }
1871 $document = JFactory::getDocument();
1872 $document->addStyleSheet(VBO_SITE_URI.'resources/jquery.fancybox.css');
1873 $this->addScript(VBO_SITE_URI.'resources/jquery.fancybox.js');
1874
1875 $reloadjs = '
1876 function reloadFancybox() {
1877 jQuery("'.$selector.'").fancybox('.$opts.');
1878 }
1879 ';
1880 $js = '
1881 <script type="text/javascript">
1882 jQuery(function() {
1883 jQuery("'.$selector.'").fancybox('.$opts.');
1884 });'.($reloadfunc ? $reloadjs : '').'
1885 </script>';
1886
1887 echo $js;
1888 }
1889
1890 /**
1891 * Method used to handle the reCAPTCHA events.
1892 *
1893 * @param string $event The reCAPTCHA event to trigger.
1894 * Here's the list of the accepted events:
1895 * - display Returns the HTML used to
1896 * display the reCAPTCHA input.
1897 * - check Validates the POST data to make sure
1898 * the reCAPTCHA input was checked.
1899 * @param array $options A configuration array.
1900 *
1901 * @return mixed The event response.
1902 *
1903 * @since 1.2.3
1904 * @wponly the Joomla integration differs
1905 */
1906 public function reCaptcha($event = 'display', array $options = array())
1907 {
1908 $response = null;
1909 // an optional configuration array (just leave empty)
1910 $options = array();
1911 // trigger reCAPTCHA display event to fill $response var
1912 do_action_ref_array('vik_recaptcha_' . $event, array(&$response, $options));
1913 // display reCAPTCHA by echoing it (empty in case reCAPTCHA is not available)
1914 return $response;
1915 }
1916
1917 /**
1918 * Checks if the com_user captcha is configured.
1919 * In case the parameter is set to global, the default one
1920 * will be retrieved.
1921 *
1922 * @param string $plugin The plugin name to check ('recaptcha' by default).
1923 *
1924 * @return boolean True if configured, otherwise false.
1925 *
1926 * @since 1.2.3
1927 * @wponly the Joomla integration differs
1928 */
1929 public function isCaptcha($plugin = 'recaptcha')
1930 {
1931 return apply_filters('vik_' . $plugin . '_on', false);
1932 }
1933
1934 /**
1935 * Checks if the global captcha is configured.
1936 *
1937 * @param string $plugin The plugin name to check ('recaptcha' by default).
1938 *
1939 * @return boolean True if configured, otherwise false.
1940 *
1941 * @since 1.2.3
1942 */
1943 public function isGlobalCaptcha($plugin = 'recaptcha')
1944 {
1945 return $this->isCaptcha($plugin);
1946 }
1947
1948 /**
1949 * Method used to obtain a WordPress media form field.
1950 *
1951 * @return string The media in HTML.
1952 *
1953 * @since 1.3.0
1954 */
1955 public function getMediaField($name, $value = null, array $data = array())
1956 {
1957 // check if WordPress is installed
1958 if (VBOPlatformDetection::isWordPress())
1959 {
1960 add_action('admin_enqueue_scripts', function() {
1961 wp_enqueue_media();
1962 });
1963
1964 // import form field class
1965 JLoader::import('adapter.form.field');
1966
1967 // create XML field manifest
1968 $xml = "<field name=\"$name\" type=\"media\" modowner=\"vikbooking\" />";
1969
1970 // instantiate field
1971 $field = JFormField::getInstance(simplexml_load_string($xml));
1972
1973 // overwrite name and value within data
1974 $data['name'] = $name;
1975 $data['value'] = $value;
1976
1977 // inject display data within field instance
1978 foreach ($data as $k => $v)
1979 {
1980 $field->bind($v, $k);
1981 }
1982
1983 // render field
1984 return $field->render();
1985 }
1986
1987 // fallback to Joomla
1988
1989 // init media field
1990 $field = new JFormFieldMedia(null, $value);
1991 // setup an empty form as placeholder
1992 $field->setForm(new JForm('vikbooking.media'));
1993
1994 // force field attributes
1995 $data['name'] = $name;
1996 $data['value'] = $value;
1997
1998 if (empty($data['previewWidth']))
1999 {
2000 // there is no preview width, set a defualt value
2001 // to make the image visible within the popover
2002 $data['previewWidth'] = 480;
2003 }
2004
2005 // render the field
2006 return $field->render('joomla.form.field.media', $data);
2007 }
2008
2009 /**
2010 * Displays a multi-state toggle switch element with unlimited buttons.
2011 * Custom values, contents, labels, attributes and JS events can be attached
2012 * to each button. VCM will use this same method.
2013 *
2014 * @param string $name the input name equal for all radio buttons.
2015 * @param string $value the current input field value to be pre-selected.
2016 * @param array $values list of radio buttons with each value.
2017 * @param array $labels list of contents for each button trigger.
2018 * @param array $attrs list of associative array attributes for each button.
2019 * @param array $wrap list of associative array attributes for the wrapper.
2020 *
2021 * @return string the necessary HTML to render the multi-state toggle switch.
2022 *
2023 * @since 1.15.0 (J) - 1.5.0 (WP)
2024 */
2025 public function multiStateToggleSwitchField($name, $value, $values = array(), $labels = array(), $attrs = array(), $wrap = array())
2026 {
2027 static $tooltip_js_declared = null;
2028
2029 // whether tooltip for titles is needed
2030 $needs_tooltip = false;
2031
2032 // HTML container
2033 $multi_state_switch = '';
2034
2035 if (!is_array($values) || !count($values)) {
2036 // values must be set or we don't know what buttons to display
2037 return $multi_state_switch;
2038 }
2039
2040 // build default classes for the tri-state toggle switch (with 3 buttons)
2041 $def_tristate_cls = array(
2042 'vik-multiswitch-radiobtn-on',
2043 'vik-multiswitch-radiobtn-def',
2044 'vik-multiswitch-radiobtn-off',
2045 );
2046
2047 // start wrapper
2048 $multi_state_switch .= "\n" . '<div class="vik-multiswitch-wrap' . (isset($wrap['class']) ? (' ' . $wrap['class']) : '') . '">' . "\n";
2049
2050 foreach ($values as $btn_k => $btn_val) {
2051 // build default classes for button label
2052 $btn_classes = array('vik-multiswitch-radiobtn');
2053 if (isset($def_tristate_cls[$btn_k])) {
2054 // push default class for a 3-state toggle switch
2055 array_push($btn_classes, $def_tristate_cls[$btn_k]);
2056 }
2057 // check if additional custom classes have been defined for this button
2058 if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['label_class']) && !empty($attrs[$btn_k]['label_class'])) {
2059 if (is_array($attrs[$btn_k]['label_class'])) {
2060 // list of additional classes for this button
2061 $btn_classes = array_merge($btn_classes, $attrs[$btn_k]['label_class']);
2062 } elseif (is_string($attrs[$btn_k]['label_class'])) {
2063 // multiple classes should be space-separated
2064 array_push($btn_classes, $attrs[$btn_k]['label_class']);
2065 }
2066 }
2067
2068 // check title as first thing, even though this is passed along with the labels
2069 $label_title = '';
2070 if (isset($labels[$btn_k]) && !is_scalar($labels[$btn_k]) && isset($labels[$btn_k]['title'])) {
2071 $needs_tooltip = true;
2072 $label_title = ' title="' . addslashes(htmlentities($labels[$btn_k]['title'])) . '"';
2073 }
2074
2075 // start button label
2076 $multi_state_switch .= "\t" . '<label class="' . implode(' ', $btn_classes) . '"' . $label_title . '>' . "\n";
2077
2078 // check button input radio
2079 $radio_attributes = array();
2080 if (($value !== null && $value == $btn_val) || ($value === null && $btn_k === 0)) {
2081 // this radio button must be checked (pre-selected)
2082 $radio_attributes['checked'] = true;
2083 }
2084 // check if custom attributes were specified for this input
2085 if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['input'])) {
2086 // must be an associative array with key = attribute name, value = attribute value
2087 foreach ($attrs[$btn_k]['input'] as $attr_name => $attr_val) {
2088 // javascript events could be attached like 'onchange'=>'myCallback(this.value)'
2089 $radio_attributes[$attr_name] = $attr_val;
2090 }
2091 }
2092 $radio_attr_string = '';
2093 foreach ($radio_attributes as $attr_name => $attr_val) {
2094 if ($attr_val === true) {
2095 // short-attribute name, like "checked"
2096 $radio_attr_string .= $attr_name . ' ';
2097 continue;
2098 }
2099 $radio_attr_string .= $attr_name . '="' . $attr_val . '" ';
2100 }
2101 $multi_state_switch .= "\t\t" . '<input type="radio" name="' . $name . '" value="' . $btn_val . '" ' . $radio_attr_string . '/>' . "\n";
2102
2103 // add button trigger
2104 $multi_state_switch .= "\t\t" . '<span class="vik-multiswitch-trigger"></span>' . "\n";
2105
2106 // check button label text
2107 if (isset($labels[$btn_k])) {
2108 /**
2109 * By default, the buttons of the toggle switch use an animation,
2110 * which requires an absolute positioning of the "label-text".
2111 * For this reason, there cannot be a minimum width for these texts
2112 * and so the content should fit the default width. Usually, using
2113 * a font-awesome icon is the best content. For using literal texts,
2114 * like "Dark", "Light" etc.. the class "vik-multiswitch-noanimation"
2115 * should be passed to the button label text.
2116 */
2117 $label_txt = '';
2118 $label_class = '';
2119 if (!is_scalar($labels[$btn_k])) {
2120 // with an associative array we accept value, title and custom classes
2121 if (isset($labels[$btn_k]['value'])) {
2122 $label_txt = $labels[$btn_k]['value'];
2123 }
2124 if (isset($labels[$btn_k]['class'])) {
2125 $label_class = ' ' . ltrim($labels[$btn_k]['class']);
2126 }
2127 } else {
2128 // just a string, maybe with text or HTML mixed content
2129 $label_txt = $labels[$btn_k];
2130 }
2131 if (strlen($label_txt)) {
2132 // append button label text only if some text has been defined
2133 $multi_state_switch .= "\t\t" . '<span class="vik-multiswitch-txt' . $label_class . '">' . $label_txt . '</span>' . "\n";
2134 }
2135 }
2136
2137 // end button label
2138 $multi_state_switch .= "\t" . '</label>' . "\n";
2139 }
2140
2141 // end wrapper
2142 $multi_state_switch .= '</div>' . "\n";
2143
2144 // check tooltip JS rendering
2145 if (!$tooltip_js_declared && $needs_tooltip) {
2146 // turn static flag on
2147 $tooltip_js_declared = 1;
2148
2149 // add script declaration for JS rendering of tooltips
2150 $doc = JFactory::getDocument();
2151 $doc->addScriptDeclaration(
2152 <<<JS
2153 jQuery(function() {
2154 if (typeof jQuery.fn.tooltip === 'function') {
2155 jQuery('.vik-multiswitch-wrap label').tooltip();
2156 }
2157 });
2158 JS
2159 );
2160 }
2161
2162 return $multi_state_switch;
2163 }
2164
2165 /**
2166 * Returns a list of supported fonts for the third-party visual editor (Quill).
2167 *
2168 * @param bool $short_names whether to return short font names.
2169 *
2170 * @return array list of font family names or short names.
2171 *
2172 * @since 1.15.0 (J) - 1.5.0 (WP)
2173 */
2174 public function getVisualEditorFonts($short_names = false)
2175 {
2176 // supported fonts
2177 $font_families = ['Sans Serif', 'Arial', 'Courier', 'Garamond', 'Tahoma', 'Times New Roman', 'Verdana', 'Inconsolata', 'Sailec Light', 'Monospace'];
2178
2179 if (!$short_names) {
2180 // return the regular names to be displayed
2181 return $font_families;
2182 }
2183
2184 // return the "short" names of the supported fonts
2185 return array_map(function($font) {
2186 return str_replace(' ', '-', strtolower($font));
2187 }, $font_families);
2188 }
2189
2190 /**
2191 * Loads the necessary language definitions for the third-party visual editor (Quill).
2192 *
2193 * @return void
2194 *
2195 * @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
2196 */
2197 public function loadVisualEditorDefinitions()
2198 {
2199 static $loaded = null;
2200
2201 if ($loaded) {
2202 return;
2203 }
2204
2205 $loaded = 1;
2206
2207 // load language definitions for JS
2208 JText::script('VBO_CONT_WRAPPER');
2209 JText::script('VBO_CONT_WRAPPER_HELP');
2210 JText::script('VBO_GEN_CONTENT');
2211 JText::script('VBO_AI_LABEL_DEF');
2212 JText::script('VBO_AITOOL_WRITER_DEF_PROMPT');
2213 JText::script('VBANNULLA');
2214 }
2215
2216 /**
2217 * Loads the necessary assets for the third-party visual editor (Quill).
2218 *
2219 * @return void
2220 *
2221 * @since 1.15.0 (J) - 1.5.0 (WP)
2222 * @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
2223 */
2224 public function loadVisualEditorAssets()
2225 {
2226 static $loaded = null;
2227
2228 if ($loaded) {
2229 return;
2230 }
2231
2232 $loaded = 1;
2233
2234 // access the document
2235 $doc = JFactory::getDocument();
2236
2237 // load JS langs
2238 $this->loadVisualEditorDefinitions();
2239
2240 // build the list of font families
2241 $font_families = $this->getVisualEditorFonts();
2242 $font_shortfam = $this->getVisualEditorFonts(true);
2243 $js_font_names = json_encode($font_shortfam);
2244
2245 // build inline CSS styles
2246 $css_font_decl = '';
2247 foreach ($font_families as $k => $font_name) {
2248 $font_val = $font_shortfam[$k];
2249 $css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="' . $font_val . '"]::before,';
2250 $css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="' . $font_val . '"]::before {' . "\n";
2251 $css_font_decl .= 'content: "' . $font_name . '";' . "\n";
2252 $css_font_decl .= 'font-family: "' . $font_name . '";' . "\n";
2253 $css_font_decl .= '}' . "\n";
2254 }
2255
2256 $css_font_decl .= '
2257 .ql-picker.ql-specialtags .ql-picker-label {
2258 padding-right: 18px;
2259 }
2260 .ql-picker.ql-specialtags .ql-picker-label:before {
2261 content: "' . htmlspecialchars(JText::translate('VBO_CONDTEXT_TKN')) . '";
2262 }
2263 .ql-formats .ql-genai {
2264 width: auto !important;
2265 font-weight: bold;
2266 }
2267 ';
2268
2269 /**
2270 * Cache the pre-configuration CSS onto a file to allow AJAX requests
2271 * to properly load the asset inline within the response. Before this
2272 * change the styles used to be added as an inline style declaration.
2273 *
2274 * @since 1.16.7 (J) - 1.6.7 (WP)
2275 * @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
2276 */
2277 $cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
2278 $cached_preconfig_css_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css']);
2279 $cached_preconfig_css_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css';
2280 $cached_preconfig_css_ok = is_file($cached_preconfig_css_path);
2281
2282 if (!$cached_preconfig_css_ok) {
2283 // attempt to create the file
2284 $cached_preconfig_css_ok = JFile::write($cached_preconfig_css_path, $css_font_decl);
2285 }
2286
2287 if ($cached_preconfig_css_ok) {
2288 // load cached script file
2289 $doc->addStyleSheet($cached_preconfig_css_uri);
2290 } else {
2291 // revert to append CSS style declaration to document
2292 $doc->addStyleDeclaration($css_font_decl);
2293 }
2294
2295 // append theme CSS to document
2296 $doc->addStyleSheet(VBO_ADMIN_URI . 'resources/quill/quill.snow.css');
2297
2298 // append JS assets to document
2299 $this->addScript(VBO_ADMIN_URI . 'resources/quill/quill.js');
2300 $this->addScript(VBO_ADMIN_URI . 'resources/quill/quill-image-resize.min.js');
2301 $this->addScript(VBO_ADMIN_URI . 'resources/quill/vik-content-builder.js');
2302
2303 // icon for mail wrapper
2304 $mail_wrapper_icn = '<i class="' . VikBookingIcons::i('minus-square') . '" title="' . htmlspecialchars(JText::translate('VBO_INSERT_CONT_WRAPPER')) . '"></i>';
2305
2306 // icon for mail preview
2307 $mail_preview_icn = '<i class="' . VikBookingIcons::i('eye') . '" title="' . htmlspecialchars(JText::translate('VBOPREVIEW')) . '"></i>';
2308
2309 // icon for property logo (home icon)
2310 $mail_homelogo_icn = '<i class="' . VikBookingIcons::i('hotel') . '" title="' . htmlspecialchars(JText::translate('VBCONFIGFOURLOGO'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401) . '"></i>';
2311
2312 // text for generating content through AI
2313 $mail_genai_icn = htmlspecialchars(JText::translate('VBO_AI_LABEL_DEF'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
2314
2315 // build script pre-configuration string
2316 $quill_preconfig_script =
2317 <<<JS
2318 // Quill pre-configuration
2319 (function() {
2320 // configure Quill to use inline styles rather than classes
2321
2322 var AlignClass = Quill.import('attributors/class/align');
2323 Quill.register(AlignClass, true);
2324
2325 var BackgroundClass = Quill.import('attributors/class/background');
2326 Quill.register(BackgroundClass, true);
2327
2328 var ColorClass = Quill.import('attributors/class/color');
2329 Quill.register(ColorClass, true);
2330
2331 var FontClass = Quill.import('attributors/class/font');
2332 Quill.register(FontClass, true);
2333
2334 var SizeClass = Quill.import('attributors/class/size');
2335 Quill.register(SizeClass, true);
2336
2337 var AlignStyle = Quill.import('attributors/style/align');
2338 Quill.register(AlignStyle, true);
2339
2340 var BackgroundStyle = Quill.import('attributors/style/background');
2341 Quill.register(BackgroundStyle, true);
2342
2343 var ColorStyle = Quill.import('attributors/style/color');
2344 Quill.register(ColorStyle, true);
2345
2346 var SizeStyle = Quill.import('attributors/style/size');
2347 Quill.register(SizeStyle, true);
2348
2349 var FontStyle = Quill.import('attributors/style/font');
2350 Quill.register(FontStyle, true);
2351
2352 // set additional fonts
2353 var Font = Quill.import('formats/font');
2354 Font.whitelist = $js_font_names;
2355 Quill.register(Font, true);
2356
2357 // register custom Blot for special tags
2358 var Inline = Quill.import('blots/inline');
2359 class Specialtag extends Inline {
2360 static create(value) {
2361 var node = super.create(value);
2362 if (value) {
2363 node.setAttribute('class', value);
2364 }
2365 return node;
2366 }
2367
2368 static formats(domNode) {
2369 return domNode.getAttribute("class");
2370 }
2371
2372 format(name, value) {
2373 if (name !== this.statics.blotName || !value) {
2374 return super.format(name, value);
2375 }
2376 if (value) {
2377 this.domNode.setAttribute('class', value);
2378 }
2379 }
2380 }
2381 Specialtag.blotName = 'specialtag';
2382 Specialtag.tagName = 'strong';
2383 Quill.register(Specialtag);
2384
2385 // register bold tag names in the proper order to avoid conflicts
2386 var Bold = Quill.import('formats/bold');
2387 Bold.tagName = ['B', 'STRONG'];
2388 Quill.register(Bold, true);
2389
2390 // register custom Blot for mail-wrapper
2391 var BlockEmbed = Quill.import('blots/block/embed');
2392 class MailWrapper extends BlockEmbed { }
2393 MailWrapper.blotName = 'mailwrapper';
2394 MailWrapper.className = 'vbo-editor-hl-mailwrapper';
2395 MailWrapper.tagName = 'hr';
2396 Quill.register(MailWrapper);
2397
2398 // register custom Blot for preview
2399 class Preview extends Inline { }
2400 Preview.blotName = 'preview';
2401 Preview.tagName = 'span';
2402 Quill.register(Preview);
2403
2404 // register custom icons for mail-wrapper and preview
2405 var icons = Quill.import('ui/icons');
2406 icons['mailwrapper'] = '$mail_wrapper_icn';
2407 icons['preview'] = '$mail_preview_icn';
2408 icons['homelogo'] = '$mail_homelogo_icn';
2409 icons['genai'] = '$mail_genai_icn';
2410 })();
2411 JS
2412 ;
2413
2414 /**
2415 * Cache the pre-configuration script onto a file to allow AJAX requests
2416 * to properly load the script inline within the response. Before this
2417 * change the script used to be added as an inline script declaration.
2418 *
2419 * @since 1.16.7 (J) - 1.6.7 (WP)
2420 * @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
2421 */
2422 $cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
2423 $cached_preconfig_script_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.js']);
2424 $cached_preconfig_script_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.js';
2425 $cached_preconfig_script_ok = is_file($cached_preconfig_script_path);
2426
2427 if (!$cached_preconfig_script_ok) {
2428 // attempt to create the file
2429 $cached_preconfig_script_ok = JFile::write($cached_preconfig_script_path, $quill_preconfig_script);
2430 }
2431
2432 if ($cached_preconfig_script_ok) {
2433 // load cached script file
2434 $this->addScript($cached_preconfig_script_uri);
2435 } else {
2436 // revert to append JS script declaration to document
2437 $doc->addScriptDeclaration($quill_preconfig_script);
2438 }
2439
2440 /**
2441 * Load Context Menu assets.
2442 *
2443 * @since 1.17.6 (J) - 1.7.6 (WP)
2444 * @since 1.18.0 (J) - 1.8.0 (WP) loaded only if not during an AJAX request.
2445 */
2446 if ((VBOPlatformDetection::isWordPress() && !wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
2447 $this->loadContextMenuAssets();
2448 }
2449 }
2450
2451 /**
2452 * Renders a third-party visual editor (Quill).
2453 *
2454 * @param string $name the input name of the textarea field.
2455 * @param string $value the current value of the textarea field/editor.
2456 * @param array $attrs list of associative array attributes for the textarea.
2457 * @param array $opts associative array of options for the editor.
2458 * @param array $btns associative array of custom buttons for the editor (special tags).
2459 *
2460 * @return string the necessary HTML to render the visual editor.
2461 *
2462 * @since 1.15.0 (J) - 1.5.0 (WP)
2463 * @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
2464 */
2465 public function renderVisualEditor($name, $value, array $attrs = [], array $opts = [], array $btns = [])
2466 {
2467 if (empty($name)) {
2468 return '';
2469 }
2470
2471 // build the AJAX endpoints for uploading files, to preview the message and more
2472 $upload_endpoint = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=upload_media_file');
2473 $ajax_preview_mess = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.preview_visual_editor');
2474 $ajax_logo_url = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.get_default_logo');
2475
2476 // the HTML to build
2477 $editor = "\n";
2478
2479 // static editor counter
2480 static $editor_counter = 0;
2481
2482 // increase counter for this instance
2483 $editor_counter++;
2484
2485 // build id attributes for text and visual editors
2486 $editor_id = 'vik-contentbuilder-editor-' . $editor_counter;
2487 if (!isset($attrs['id'])) {
2488 $attrs['id'] = 'vik-contentbuilder-tarea-' . $editor_counter;
2489 }
2490
2491 // labels for modes and editor
2492 $text_html_lbl = JText::translate('VBO_MODE_TEXTHTML');
2493 $visual_mode_lbl = JText::translate('VBO_MODE_VISUAL');
2494
2495 // allowed modes to display the visual editor
2496 $allowed_modes = [
2497 'text' => $text_html_lbl,
2498 'visual' => $visual_mode_lbl,
2499 'modal-visual' => $visual_mode_lbl,
2500 ];
2501
2502 // define the default buttons to display for mode switching
2503 $modes = [
2504 'text' => $text_html_lbl,
2505 'modal-visual' => $visual_mode_lbl,
2506 ];
2507
2508 // overwrite modes to display
2509 if (isset($opts['modes']) && is_array($opts['modes']) && $opts['modes']) {
2510 // check if the given array is associative, hence inclusive of texts
2511 if (array_keys($opts['modes']) != range(0, (count($opts['modes']) - 1))) {
2512 // replace modes
2513 $modes = $opts['modes'];
2514 } else {
2515 // only the allowed keys must have been passed
2516 $new_modes = [];
2517 foreach ($opts['modes'] as $mode_type) {
2518 if (!isset($allowed_modes[$mode_type])) {
2519 continue;
2520 }
2521 $new_modes[$mode_type] = $allowed_modes[$mode_type];
2522 }
2523 $modes = $new_modes ?: $modes;
2524 }
2525 }
2526
2527 // resolve conflicts for both visual modes
2528 if (($modes['visual'] ?? '') && ($modes['modal-visual'] ?? '') && $modes['visual'] == $modes['modal-visual']) {
2529 $modes['modal-visual'] .= ' <i class="' . VikBookingIcons::i('window-restore') . '"></i>';
2530 }
2531
2532 // ensure the text mode is set
2533 if (!isset($modes['text'])) {
2534 $modes['text'] = $allowed_modes['text'];
2535 }
2536
2537 // overwrite default mode
2538 if (isset($opts['def_mode']) && isset($modes[$opts['def_mode']]) && count($modes) > 1) {
2539 $def_mode_val = $modes[$opts['def_mode']];
2540 unset($modes[$opts['def_mode']]);
2541 // sort modes accordingly
2542 $modes = array_merge([$opts['def_mode'] => $def_mode_val], $modes);
2543 // reset the array pointer
2544 reset($modes);
2545 }
2546
2547 // set default mode
2548 $default_mode = key($modes);
2549
2550 // ensure text-area styles are merged or set when not the default mode
2551 if ($default_mode != 'text') {
2552 if (!($attrs['style'] ?? '')) {
2553 // set style attribute to hide the text-area
2554 $attrs['style'] = 'display: none;';
2555 } else {
2556 // append the style instruction to hide the text-area
2557 $attrs['style'] .= ' display: none;';
2558 }
2559 }
2560
2561 // textarea attributes
2562 $ta_attributes = [];
2563 foreach ($attrs as $aname => $aval) {
2564 if ($aname == 'name') {
2565 // skip reserved attribute name
2566 continue;
2567 }
2568 $ta_attributes[] = $aname . '="' . JHtml::fetch('esc_attr', $aval) . '"';
2569 }
2570
2571 // visual editor JS options for list buttons
2572 $js_editor_opts_list_btns = [
2573 // ordered list (ol)
2574 ['list' => 'ordered'],
2575 // un-ordered list (ul)
2576 ['list' => 'bullet'],
2577 ];
2578
2579 if ($opts['list_check'] ?? null) {
2580 // push the un-ordered list (ul) with checked data-attribute, simulating a check-boxes list
2581 $js_editor_opts_list_btns[] = ['list' => 'check'];
2582 }
2583
2584 // build visual editor JS options
2585 $js_editor_opts = [
2586 'snippetsyntax' => true,
2587 'modules' => [
2588 'toolbar' => [
2589 'container' => [
2590 [
2591 [
2592 'font' => $this->getVisualEditorFonts(true)
2593 ],
2594 ],
2595 [
2596 [
2597 'header' => [1, 2, 3, 4, 5, 6, false]
2598 ]
2599 ],
2600 [
2601 'bold',
2602 'italic',
2603 'underline',
2604 'strike',
2605 'blockquote',
2606 ],
2607 [
2608 ['align' => []],
2609 ['indent' => '-1'],
2610 ['indent' => '+1'],
2611 ],
2612 [
2613 [
2614 'color' => []
2615 ],
2616 [
2617 'background' => []
2618 ],
2619 ],
2620 $js_editor_opts_list_btns,
2621 [
2622 'link',
2623 'image',
2624 'homelogo',
2625 ],
2626 [
2627 'mailwrapper',
2628 'preview',
2629 ],
2630 ],
2631 ],
2632 'imageResize' => [
2633 'displaySize' => true,
2634 ],
2635 ],
2636 'theme' => 'snow',
2637 ];
2638
2639 // check for Generative AI support through Vik Channel Manager and E4jConnect
2640 if (class_exists('VikChannelManager') && defined('VikChannelManagerConfig::AI')) {
2641 // add editor button for Gen-AI
2642 $js_editor_opts['modules']['toolbar']['container'][] = ['genai'];
2643 }
2644
2645 // build the list of special tags to be added to the editor
2646 $special_tags_btns = [];
2647 foreach ($btns as $tag_val) {
2648 $special_tags_btns[] = $tag_val;
2649 }
2650
2651 if ($special_tags_btns) {
2652 // add custom buttons to the editor to manage special tags
2653 $js_editor_opts['modules']['toolbar']['container'][] = [
2654 ['specialtags' => $special_tags_btns]
2655 ];
2656 // append CSS inline styles
2657 $editor .= '<style type="text/css">' . "\n";
2658 foreach ($special_tags_btns as $tag_val) {
2659 $editor .= '.ql-picker.ql-specialtags .ql-picker-item[data-value="' . $tag_val . '"]:before {
2660 content: "' . $tag_val . '";
2661 }' . "\n";
2662 }
2663 $editor .= '</style>' . "\n";
2664 }
2665
2666 if ($opts['unset_buttons'] ?? null) {
2667 // unset toolbar container main-level buttons
2668 $opts['unset_buttons'] = (array) $opts['unset_buttons'];
2669 foreach ($js_editor_opts['modules']['toolbar']['container'] as $tc_index => $tc_buttons) {
2670 if (!is_array($tc_buttons)) {
2671 continue;
2672 }
2673 foreach ($opts['unset_buttons'] as $unset_btn) {
2674 if (!is_string($unset_btn)) {
2675 continue;
2676 }
2677 foreach ($tc_buttons as $tc_btn_index => $tc_btn) {
2678 if (is_string($tc_btn) && $tc_btn === $unset_btn) {
2679 // unset toolbar container button
2680 unset($js_editor_opts['modules']['toolbar']['container'][$tc_index][$tc_btn_index]);
2681 }
2682 }
2683 }
2684 if (!$js_editor_opts['modules']['toolbar']['container'][$tc_index]) {
2685 // unset the whole container
2686 unset($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
2687 } else {
2688 // restore numeric (non-associative) list
2689 $js_editor_opts['modules']['toolbar']['container'][$tc_index] = array_values($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
2690 }
2691 }
2692 // restore numeric (non-associative) list
2693 $js_editor_opts['modules']['toolbar']['container'] = array_values($js_editor_opts['modules']['toolbar']['container']);
2694 }
2695
2696 // attept to pretty print a JSON encoded string for the editor options
2697 $editor_opts_str = defined('JSON_PRETTY_PRINT') ? json_encode($js_editor_opts, JSON_PRETTY_PRINT) : json_encode($js_editor_opts);
2698
2699 // safe default value for editor (HTML tags should not be converted to entities)
2700 $safe_value = preg_replace("/(<\/ ?textarea>)+/i", '', $value);
2701
2702 // the HTML to render the visual editor
2703 $html_visual_editor = '<div class="vik-contentbuilder-editor-container" id="' . $editor_id . '"></div>';
2704
2705 // build the actual HTML content
2706 $editor .= '<div class="vik-contentbuilder-wrapper">' . "\n";
2707 if (count($modes) > 1) {
2708 // display buttons to switch mode only if more than one mode available
2709 $editor .= "\t" . '<div class="vik-contentbuilder-switcher"' . (($opts['hide_modes'] ?? null) ? ' style="display: none;"' : '') . '>' . "\n";
2710 foreach ($modes as $key => $val) {
2711 if (!isset($allowed_modes[$key])) {
2712 continue;
2713 }
2714 $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>';
2715 }
2716 $editor .= "\t" . '</div>' . "\n";
2717 }
2718 $editor .= "\t" . '<div class="vik-contentbuilder-inner">' . "\n";
2719 foreach ($modes as $key => $val) {
2720 if ($key == 'text') {
2721 // if text is not the default mode, the text-area will be always hid through the style attribute
2722 $editor .= "\t\t" . '<textarea name="' . $name . '" data-switch="text" ' . implode(' ', $ta_attributes) . '>' . $safe_value . '</textarea>' . "\n";
2723 } elseif ($key == 'visual') {
2724 $editor .= "\t\t" . '<div class="vik-contentbuilder-container" data-switch="visual" style="' . ($default_mode != 'visual' ? 'display: none;' : '') . '">' . "\n";
2725 $editor .= "\t\t\t" . '<div class="vik-contentbuilder-editor-wrap">' . "\n";
2726 $editor .= "\t\t\t\t" . $html_visual_editor . "\n";
2727 $editor .= "\t\t\t" . '</div>' . "\n";
2728 $editor .= "\t\t" . '</div>' . "\n";
2729 } elseif ($key == 'modal-visual') {
2730 $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";
2731 $editor .= "\t\t\t" . '<div class="vik-contentbuilder-editor-wrap">' . "\n";
2732 $editor .= "\t\t\t\t" . (!isset($modes['visual']) ? $html_visual_editor : '') . "\n";
2733 $editor .= "\t\t\t" . '</div>' . "\n";
2734 $editor .= "\t\t" . '</div>' . "\n";
2735 }
2736 }
2737 $editor .= "\t" . '</div>' . "\n";
2738 $editor .= '</div>' . "\n";
2739 $editor .= "\n";
2740
2741 // default prompt for Gen-AI
2742 $gen_ai_use_prompt = $opts['gen_ai']['prompt'] ?? null;
2743 if (!$gen_ai_use_prompt && ($opts['gen_ai']['customer'] ?? [])) {
2744 $guest_name = trim(($opts['gen_ai']['customer']['first_name'] ?? '') . ' ' . ($opts['gen_ai']['customer']['last_name'] ?? ''));
2745 if ($guest_name) {
2746 // set proper prompt message with the guest name
2747 $gen_ai_use_prompt = JText::sprintf('VBO_AI_DISC_WRITER_FN_TEXT_GEN_MESS_EXA', $guest_name);
2748 if (!empty($opts['gen_ai']['booking']['lang']) && JFactory::getLanguage()->getTag() != $opts['gen_ai']['booking']['lang']) {
2749 // write the message in the guest language
2750 $guest_lang = $opts['gen_ai']['booking']['lang'];
2751 $known_langs = $this->getKnownLanguages();
2752 if ($known_langs[$guest_lang]['nativeName'] ?? '') {
2753 $guest_lang = $known_langs[$guest_lang]['nativeName'];
2754 }
2755 $gen_ai_use_prompt .= ' ' . JText::sprintf('VBO_AI_GEN_MESS_LANG', $guest_lang);
2756 }
2757 }
2758 }
2759
2760 if (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'cron')) {
2761 // use default prompt for cron messages
2762 $gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_CRON_DEF_PROMPT');
2763 } elseif (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'taskmanager')) {
2764 // use default prompt for the task manager
2765 $gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_TM_DEF_PROMPT');
2766 } elseif (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'quote')) {
2767 // use default prompt for making a quote
2768 $gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_QUOTE_DEF_PROMPT');
2769 }
2770
2771 if ($gen_ai_use_prompt && ($opts['gen_ai']['placeholders'] ?? 0) && $btns) {
2772 // add prompt text for using the placeholder tags
2773 $placeholders = array_filter($btns, function($tag) {
2774 // remove conditional text rule tags
2775 return !preg_match('/^\{condition\:\s?.+$/i', $tag);
2776 });
2777 if ($placeholders) {
2778 $gen_ai_use_prompt .= ' ' . JText::translate('VBO_AITOOL_WRITER_USE_PLACEHOLDERS') . "\n" . implode(', ', $placeholders);
2779 }
2780 }
2781
2782 // sanitize default prompt
2783 $gen_ai_use_prompt = json_encode((string) $gen_ai_use_prompt);
2784
2785 // add JS script to HTML content
2786 $toast_icon = VikBookingIcons::i('minus-square');
2787 $envelope_icon = VikBookingIcons::i('envelope');
2788 $booking_icon = VikBookingIcons::i('address-card');
2789 $preview_lbl = htmlspecialchars(JText::translate('VBOPREVIEW'));
2790 $booking_lbl = htmlspecialchars(JText::translate('VBDASHBOOKINGID'));
2791 $editor .= <<<HTML
2792 <script>
2793 jQuery(function() {
2794
2795 const message_preview_fn = (content, bid) => {
2796 let use_bid = bid || (typeof window['vbo_current_bid'] !== 'undefined' ? window['vbo_current_bid'] : null);
2797 VBOCore.doAjax('$ajax_preview_mess', {
2798 content: content,
2799 bid: use_bid,
2800 }, (resp) => {
2801 var pop_win = window.open('', '', 'width=800, height=600, scrollbars=yes');
2802 pop_win.document.body.innerHTML = resp[0];
2803 }, (err) => {
2804 console.log(err);
2805 alert(err.responseText);
2806 });
2807 };
2808
2809 var vbo_toast_mailwrapper = null;
2810
2811 var visual_editor_handlers = {
2812 specialtags: function(tag) {
2813 if (tag) {
2814 var cursorPosition = this.quill.getSelection().index;
2815 this.quill.insertText(cursorPosition, tag, 'specialtag', 'vbo-editor-hl-specialtag');
2816 cursorPosition += tag.length + 1;
2817 this.quill.setSelection(cursorPosition, 'silent');
2818 this.quill.insertText(cursorPosition, ' ');
2819 this.quill.setSelection(cursorPosition + 1, 'silent');
2820 this.quill.deleteText(cursorPosition - 1, 1);
2821 }
2822 },
2823 image: function(clicked) {
2824 var img_handler = new VikContentBuilderImageHandler(this.quill);
2825 img_handler.setEndpoint('$upload_endpoint').present();
2826 },
2827 mailwrapper: function(clicked) {
2828 var range = this.quill.getSelection(true);
2829 this.quill.insertText(range.index, "\\n", 'user');
2830 this.quill.insertEmbed(range.index + 1, 'mailwrapper', true, 'user');
2831 this.quill.setSelection(range.index + 2, 'silent');
2832 if (!vbo_toast_mailwrapper) {
2833 vbo_toast_mailwrapper = 1;
2834 VBOToast.enqueue(new VBOToastMessage({
2835 title: Joomla.JText._('VBO_CONT_WRAPPER'),
2836 body: Joomla.JText._('VBO_CONT_WRAPPER_HELP'),
2837 icon: '$toast_icon',
2838 delay: {
2839 min: 6000,
2840 max: 20000,
2841 tolerance: 4000,
2842 },
2843 action: () => {
2844 VBOToast.dispose(true);
2845 }
2846 }));
2847 }
2848 },
2849 preview: function(clicked) {
2850 let content = this.quill.root.innerHTML;
2851 try {
2852 let preview_btn = this.quill.container.closest('.vik-contentbuilder-editor-wrap').querySelector('button.ql-preview');
2853 jQuery(preview_btn).vboContextMenu({
2854 placement: 'bottom-right',
2855 buttons: [
2856 {
2857 icon: '$envelope_icon',
2858 text: '$preview_lbl',
2859 separator: true,
2860 action: (root, config) => {
2861 message_preview_fn.call(clicked, content);
2862 setTimeout(() => {
2863 jQuery(preview_btn).vboContextMenu('destroy');
2864 }, 500);
2865 },
2866 },
2867 {
2868 icon: '$booking_icon',
2869 text: '$booking_lbl',
2870 action: (root, config) => {
2871 let bid = prompt('$preview_lbl - $booking_lbl');
2872 message_preview_fn.call(clicked, content, bid);
2873 setTimeout(() => {
2874 jQuery(preview_btn).vboContextMenu('destroy');
2875 }, 500);
2876 },
2877 },
2878 ],
2879 });
2880 jQuery(preview_btn).vboContextMenu('show');
2881 } catch(e) {
2882 // fallback on regular preview
2883 console.error(e);
2884 message_preview_fn.call(clicked, content);
2885 }
2886 },
2887 homelogo: function(clicked) {
2888 VBOCore.doAjax('$ajax_logo_url', {}, (resp) => {
2889 try {
2890 this.quill.insertEmbed(this.quill.getSelection().index, 'image', resp.url);
2891 } catch(e) {
2892 alert('Generic logo image error');
2893 }
2894 }, (err) => {
2895 console.log(err);
2896 alert(err.responseText);
2897 });
2898 },
2899 genai: function(clicked) {
2900 let visualEditor = this.quill;
2901 let cursorPosition = visualEditor.getSelection().index;
2902
2903 const vboVisualEditorGenaiGetContentFn = (e) => {
2904 // check if any data was sent within the event
2905 if (e && e.detail?.content) {
2906 // set the generated and picked content to editor
2907 let ai_content = e.detail.content;
2908 if ((e.detail?.type || '') == 'html') {
2909 // convert HTML content into Delta for the Visual Editor
2910 let delta = visualEditor.clipboard.convert(ai_content);
2911 // set (replace) editor HTML content (2nd argument "source" should be "api" so that the "text-change" event will fire)
2912 visualEditor.setContents(delta, 'api');
2913 } else {
2914 // default to plain text
2915 visualEditor.insertText(cursorPosition, ai_content, 'user');
2916 cursorPosition += ai_content.length + 1;
2917 visualEditor.setSelection(cursorPosition, 'silent');
2918 }
2919 }
2920 };
2921
2922 // register event to receive the Gen-AI content picked
2923 document.addEventListener('vbo-ai-tools-writer-content-picked', vboVisualEditorGenaiGetContentFn);
2924
2925 // register listener to un-register the needed events
2926 document.addEventListener('vbo-ai-tools-writer-content-dismissed', function vboVisualEditorGenaiDismissedFn(e) {
2927 // un-register the events asynchronously to avoid unexpected behaviors
2928 setTimeout(() => {
2929 // make sure the same event will not trigger again
2930 e.target.removeEventListener(e.type, vboVisualEditorGenaiDismissedFn);
2931
2932 // unregister the event for getting content data
2933 document.removeEventListener('vbo-ai-tools-writer-content-picked', vboVisualEditorGenaiGetContentFn);
2934 });
2935 });
2936
2937 // default prompt
2938 let writer_prompt = $gen_ai_use_prompt;
2939
2940 // render modal widget
2941 VBOCore.handleDisplayWidgetNotification({
2942 widget_id: 'aitools',
2943 }, {
2944 scope: 'writer',
2945 prompt: {
2946 message: (writer_prompt || Joomla.JText._('VBO_AITOOL_WRITER_DEF_PROMPT')),
2947 submit: 0,
2948 },
2949 modal_options: {
2950 suffix: 'vbo-ai-tools-writer-inner',
2951 title: Joomla.JText._('VBO_GEN_CONTENT') + ' - ' + Joomla.JText._('VBO_AI_LABEL_DEF'),
2952 lock_scroll: false,
2953 enlargeable: false,
2954 minimizeable: false,
2955 dismiss_event: 'vbo-ai-tools-writer-content-picked',
2956 dismissed_event: 'vbo-ai-tools-writer-content-dismissed',
2957 },
2958 });
2959 }
2960 };
2961 var visual_editor_ext_opts = $editor_opts_str;
2962 visual_editor_ext_opts['modules']['toolbar']['handlers'] = visual_editor_handlers;
2963 var visual_editor = new Quill('#$editor_id', visual_editor_ext_opts);
2964 var editor_content = jQuery('textarea#{$attrs['id']}').val();
2965 if (editor_content && editor_content.length) {
2966 if (editor_content.indexOf('<') >= 0) {
2967 // replace special tags
2968 editor_content = editor_content.replace(/([^"']|^)({(?:condition: ?)?[a-z0-9_]{5,64}})([^"']|$)/g, function(match, before, tag, after) {
2969 return before + '<strong class="vbo-editor-hl-specialtag">' + tag + '</strong>' + after;
2970 });
2971 var editor_delta = visual_editor.clipboard.convert(editor_content);
2972 // set editor HTML content
2973 visual_editor.setContents(editor_delta, 'api');
2974 } else {
2975 // set text content
2976 visual_editor.setText(editor_content, 'silent');
2977 }
2978 }
2979 visual_editor.on('text-change', function(delta, source) {
2980 jQuery('textarea#{$attrs['id']}').val(visual_editor.root.innerHTML);
2981 });
2982 jQuery('textarea#{$attrs['id']}').on('change', function() {
2983 var editor_content = jQuery(this).val();
2984 var editor_delta = visual_editor.clipboard.convert(editor_content);
2985 visual_editor.setContents(editor_delta, 'silent');
2986 });
2987 try {
2988 // push editor instance to the pool
2989 VikContentBuilder.pushEditor(visual_editor);
2990 } catch(e) {
2991 console.error('Could not push new visual editor instance', e);
2992 }
2993 setTimeout(() => {
2994 jQuery('.vik-contentbuilder-switcher-btn-active').trigger('click');
2995 });
2996 });
2997 </script>
2998 HTML;
2999
3000 // return the necessary HTML string to be displayed
3001 return $editor;
3002 }
3003
3004 /**
3005 * Loads the necessary assets to render context menus.
3006 *
3007 * @return void
3008 *
3009 * @since 1.16.0 (J) - 1.6.0 (WP)
3010 */
3011 public function loadContextMenuAssets()
3012 {
3013 static $loaded = null;
3014
3015 if ($loaded) {
3016 return;
3017 }
3018
3019 $dark_mode = 'null';
3020
3021 if (JFactory::getApplication()->isClient('administrator')) {
3022 // get appearance preference
3023 $app_pref = VikBooking::getAppearancePref();
3024
3025 if ($app_pref == 'light') {
3026 $dark_mode = 'false';
3027 } elseif ($app_pref == 'dark') {
3028 $dark_mode = 'true';
3029 }
3030 }
3031
3032 $this->addScript(VBO_ADMIN_URI . 'resources/contextmenu.js');
3033
3034 $doc = JFactory::getDocument();
3035
3036 $doc->addStyleSheet(VBO_ADMIN_URI . 'resources/contextmenu.css');
3037 $doc->addScriptDeclaration(
3038 <<<JS
3039 (function($) {
3040 'use strict';
3041
3042 $(function() {
3043 $.vboContextMenu.defaults.darkMode = {$dark_mode};
3044 $.vboContextMenu.defaults.class = 'vbo-dropdown-cxmenu';
3045 });
3046 })(jQuery);
3047 JS
3048 );
3049
3050 $loaded = 1;
3051 }
3052
3053 /**
3054 * Loads the assets necessary to render a phone input field.
3055 *
3056 * @return void
3057 *
3058 * @since 1.16.0 (J) - 1.6.0 (WP)
3059 */
3060 public function loadPhoneInputFieldAssets()
3061 {
3062 static $loaded = null;
3063
3064 if ($loaded) {
3065 return;
3066 }
3067
3068 $loaded = 1;
3069
3070 $document = JFactory::getDocument();
3071
3072 $document->addStyleSheet(VBO_SITE_URI . 'resources/intlTelInput.css');
3073 $document->addScript(VBO_SITE_URI . 'resources/intlTelInput.js');
3074 }
3075 }
3076