PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | admin/helpers/jv_helper.php +1862 -194 1.7.2 → 1.8.16 View file →
@@ -138,15 +138,15 @@
138 138 return VBOFactory::getPlatform()->getMailer()->send($mail_data);
139 139 }
140 140
141 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 - */
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 149 public function getDropDown($arr_values, $current_key, $empty_value, $default, $input_name, $record_id = '')
150 150 {
151 151 $dropdown = '';
152 152 $dropdown .= '<select name="'.$input_name.'" onchange="document.adminForm.submit();">'."\n";
@@ -161,9 +161,19 @@
161 161 }
162 162
163 163 public function loadSelect2()
164 164 {
165 - //load JS + CSS
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
166 176 $document = JFactory::getDocument();
167 177 $document->addStyleSheet(VBO_ADMIN_URI.'resources/select2.min.css');
168 178 $this->addScript(VBO_ADMIN_URI.'resources/select2.min.js');
169 179 }
@@ -209,9 +219,9 @@
209 219 $dropdown .= '</select>'."\n";
210 220
211 221 //js code
212 222 $dropdown .= '<script type="text/javascript">'."\n";
213 - $dropdown .= 'jQuery(document).ready(function() {'."\n";
223 + $dropdown .= 'jQuery(function() {'."\n";
214 224 $dropdown .= ' jQuery("#'.$idattr.'").select2('.(!empty($placeholder) ? '{placeholder: "'.addslashes($placeholder).'"}' : '').');'."\n";
215 225 $dropdown .= '});'."\n";
216 226 $dropdown .= '</script>'."\n";
217 227
@@ -296,14 +306,1331 @@
296 306 return $mb_supported ? mb_substr($text, 0, $len, 'UTF-8') : substr($text, 0, $len);
297 307 }
298 308
299 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 + /**
300 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.
301 1623 *
1624 + * @param array $options Associative list of loading options.
1625 + *
1626 + * @return void
1627 + *
302 1628 * @since 1.1.0
303 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.
304 1631 */
305 - public function loadDatePicker()
1632 + public function loadDatePicker(array $options = [])
306 1633 {
307 1634 static $datepicker_loaded = null;
308 1635
309 1636 if ($datepicker_loaded) {
@@ -311,49 +1638,106 @@
311 1638 return;
312 1639 }
313 1640
314 1641 $document = JFactory::getDocument();
315 - $document->addStyleSheet(VBO_SITE_URI.'resources/jquery-ui.min.css');
1642 + $document->addStyleSheet(VBO_SITE_URI . 'resources/jquery-ui.min.css');
316 1643
317 1644 JHtml::fetch('jquery.framework', true, true);
318 - $this->addScript(VBO_SITE_URI.'resources/jquery-ui.min.js');
1645 + $this->addScript(VBO_SITE_URI . 'resources/jquery-ui.min.js');
319 1646
1647 + if (!strcasecmp(($options['type'] ?? ''), 'dates_range')) {
1648 + // load DRP calendar assets
1649 + $this->loadDatesRangePicker($options);
1650 + }
1651 +
320 1652 $vbo_df = VikBooking::getDateFormat();
321 1653 $juidf = $vbo_df == "%d/%m/%Y" ? 'dd/mm/yy' : ($vbo_df == "%m/%d/%Y" ? 'mm/dd/yy' : 'yy/mm/dd');
322 1654
323 1655 $is_rtl_lan = false;
324 - $is_rtl_str = 'false';
325 - $day_names_min_len = 2;
326 1656 $now_lang = JFactory::getLanguage();
327 1657 if (method_exists($now_lang, 'isRtl')) {
328 1658 $is_rtl_lan = $now_lang->isRtl();
329 - $is_rtl_str = $is_rtl_lan ? 'true' : $is_rtl_str;
330 - if ($is_rtl_lan) {
331 - // for most RTL languages, 2 chars for the week-days would not make sense
332 - $day_names_min_len = 3;
333 - }
334 1659 }
335 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 +
336 1736 $ldecl = '
337 -jQuery(function($){'."\n".'
338 - $.datepicker.regional["vikbooking"] = {'."\n".'
339 - closeText: "'.JText::translate('VBJQCALDONE').'",'."\n".'
340 - prevText: "'.JText::translate('VBJQCALPREV').'",'."\n".'
341 - nextText: "'.JText::translate('VBJQCALNEXT').'",'."\n".'
342 - currentText: "'.JText::translate('VBJQCALTODAY').'",'."\n".'
343 - monthNames: ["'.JText::translate('VBMONTHONE').'","'.JText::translate('VBMONTHTWO').'","'.JText::translate('VBMONTHTHREE').'","'.JText::translate('VBMONTHFOUR').'","'.JText::translate('VBMONTHFIVE').'","'.JText::translate('VBMONTHSIX').'","'.JText::translate('VBMONTHSEVEN').'","'.JText::translate('VBMONTHEIGHT').'","'.JText::translate('VBMONTHNINE').'","'.JText::translate('VBMONTHTEN').'","'.JText::translate('VBMONTHELEVEN').'","'.JText::translate('VBMONTHTWELVE').'"],'."\n".'
344 - monthNamesShort: ["'.$this->safeSubstr(JText::translate('VBMONTHONE')).'","'.$this->safeSubstr(JText::translate('VBMONTHTWO')).'","'.$this->safeSubstr(JText::translate('VBMONTHTHREE')).'","'.$this->safeSubstr(JText::translate('VBMONTHFOUR')).'","'.$this->safeSubstr(JText::translate('VBMONTHFIVE')).'","'.$this->safeSubstr(JText::translate('VBMONTHSIX')).'","'.$this->safeSubstr(JText::translate('VBMONTHSEVEN')).'","'.$this->safeSubstr(JText::translate('VBMONTHEIGHT')).'","'.$this->safeSubstr(JText::translate('VBMONTHNINE')).'","'.$this->safeSubstr(JText::translate('VBMONTHTEN')).'","'.$this->safeSubstr(JText::translate('VBMONTHELEVEN')).'","'.$this->safeSubstr(JText::translate('VBMONTHTWELVE')).'"],'."\n".'
345 - dayNames: ["'.JText::translate('VBWEEKDAYZERO').'", "'.JText::translate('VBWEEKDAYONE').'", "'.JText::translate('VBWEEKDAYTWO').'", "'.JText::translate('VBWEEKDAYTHREE').'", "'.JText::translate('VBWEEKDAYFOUR').'", "'.JText::translate('VBWEEKDAYFIVE').'", "'.JText::translate('VBWEEKDAYSIX').'"],'."\n".'
346 - dayNamesShort: ["'.$this->safeSubstr(JText::translate('VBWEEKDAYZERO')).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYONE')).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYTWO')).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYTHREE')).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYFOUR')).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYFIVE')).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYSIX')).'"],'."\n".'
347 - dayNamesMin: ["'.$this->safeSubstr(JText::translate('VBWEEKDAYZERO'), $day_names_min_len).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYONE'), $day_names_min_len).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYTWO'), $day_names_min_len).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYTHREE'), $day_names_min_len).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYFOUR'), $day_names_min_len).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYFIVE'), $day_names_min_len).'", "'.$this->safeSubstr(JText::translate('VBWEEKDAYSIX'), $day_names_min_len).'"],'."\n".'
348 - weekHeader: "'.JText::translate('VBJQCALWKHEADER').'",'."\n".'
349 - dateFormat: "'.$juidf.'",'."\n".'
350 - firstDay: '.VikBooking::getFirstWeekDay().','."\n".'
351 - isRTL: ' . $is_rtl_str . ','."\n".'
352 - showMonthAfterYear: false,'."\n".'
353 - yearSuffix: ""'."\n".'
354 - };'."\n".'
355 - $.datepicker.setDefaults($.datepicker.regional["vikbooking"]);'."\n".'
1737 +jQuery(function($) {' . "\n" . '
1738 + $.datepicker.regional["vikbooking"] = ' . json_encode($vbo_dp_regional_vals) . ';
1739 + $.datepicker.setDefaults($.datepicker.regional["vikbooking"]);' . "\n" . '
356 1740 });';
357 1741
358 1742 /**
359 1743 * Trigger event to allow third party plugins to overwrite the JS declaration for the datepicker.
@@ -494,9 +1878,9 @@
494 1878 }
495 1879 ';
496 1880 $js = '
497 1881 <script type="text/javascript">
498 - jQuery(document).ready(function() {
1882 + jQuery(function() {
499 1883 jQuery("'.$selector.'").fancybox('.$opts.');
500 1884 });'.($reloadfunc ? $reloadjs : '').'
501 1885 </script>';
502 1886
@@ -766,9 +2150,9 @@
766 2150 $doc = JFactory::getDocument();
767 2151 $doc->addScriptDeclaration(
768 2152 <<<JS
769 2153 jQuery(function() {
770 - if (jQuery.isFunction(jQuery.fn.tooltip)) {
2154 + if (typeof jQuery.fn.tooltip === 'function') {
771 2155 jQuery('.vik-multiswitch-wrap label').tooltip();
772 2156 }
773 2157 });
774 2158 JS
@@ -803,13 +2187,40 @@
803 2187 }, $font_families);
804 2188 }
805 2189
806 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 + /**
807 2217 * Loads the necessary assets for the third-party visual editor (Quill).
808 2218 *
809 2219 * @return void
810 2220 *
811 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.
812 2223 */
813 2224 public function loadVisualEditorAssets()
814 2225 {
815 2226 static $loaded = null;
@@ -822,8 +2233,11 @@
822 2233
823 2234 // access the document
824 2235 $doc = JFactory::getDocument();
825 2236
2237 + // load JS langs
2238 + $this->loadVisualEditorDefinitions();
2239 +
826 2240 // build the list of font families
827 2241 $font_families = $this->getVisualEditorFonts();
828 2242 $font_shortfam = $this->getVisualEditorFonts(true);
829 2243 $js_font_names = json_encode($font_shortfam);
@@ -845,8 +2259,12 @@
845 2259 }
846 2260 .ql-picker.ql-specialtags .ql-picker-label:before {
847 2261 content: "' . htmlspecialchars(JText::translate('VBO_CONDTEXT_TKN')) . '";
848 2262 }
2263 + .ql-formats .ql-genai {
2264 + width: auto !important;
2265 + font-weight: bold;
2266 + }
849 2267 ';
850 2268
851 2269 /**
852 2270 * Cache the pre-configuration CSS onto a file to allow AJAX requests
@@ -853,11 +2271,13 @@
853 2271 * to properly load the asset inline within the response. Before this
854 2272 * change the styles used to be added as an inline style declaration.
855 2273 *
856 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.
857 2276 */
858 - $cached_preconfig_css_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache.css']);
859 - $cached_preconfig_css_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache.css';
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';
860 2280 $cached_preconfig_css_ok = is_file($cached_preconfig_css_path);
861 2281
862 2282 if (!$cached_preconfig_css_ok) {
863 2283 // attempt to create the file
@@ -888,8 +2308,11 @@
888 2308
889 2309 // icon for property logo (home icon)
890 2310 $mail_homelogo_icn = '<i class="' . VikBookingIcons::i('hotel') . '" title="' . htmlspecialchars(JText::translate('VBCONFIGFOURLOGO'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401) . '"></i>';
891 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 +
892 2315 // build script pre-configuration string
893 2316 $quill_preconfig_script =
894 2317 <<<JS
895 2318 // Quill pre-configuration
@@ -958,8 +2381,13 @@
958 2381 Specialtag.blotName = 'specialtag';
959 2382 Specialtag.tagName = 'strong';
960 2383 Quill.register(Specialtag);
961 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 +
962 2390 // register custom Blot for mail-wrapper
963 2391 var BlockEmbed = Quill.import('blots/block/embed');
964 2392 class MailWrapper extends BlockEmbed { }
965 2393 MailWrapper.blotName = 'mailwrapper';
@@ -977,8 +2405,9 @@
977 2405 var icons = Quill.import('ui/icons');
978 2406 icons['mailwrapper'] = '$mail_wrapper_icn';
979 2407 icons['preview'] = '$mail_preview_icn';
980 2408 icons['homelogo'] = '$mail_homelogo_icn';
2409 + icons['genai'] = '$mail_genai_icn';
981 2410 })();
982 2411 JS
983 2412 ;
984 2413
@@ -987,11 +2416,13 @@
987 2416 * to properly load the script inline within the response. Before this
988 2417 * change the script used to be added as an inline script declaration.
989 2418 *
990 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.
991 2421 */
992 - $cached_preconfig_script_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache.js']);
993 - $cached_preconfig_script_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache.js';
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';
994 2425 $cached_preconfig_script_ok = is_file($cached_preconfig_script_path);
995 2426
996 2427 if (!$cached_preconfig_script_ok) {
997 2428 // attempt to create the file
@@ -1004,8 +2435,18 @@
1004 2435 } else {
1005 2436 // revert to append JS script declaration to document
1006 2437 $doc->addScriptDeclaration($quill_preconfig_script);
1007 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 + }
1008 2449 }
1009 2450
1010 2451 /**
1011 2452 * Renders a third-party visual editor (Quill).
@@ -1018,16 +2459,17 @@
1018 2459 *
1019 2460 * @return string the necessary HTML to render the visual editor.
1020 2461 *
1021 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.
1022 2464 */
1023 - public function renderVisualEditor($name, $value, $attrs = array(), $opts = array(), $btns = array())
2465 + public function renderVisualEditor($name, $value, array $attrs = [], array $opts = [], array $btns = [])
1024 2466 {
1025 2467 if (empty($name)) {
1026 - return null;
2468 + return '';
1027 2469 }
1028 2470
1029 - // build the AJAX endpoint for uploading files and to preview the message
2471 + // build the AJAX endpoints for uploading files, to preview the message and more
1030 2472 $upload_endpoint = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=upload_media_file');
1031 2473 $ajax_preview_mess = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.preview_visual_editor');
1032 2474 $ajax_logo_url = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.get_default_logo');
1033 2475
@@ -1045,39 +2487,27 @@
1045 2487 if (!isset($attrs['id'])) {
1046 2488 $attrs['id'] = 'vik-contentbuilder-tarea-' . $editor_counter;
1047 2489 }
1048 2490
1049 - // textarea attributes
1050 - $ta_attributes = array();
1051 - foreach ($attrs as $aname => $aval) {
1052 - if ($aname == 'name') {
1053 - // skip reserved attribute name
1054 - continue;
1055 - }
1056 - $ta_attributes[] = $aname . '="' . JHtml::fetch('esc_attr', $aval) . '"';
1057 - }
1058 -
1059 2491 // labels for modes and editor
1060 2492 $text_html_lbl = JText::translate('VBO_MODE_TEXTHTML');
1061 2493 $visual_mode_lbl = JText::translate('VBO_MODE_VISUAL');
1062 - JText::script('VBO_CONT_WRAPPER');
1063 - JText::script('VBO_CONT_WRAPPER_HELP');
1064 2494
1065 2495 // allowed modes to display the visual editor
1066 - $allowed_modes = array(
2496 + $allowed_modes = [
1067 2497 'text' => $text_html_lbl,
1068 2498 'visual' => $visual_mode_lbl,
1069 2499 'modal-visual' => $visual_mode_lbl,
1070 - );
2500 + ];
1071 2501
1072 2502 // define the default buttons to display for mode switching
1073 - $modes = array(
2503 + $modes = [
1074 2504 'text' => $text_html_lbl,
1075 2505 'modal-visual' => $visual_mode_lbl,
1076 - );
2506 + ];
1077 2507
1078 2508 // overwrite modes to display
1079 - if (isset($opts['modes']) && is_array($opts['modes']) && count($opts['modes'])) {
2509 + if (isset($opts['modes']) && is_array($opts['modes']) && $opts['modes']) {
1080 2510 // check if the given array is associative, hence inclusive of texts
1081 2511 if (array_keys($opts['modes']) != range(0, (count($opts['modes']) - 1))) {
1082 2512 // replace modes
1083 2513 $modes = $opts['modes'];
@@ -1082,9 +2512,9 @@
1082 2512 // replace modes
1083 2513 $modes = $opts['modes'];
1084 2514 } else {
1085 2515 // only the allowed keys must have been passed
1086 - $new_modes = array();
2516 + $new_modes = [];
1087 2517 foreach ($opts['modes'] as $mode_type) {
1088 2518 if (!isset($allowed_modes[$mode_type])) {
1089 2519 continue;
1090 2520 }
@@ -1089,18 +2519,28 @@
1089 2519 continue;
1090 2520 }
1091 2521 $new_modes[$mode_type] = $allowed_modes[$mode_type];
1092 2522 }
1093 - $modes = count($new_modes) ? $new_modes : $modes;
2523 + $modes = $new_modes ?: $modes;
1094 2524 }
1095 2525 }
1096 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 +
1097 2537 // overwrite default mode
1098 2538 if (isset($opts['def_mode']) && isset($modes[$opts['def_mode']]) && count($modes) > 1) {
1099 2539 $def_mode_val = $modes[$opts['def_mode']];
1100 2540 unset($modes[$opts['def_mode']]);
1101 2541 // sort modes accordingly
1102 - $modes = array_merge(array($opts['def_mode'] => $def_mode_val), $modes);
2542 + $modes = array_merge([$opts['def_mode'] => $def_mode_val], $modes);
1103 2543 // reset the array pointer
1104 2544 reset($modes);
1105 2545 }
1106 2546
@@ -1106,8 +2546,42 @@
1106 2546
1107 2547 // set default mode
1108 2548 $default_mode = key($modes);
1109 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 +
1110 2584 // build visual editor JS options
1111 2585 $js_editor_opts = [
1112 2586 'snippetsyntax' => true,
1113 2587 'modules' => [
@@ -1142,13 +2616,10 @@
1142 2616 [
1143 2617 'background' => []
1144 2618 ],
1145 2619 ],
2620 + $js_editor_opts_list_btns,
1146 2621 [
1147 - ['list' => 'ordered'],
1148 - ['list' => 'bullet'],
1149 - ],
1150 - [
1151 2622 'link',
1152 2623 'image',
1153 2624 'homelogo',
1154 2625 ],
@@ -1164,19 +2635,25 @@
1164 2635 ],
1165 2636 'theme' => 'snow',
1166 2637 ];
1167 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 +
1168 2645 // build the list of special tags to be added to the editor
1169 - $special_tags_btns = array();
2646 + $special_tags_btns = [];
1170 2647 foreach ($btns as $tag_val) {
1171 2648 $special_tags_btns[] = $tag_val;
1172 2649 }
1173 2650
1174 - if (count($special_tags_btns)) {
2651 + if ($special_tags_btns) {
1175 2652 // add custom buttons to the editor to manage special tags
1176 - $js_editor_opts['modules']['toolbar']['container'][] = array(
1177 - array('specialtags' => $special_tags_btns)
1178 - );
2653 + $js_editor_opts['modules']['toolbar']['container'][] = [
2654 + ['specialtags' => $special_tags_btns]
2655 + ];
1179 2656 // append CSS inline styles
1180 2657 $editor .= '<style type="text/css">' . "\n";
1181 2658 foreach ($special_tags_btns as $tag_val) {
1182 2659 $editor .= '.ql-picker.ql-specialtags .ql-picker-item[data-value="' . $tag_val . '"]:before {
@@ -1185,8 +2662,38 @@
1185 2662 }
1186 2663 $editor .= '</style>' . "\n";
1187 2664 }
1188 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 +
1189 2696 // attept to pretty print a JSON encoded string for the editor options
1190 2697 $editor_opts_str = defined('JSON_PRETTY_PRINT') ? json_encode($js_editor_opts, JSON_PRETTY_PRINT) : json_encode($js_editor_opts);
1191 2698
1192 2699 // safe default value for editor (HTML tags should not be converted to entities)
@@ -1198,9 +2705,9 @@
1198 2705 // build the actual HTML content
1199 2706 $editor .= '<div class="vik-contentbuilder-wrapper">' . "\n";
1200 2707 if (count($modes) > 1) {
1201 2708 // display buttons to switch mode only if more than one mode available
1202 - $editor .= "\t" . '<div class="vik-contentbuilder-switcher">' . "\n";
2709 + $editor .= "\t" . '<div class="vik-contentbuilder-switcher"' . (($opts['hide_modes'] ?? null) ? ' style="display: none;"' : '') . '>' . "\n";
1203 2710 foreach ($modes as $key => $val) {
1204 2711 if (!isset($allowed_modes[$key])) {
1205 2712 continue;
1206 2713 }
@@ -1208,130 +2715,288 @@
1208 2715 }
1209 2716 $editor .= "\t" . '</div>' . "\n";
1210 2717 }
1211 2718 $editor .= "\t" . '<div class="vik-contentbuilder-inner">' . "\n";
1212 - $editor .= "\t\t" . '<textarea name="' . $name . '" data-switch="text" ' . implode(' ', $ta_attributes) . ' style="' . ($default_mode != 'text' ? 'display: none;' : '') . '">' . $safe_value . '</textarea>' . "\n";
1213 - if (isset($modes['visual'])) {
1214 - $editor .= "\t\t" . '<div class="vik-contentbuilder-container" data-switch="visual" style="' . ($default_mode != 'visual' ? 'display: none;' : '') . '">' . "\n";
1215 - $editor .= "\t\t\t" . $html_visual_editor . "\n";
1216 - $editor .= "\t\t" . '</div>' . "\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 + }
1217 2736 }
1218 - if (isset($modes['modal-visual'])) {
1219 - $editor .= "\t\t" . '<div class="vbo-modal-overlay-block vik-contentbuilder-modal" data-switch="modal-visual" data-appendto=".vik-contentbuilder-modal-content" style="' . ($default_mode != 'modal-visual' ? 'display: none;' : '') . '">' . "\n";
1220 - $editor .= "\t\t\t" . '<a class="vbo-modal-overlay-close" href="javascript: void(0);" onclick="VikContentBuilder.switchMode(this, \'text\');"></a>' . "\n";
1221 - $editor .= "\t\t\t" . '<div class="vbo-modal-overlay-content vik-contentbuilder-modal-content">' . "\n";
1222 - $editor .= "\t\t\t\t" . '<div class="vbo-modal-overlay-content-head vik-contentbuilder-modal-head"><span class="vbo-modal-overlay-close-times" onclick="VikContentBuilder.switchMode(this, \'text\');">&times;</span></div>' . "\n";
1223 - $editor .= "\t\t\t\t" . '<div class="vbo-modal-overlay-content-body vik-contentbuilder-modal-body">' . "\n";
1224 - $editor .= "\t\t\t\t\t" . (!isset($modes['visual']) ? $html_visual_editor : '') . "\n";
1225 - $editor .= "\t\t\t\t" . '</div>' . "\n";
1226 - $editor .= "\t\t\t" . '</div>' . "\n";
1227 - $editor .= "\t\t" . '</div>' . "\n";
1228 - }
1229 2737 $editor .= "\t" . '</div>' . "\n";
1230 2738 $editor .= '</div>' . "\n";
1231 2739 $editor .= "\n";
1232 -
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 +
1233 2785 // add JS script to HTML content
1234 - $editor .= '<script>' . "\n";
1235 - $editor .= 'jQuery(function() {
1236 - var vbo_toast_mailwrapper = null;
1237 - var visual_editor_handlers = {
1238 - "specialtags": function(tag) {
1239 - if (tag) {
1240 - var cursorPosition = this.quill.getSelection().index;
1241 - this.quill.insertText(cursorPosition, tag, "specialtag", "vbo-editor-hl-specialtag");
1242 - cursorPosition += tag.length + 1;
1243 - this.quill.setSelection(cursorPosition, "silent");
1244 - this.quill.insertText(cursorPosition, " ");
1245 - this.quill.setSelection(cursorPosition + 1, "silent");
1246 - this.quill.deleteText(cursorPosition - 1, 1);
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);
1247 2845 }
1248 - },
1249 - "image": function(clicked) {
1250 - var img_handler = new VikContentBuilderImageHandler(this.quill);
1251 - img_handler.setEndpoint("' . $upload_endpoint . '").present();
1252 - },
1253 - "mailwrapper": function(clicked) {
1254 - var range = this.quill.getSelection(true);
1255 - this.quill.insertText(range.index, "\n", "user");
1256 - this.quill.insertEmbed(range.index + 1, "mailwrapper", true, "user");
1257 - this.quill.setSelection(range.index + 2, "silent");
1258 - if (!vbo_toast_mailwrapper) {
1259 - vbo_toast_mailwrapper = 1;
1260 - VBOToast.enqueue(new VBOToastMessage({
1261 - title: Joomla.JText._("VBO_CONT_WRAPPER"),
1262 - body: Joomla.JText._("VBO_CONT_WRAPPER_HELP"),
1263 - icon: "' . VikBookingIcons::i('minus-square') . '",
1264 - delay: {
1265 - min: 6000,
1266 - max: 20000,
1267 - tolerance: 4000,
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);
1268 2865 },
1269 - action: () => {
1270 - VBOToast.dispose(true);
1271 - }
1272 - }));
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');
1273 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,
1274 2948 },
1275 - "preview": function(clicked) {
1276 - VBOCore.doAjax("' . $ajax_preview_mess . '", {
1277 - content: this.quill.root.innerHTML,
1278 - bid: (typeof window["vbo_current_bid"] !== "undefined" ? window["vbo_current_bid"] : null)
1279 - }, (resp) => {
1280 - var pop_win = window.open("", "", "width=800, height=600, scrollbars=yes");
1281 - pop_win.document.body.innerHTML = resp[0];
1282 - }, (err) => {
1283 - console.log(err);
1284 - alert(err.responseText);
1285 - });
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',
1286 2957 },
1287 - "homelogo": function(clicked) {
1288 - VBOCore.doAjax("' . $ajax_logo_url . '", {}, (resp) => {
1289 - try {
1290 - this.quill.insertEmbed(this.quill.getSelection().index, "image", resp.url);
1291 - } catch(e) {
1292 - alert("Generic logo image error");
1293 - }
1294 - }, (err) => {
1295 - console.log(err);
1296 - alert(err.responseText);
1297 - });
1298 - }
1299 - };
1300 - var visual_editor_ext_opts = ' . $editor_opts_str . ';
1301 - visual_editor_ext_opts["modules"]["toolbar"]["handlers"] = visual_editor_handlers;
1302 - var visual_editor = new Quill("#' . $editor_id . '", visual_editor_ext_opts);
1303 - var editor_content = jQuery("textarea#' . $attrs['id'] . '").val();
1304 - if (editor_content && editor_content.length) {
1305 - if (editor_content.indexOf("<") >= 0) {
1306 - // replace special tags
1307 - editor_content = editor_content.replace(/([^"\']|^)({(?:condition: ?)?[a-z0-9_]{5,64}})([^"\']|$)/g, function(match, before, tag, after) {
1308 - return before + "<strong class=\"vbo-editor-hl-specialtag\">" + tag + "</strong>" + after;
1309 - });
1310 - var editor_delta = visual_editor.clipboard.convert(editor_content);
1311 - // set editor HTML content
1312 - visual_editor.setContents(editor_delta, "silent");
1313 - } else {
1314 - // set text content
1315 - visual_editor.setText(editor_content, "silent");
1316 - }
1317 - }
1318 - visual_editor.on("text-change", function(delta, source) {
1319 - jQuery("textarea#' . $attrs['id'] . '").val(visual_editor.root.innerHTML);
1320 2958 });
1321 - jQuery("textarea#' . $attrs['id'] . '").on("change", function() {
1322 - var editor_content = jQuery(this).val();
1323 - var editor_delta = visual_editor.clipboard.convert(editor_content);
1324 - visual_editor.setContents(editor_delta, "silent");
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;
1325 2970 });
1326 - try {
1327 - // push editor instance to the pool
1328 - VikContentBuilder.pushEditor(visual_editor);
1329 - } catch(e) {
1330 - console.error("Could not push new visual editor instance", e);
1331 - }
1332 - });' . "\n";
1333 - $editor .= '</script>' . "\n";
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;
1334 2999
1335 3000 // return the necessary HTML string to be displayed
1336 3001 return $editor;
1337 3002 }
@@ -1350,16 +3015,19 @@
1350 3015 if ($loaded) {
1351 3016 return;
1352 3017 }
1353 3018
1354 - // get appearance preference
1355 - $app_pref = VikBooking::getAppearancePref();
3019 + $dark_mode = 'null';
1356 3020
1357 - $dark_mode = 'null';
1358 - if ($app_pref == 'light') {
1359 - $dark_mode = 'false';
1360 - } elseif ($app_pref == 'dark') {
1361 - $dark_mode = 'true';
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 + }
1362 3030 }
1363 3031
1364 3032 $this->addScript(VBO_ADMIN_URI . 'resources/contextmenu.js');
1365 3033