<?php
/**
* @package VikBooking
* @subpackage com_vikbooking
* @author Alessio Gaggii - e4j - Extensionsforjoomla.com
* @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
* @link https://vikwp.com
*/
defined('ABSPATH') or die('No script kiddies please!');
/**
* Extends native application functions.
*
* @wponly the class extends VikApplication and uses different vars.
* @since 1.0
* @see VikApplication
*/
class VboApplication extends VikApplication
{
/**
* Additional commands container for any methods.
*
* @var array
*/
private $commands;
/**
* This method loads an additional CSS file (if available)
* for the current CMS, and CMS version.
*
* @return void
**/
public function normalizeBackendStyles()
{
$document = JFactory::getDocument();
if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'wp.css')) {
$document->addStyleSheet(VBO_ADMIN_URI . 'helpers/' . 'wp.css', ['version' => VIKBOOKING_SOFTWARE_VERSION], ['id' => 'vbo-wp-style']);
}
}
/**
* Includes a script URI.
*
* @param string $uri The script URI.
*
* @return void
*/
public function addScript($uri)
{
JHtml::fetch('script', $uri);
}
/**
* Sets additional commands for any methods. Like raise an error if the recipient email address is empty.
* Returns this object for chainability.
*/
public function setCommand($key, $value)
{
if (!empty($key)) {
$this->commands[$key] = $value;
}
return $this;
}
public function sendMail($from_address, $from_name, $to, $reply_address, $subject, $hmess, $is_html = true, $encoding = 'base64', $attachment = null)
{
if (!is_array($to) && strpos($to, ',') !== false) {
$all_recipients = explode(',', $to);
foreach ($all_recipients as $k => $v) {
if (empty($v)) {
unset($all_recipients[$k]);
}
}
if (count($all_recipients) > 0) {
$to = $all_recipients;
}
}
if (empty($to)) {
//Prevent Joomla Exceptions that would stop the script execution
if (isset($this->commands['print_errors'])) {
VikError::raiseWarning('', 'The recipient email address is empty. Email message could not be sent. Please check your configuration.');
}
return false;
}
if ($from_name == $from_address) {
$mainframe = JFactory::getApplication();
$attempt_fromn = $mainframe->get('fromname', '');
if (!empty($attempt_fromn)) {
$from_name = $attempt_fromn;
}
}
/**
* Conditional text rules may set extra recipients or attachments.
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
$extra_admin_recipients = VikBooking::addAdminEmailRecipient(null);
$bcc_addresses = VikBooking::addAdminEmailRecipient(null, $bcc = true);
$extra_attachments = VikBooking::addEmailAttachment(null);
if ($extra_admin_recipients) {
// cast a possible string to array
$to = (array) $to;
// merge additional recipients
$to = array_merge($to, $extra_admin_recipients);
}
if ($extra_attachments) {
$attachment = $attachment ? (array) $attachment : [];
$attachment = array_merge($attachment, $extra_attachments);
}
/**
* We let the internal library process the email sending depending on the platform.
* This will allow us to perform the required manipulation of the content, if needed.
*
* @since 1.15.2 (J) - 1.5.5 (WP)
*/
$mail_data = new VBOMailWrapper([
'sender' => [$from_address, $from_name],
'recipient' => $to,
'bcc' => $bcc_addresses,
'reply' => $reply_address,
'subject' => $subject,
'content' => $hmess,
'attachments' => $attachment,
]);
// unset queues for the next email sending operation
VikBooking::addAdminEmailRecipient(null, false, $reset = true);
VikBooking::addEmailAttachment(null, $reset = true);
// dispatch the email sending command
return VBOFactory::getPlatform()->getMailer()->send($mail_data);
}
/**
* @param $arr_values array
* @param $current_key string
* @param $empty_value string (J3.x only)
* @param $default
* @param $input_name string
* @param $record_id = '' string
*/
public function getDropDown($arr_values, $current_key, $empty_value, $default, $input_name, $record_id = '')
{
$dropdown = '';
$dropdown .= '<select name="'.$input_name.'" onchange="document.adminForm.submit();">'."\n";
$dropdown .= '<option value="">'.$default.'</option>'."\n";
$list = "\n";
foreach ($arr_values as $k => $v) {
$dropdown .= '<option value="'.$k.'"'.($k == $current_key ? ' selected="selected"' : '').'>'.$v.'</option>'."\n";
}
$dropdown .= '</select>'."\n";
return $dropdown;
}
public function loadSelect2()
{
static $st_loaded = null;
if ($st_loaded) {
// loaded flag
return;
}
// cache loaded flag
$st_loaded = 1;
// load JS + CSS
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_ADMIN_URI.'resources/select2.min.css');
$this->addScript(VBO_ADMIN_URI.'resources/select2.min.js');
}
/**
* Returns the HTML code to render a regular dropdown
* menu styled through the jQuery plugin Select2.
*
* @param $arr_values array
* @param $current_key string
* @param $input_name string
* @param $placeholder string used when the select has no selected option (it's empty)
* @param $empty_name [string] the name of the option to set an empty value to the field (<option>$empty_name</option>)
* @param $empty_val [string] the value of the option to set an empty value to the field (<option>$empty_val</option>)
* @param $onchange [string] javascript code for the onchange attribute
* @param $idattr [string] the identifier attribute of the select
*
* @return string
*/
public function getNiceSelect($arr_values, $current_key, $input_name, $placeholder, $empty_name = '', $empty_val = '', $onchange = 'document.adminForm.submit();', $idattr = '')
{
//load JS + CSS
$this->loadSelect2();
//attribute
$idattr = empty($idattr) ? rand(1, 999) : $idattr;
//select
$dropdown = '<select id="'.$idattr.'" name="'.$input_name.'"'.(!empty($onchange) ? ' onchange="'.$onchange.'"' : '').'>'."\n";
if (!empty($placeholder) && empty($current_key)) {
//in order for the placeholder value to appear, there must be a blank <option> as the first option in the select
$dropdown .= '<option></option>'."\n";
} else {
//unset the placeholder to not pass it to the select2 object, or the empty value will not be displayed
$placeholder = '';
}
if (strlen($empty_name) || strlen($empty_val)) {
$dropdown .= '<option value="'.$empty_val.'">'.$empty_name.'</option>'."\n";
}
foreach ($arr_values as $k => $v) {
$dropdown .= '<option value="'.$k.'"'.($k == $current_key ? ' selected="selected"' : '').'>'.$v.'</option>'."\n";
}
$dropdown .= '</select>'."\n";
//js code
$dropdown .= '<script type="text/javascript">'."\n";
$dropdown .= 'jQuery(function() {'."\n";
$dropdown .= ' jQuery("#'.$idattr.'").select2('.(!empty($placeholder) ? '{placeholder: "'.addslashes($placeholder).'"}' : '').');'."\n";
$dropdown .= '});'."\n";
$dropdown .= '</script>'."\n";
return $dropdown;
}
/**
* Adds the script declaration to render the Bootstrap JModal window.
* The suffix can be passed to generate other JS functions.
* Optionally pass JavaScript code for the 'show' and 'hide' events.
* For compatibility with the Joomla framework, this method should be
* echoed although it does not return anything on WordPress.
*
* @param $suffix string
* @param $hide_js string
* @param $show_js string
*
* @return void should still be echoed for compatibility with J.
*/
public function getJmodalScript($suffix = '', $hide_js = '', $show_js = '')
{
static $loaded = [];
$doc = JFactory::getDocument();
if (!isset($loaded[$suffix]))
{
$doc->addScriptDeclaration(
<<<JS
function vboOpenJModal$suffix(id, modal_url, new_title) {
var on_hide = null;
if ("$hide_js") {
on_hide = function() {
$hide_js
}
}
var on_show = null;
if ("$show_js") {
on_show = function() {
$show_js
}
}
wpOpenJModal(id, modal_url, on_show, on_hide);
if (new_title) {
jQuery('#jmodal-' + id + ' .modal-header h3').text(new_title);
}
return false;
}
JS
);
$loaded[$suffix] = 1;
}
}
/**
* Returns a safe sub-string string with the requested length, by
* avoiding errors for those errors not supporting multi-byte strings.
*
* @param string $text the text to apply the substr onto.
* @param string $len the length of the sub-string to take.
*
* @return string the portion of the string.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
public function safeSubstr($text, $len = 3)
{
$mb_supported = function_exists('mb_substr');
if ($len < 1) {
return $text;
}
return $mb_supported ? mb_substr($text, 0, $len, 'UTF-8') : substr($text, 0, $len);
}
/**
* Renders a date-time locale input element to pick a date and time.
*
* @param array $options Associative list of element options.
*
* @return string The HTML string necessary to render the date-time picker.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
public function renderDateTimePicker(array $options = [])
{
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('dtp_');
}
// ensure attributes are set
if (!($options['attributes'] ?? [])) {
$options['attributes'] = [];
}
// attributes name, value, min and max can also be specified outside the "attributes" key
if (($options['name'] ?? null) && !($options['attributes']['name'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['name'] = $options['name'];
}
if (($options['value'] ?? null) && !($options['attributes']['value'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['value'] = $options['value'];
}
if (($options['min'] ?? null) && !($options['attributes']['min'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['min'] = $options['min'];
}
if (($options['max'] ?? null) && !($options['attributes']['max'] ?? null)) {
// resort the attribute inside the apposite key
$options['attributes']['max'] = $options['max'];
}
// check for "min" attribute, required to hide seconds from the time-picker
if (!($options['attributes']['min'] ?? null)) {
// default to 10 years in the past
$options['attributes']['min'] = JFactory::getDate('-10 years')->format('Y-m-d\TH:i');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], $options['attributes']);
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build HTML string
$html = <<<HTML
<input type="datetime-local" {$attr_str} />
HTML;
// return the HTML string to be displayed
return $html;
}
/**
* Renders a select2 component to display existing tags or to add new ones.
*
* @param array $options Associative list of dropdown options.
* @param array $elements Associative list of element records.
* @param array $groups Optional list of element groups to source.
*
* @return string The HTML string necessary to render the dropdown.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
public function renderTagsDropDown(array $options = [], array $elements = [], array $groups = [])
{
// load select2 assets
$this->loadSelect2();
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('tdd_');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], ($options['attributes'] ?? []));
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build data sources
$data_sources = [];
foreach ($elements as $element) {
if (is_object($element)) {
$element = (array) $element;
}
if (empty($element['id'])) {
continue;
}
// build element data source
$data_source = [
'id' => $element['id'],
'text' => $element['name'] ?? $element['id'],
'color' => $element['color'] ?? null,
'hex' => $element['hex'] ?? null,
];
// check for option selected status
if (($options['selected_value'] ?? null) && $options['selected_value'] == $element['id']) {
$data_source['selected'] = true;
} elseif (is_array($options['selected_values'] ?? null) && in_array($element['id'], $options['selected_values'])) {
$data_source['selected'] = true;
}
// check for option disabled status
if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $element['id']) {
$data_source['disabled'] = true;
} elseif (is_array($options['disabled_values'] ?? null) && in_array($element['id'], $options['disabled_values'])) {
$data_source['disabled'] = true;
}
// push element data source
$data_sources[] = $data_source;
}
// append groups to source as data elements
foreach ($groups as $group) {
if (is_object($group)) {
// always cast to array
$group = (array) $group;
}
if (!is_array($group) || empty($group['text']) || empty($group['elements'])) {
continue;
}
// filter out invalid group elements
$group['elements'] = array_filter((array) $group['elements'], function($group_element) {
return is_array($group_element) && isset($group_element['id']) && isset($group_element['text']);
});
// check for option selected status
if (($options['selected_value'] ?? null) || (is_array($options['selected_values'] ?? null) && $options['selected_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['selected_value'] ?? null)) {
if ($options['selected_value'] == $element['id']) {
$group['elements'][$k]['selected'] = true;
}
} else {
if (in_array($element['id'], $options['selected_values'])) {
$group['elements'][$k]['selected'] = true;
}
}
}
}
// check for option disabled status
if (($options['disabled_value'] ?? null) || (is_array($options['disabled_values'] ?? null) && $options['disabled_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['disabled_value'] ?? null)) {
if ($options['disabled_value'] == $element['id']) {
$group['elements'][$k]['disabled'] = true;
}
} else {
if (in_array($element['id'], $options['disabled_values'])) {
$group['elements'][$k]['disabled'] = true;
}
}
}
}
// push group element data source
$data_sources[] = [
'text' => $group['text'],
'children' => $group['elements'],
];
}
// data sources JSON encoded string
$data_sources_str = json_encode($data_sources);
// empty option tag
$empty_option = '';
if (!($options['attributes']['multiple'] ?? null)) {
$empty_option = '<option></option>';
}
// clearing allowed
$clearable = (bool) ($options['allow_clear'] ?? 1);
$clearable_str = $clearable ? 'true' : 'false';
// tags allowed (for entering custom values)
$taggable = (bool) ($options['allow_tags'] ?? 1);
$taggable_str = $taggable ? 'true' : 'false';
// placeholder text
$placeholder = json_encode($options['placeholder'] ?? '');
// select2 width
$sel2width = $options['width'] ?? 'resolve';
// supported tag colors
$colors = json_encode((array) ($options['colors'] ?? []));
// build HTML string
$html = <<<HTML
<select {$attr_str}>{$empty_option}</select>
HTML;
// build script declaration
$js_decl = <<<JAVASCRIPT
jQuery(function() {
const supportedColors = $colors;
let remainingColors = supportedColors.slice();
if (remainingColors.length) {
// internally rearrange tags by ID to preserve the linear color scheme supported by default
const existingTags = {$data_sources_str}.sort((a, b) => parseInt(a.id) - parseInt(b.id));
// iterate all the existing tags
existingTags.forEach((tag) => {
let index = remainingColors.indexOf(tag.color);
if (index != -1) {
// remove the tag color from the remaining ones
remainingColors.splice(index, 1);
if (remainingColors.length == 0) {
// no more remaining colors, reset array
remainingColors = supportedColors.slice();
}
}
});
}
jQuery('select#{$options['id']}').select2({
width: '$sel2width',
allowClear: $clearable_str,
data: $data_sources_str,
placeholder: $placeholder,
tags: $taggable_str,
createTag: function (params) {
const term = (params.term || '').replace(/:/g, '').trim();
if (term === '') {
return null;
}
// temporarily assign the first available color
const color = remainingColors[0];
return {
id: term + ':' + color,
text: term,
color: color,
newTag: true,
};
},
templateResult: (element) => {
if (!element.id) {
return element.text;
}
let tag_class = '';
let tag_style = '';
if (element?.color) {
tag_class = element.color;
} else if (element?.hex) {
tag_style = 'background-color: ' + element.hex + ';';
} else {
tag_class = (element.id + '').toLowerCase().replace(/[^a-z0-9]/ig, '');
}
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>');
},
templateSelection: (element) => {
if (!element.id) {
return element.text;
}
let tag_elem = jQuery('<span></span>')
.addClass('vbo-sel2-selected-tag')
.text(element.text);
if (element.newTag) {
// we can understand here whether a new tag has been officially submitted
element.newTag = false;
// permanently detach the last color assigned
remainingColors.shift();
if (remainingColors.length == 0) {
// no more remaining colors, reset array
remainingColors = supportedColors.slice();
}
}
if (element?.color) {
tag_elem.addClass(element.color);
} else if (element?.hex) {
tag_elem.css('background-color', element.hex);
} else {
tag_elem.addClass((element.id + '').toLowerCase().replace(/[^a-z0-9]/ig, ''));
}
return tag_elem;
},
});
});
JAVASCRIPT;
if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
// concatenate script to HTML string when doing an AJAX request
$html .= "\n" . '<script>' . $js_decl . '</script>';
} else {
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($js_decl);
}
// return the HTML string to be displayed
return $html;
}
/**
* Renders a select2 component to display elements with thumbnails.
*
* @param array $options Associative list of dropdown options.
* @param array $elements Associative list of element records.
* @param array $groups Optional list of element groups to source.
*
* @return string The HTML string necessary to render the dropdown.
*
* @since 1.17.5 (J) - 1.7.5 (WP)
*/
public function renderElementsDropDown(array $options = [], array $elements = [], array $groups = [])
{
if (!$elements && ($options['elements'] ?? '') == 'listings') {
// load listing records
$filter_listing_ids = (array) ($options['element_ids'] ?? []);
$elements = VikBooking::getAvailabilityInstance(true)->loadRooms($filter_listing_ids, 0, true);
}
if (!$elements) {
// abort
return '';
}
// load select2 assets
$this->loadSelect2();
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('ldd_');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], ($options['attributes'] ?? []));
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build data sources
$data_sources = [];
$base_img_path = implode(DIRECTORY_SEPARATOR, [VBO_SITE_PATH, 'resources', 'uploads']) . DIRECTORY_SEPARATOR;
$base_img_uri = VBO_SITE_URI . 'resources/uploads/';
foreach ($elements as $element) {
if (is_object($element)) {
$element = (array) $element;
}
if (empty($element['id'])) {
continue;
}
// element image
$element_img_uri = null;
if (($options['elements'] ?? '') == 'listings') {
// check for listing mini-thumbnail
if (!empty($element['img']) && is_file($base_img_path . 'mini_' . $element['img'])) {
$element_img_uri = $base_img_uri . 'mini_' . $element['img'];
}
}
if (!$element_img_uri && ($options['element_def_img_uri'] ?? '')) {
// use the provided default element image
$element_img_uri = $options['element_def_img_uri'];
} elseif (!$element_img_uri && ($element['img_uri'] ?? '')) {
// use the provided element image URI
$element_img_uri = $element['img_uri'];
}
// build element data source
$data_source = [
'id' => $element['id'],
'text' => $element['name'] ?? $element['id'],
'img' => $element_img_uri,
];
// check for option selected status
if (($options['selected_value'] ?? null) && $options['selected_value'] == $element['id']) {
$data_source['selected'] = true;
} elseif (is_array($options['selected_values'] ?? null) && in_array($element['id'], $options['selected_values'])) {
$data_source['selected'] = true;
}
// check for option disabled status
if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $element['id']) {
$data_source['disabled'] = true;
} elseif (is_array($options['disabled_values'] ?? null) && in_array($element['id'], $options['disabled_values'])) {
$data_source['disabled'] = true;
}
// push element data source
$data_sources[] = $data_source;
}
// check for listing category groups
if (($options['elements'] ?? '') == 'listings' && ($options['load_categories'] ?? null)) {
// load listing categories
$categories = VikBooking::getAvailabilityInstance(true)->loadRoomCategories();
if (count($categories) > 1) {
// turn the category IDs into negative
$categories = array_map(function($cat) {
return [
'id' => ($cat['id'] - ($cat['id'] * 2)),
'text' => $cat['name'],
];
}, $categories);
// active or disabled listing categories
foreach ($categories as &$category) {
// check for option selected status
if (($options['selected_value'] ?? null) && $options['selected_value'] == $category['id']) {
$category['selected'] = true;
} elseif (is_array($options['selected_values'] ?? null) && in_array($category['id'], $options['selected_values'])) {
$category['selected'] = true;
}
// check for option disabled status
if (($options['disabled_value'] ?? null) && $options['disabled_value'] == $category['id']) {
$category['disabled'] = true;
} elseif (is_array($options['disabled_values'] ?? null) && in_array($category['id'], $options['disabled_values'])) {
$category['disabled'] = true;
}
}
unset($category);
// push listing category groups
$groups[] = [
'text' => $options['categories_lbl'] ?? 'Filter by category',
'elements' => $categories,
];
}
}
// append groups to source as data elements
foreach ($groups as $group) {
if (is_object($group)) {
// always cast to array
$group = (array) $group;
}
if (!is_array($group) || empty($group['text']) || empty($group['elements'])) {
continue;
}
// filter out invalid group elements
$group['elements'] = array_filter((array) $group['elements'], function($group_element) {
return is_array($group_element) && isset($group_element['id']) && isset($group_element['text']);
});
// check for option selected status
if (($options['selected_value'] ?? null) || (is_array($options['selected_values'] ?? null) && $options['selected_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['selected_value'] ?? null)) {
if ($options['selected_value'] == $element['id']) {
$group['elements'][$k]['selected'] = true;
}
} else {
if (in_array($element['id'], $options['selected_values'])) {
$group['elements'][$k]['selected'] = true;
}
}
}
}
// check for option disabled status
if (($options['disabled_value'] ?? null) || (is_array($options['disabled_values'] ?? null) && $options['disabled_values'])) {
foreach ($group['elements'] as $k => $element) {
if (($options['disabled_value'] ?? null)) {
if ($options['disabled_value'] == $element['id']) {
$group['elements'][$k]['disabled'] = true;
}
} else {
if (in_array($element['id'], $options['disabled_values'])) {
$group['elements'][$k]['disabled'] = true;
}
}
}
}
// push group element data source
$data_sources[] = [
'text' => $group['text'],
'children' => $group['elements'],
];
}
// data sources JSON encoded string
$data_sources_str = json_encode($data_sources);
// empty option tag
$empty_option = '';
if (!($options['attributes']['multiple'] ?? null)) {
$empty_option = '<option></option>';
}
// clearing allowed
$clearable = (bool) ($options['allow_clear'] ?? 1);
$clearable_str = $clearable ? 'true' : 'false';
// placeholder text
$placeholder = json_encode($options['placeholder'] ?? '');
// select2 width
$sel2width = $options['width'] ?? 'resolve';
// build HTML string
$html = <<<HTML
<select {$attr_str}>{$empty_option}</select>
HTML;
// template selection function
$selectionFn = '';
if ($options['style_selection'] ?? null) {
$defaultSelectionIcn = $options['default_selection_icon'] ?? '';
$selectionFn = <<<JAVASCRIPT
templateSelection: (element) => {
if (!element.id) {
return element.text;
}
let sel_elem = jQuery('<span></span>')
.addClass('vbo-sel2-selected-tag')
.text(element.text);
if (element.img) {
let avatar_elem = jQuery('<img/>')
.addClass('vbo-sel2-selected-tag-avatar')
.attr('src', element.img);
sel_elem.prepend(avatar_elem);
} else if ('$defaultSelectionIcn') {
let icn_elem = jQuery('<i></i>')
.addClass('$defaultSelectionIcn')
.addClass('vbo-sel2-selected-tag-avatar');
sel_elem.prepend(icn_elem);
}
return sel_elem;
}
JAVASCRIPT;
}
// build script declaration
$js_decl = <<<JAVASCRIPT
jQuery(function() {
jQuery('select#{$options['id']}').select2({
width: '$sel2width',
allowClear: $clearable_str,
data: $data_sources_str,
placeholder: $placeholder,
templateResult: (element) => {
if (!element.img) {
return element.text;
}
return jQuery('<span class="vbo-sel2-element-img"><img src="' + element.img + '" /> <span>' + element.text + '</span></span>');
},
$selectionFn
});
});
JAVASCRIPT;
if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
// concatenate script to HTML string when doing an AJAX request
$html .= "\n" . '<script>' . $js_decl . '</script>';
} else {
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($js_decl);
}
// return the HTML string to be displayed
return $html;
}
/**
* Renders a select2 component to display searchable elements with thumbnails.
*
* @param array $options Associative list of dropdown and search options.
*
* @return string The HTML string necessary to render the dropdown.
*
* @since 1.18.0 (J) - 1.8.0 (WP)
*/
public function renderSearchElementsDropDown(array $options = [])
{
if (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'bookings') {
// search bookings endpoint
$options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.bookings_search');
} elseif (!($options['endpoint'] ?? null) && ($options['elements'] ?? '') == 'customers') {
// search customers endpoint
$options['endpoint'] = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=bookings.customer_elements_search');
}
if (empty($options['endpoint'])) {
// abort
return '';
}
// set AJAX endpoint for searching
$endpoint = $options['endpoint'];
if ($options['load_assets'] ?? true) {
// load select2 assets
$this->loadSelect2();
}
if (!($options['id'] ?? null)) {
// the ID attribute is mandatory
$options['id'] = uniqid('ldd_');
}
// build attributes list
$attributes = array_merge([
'id' => $options['id'],
], ($options['attributes'] ?? []));
// build attributes string
$attr_str = implode(' ', array_map(function($name, $value) {
return $name . '="' . htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') . '"';
}, array_keys($attributes), array_values($attributes)));
// build data sources
$data_sources = [];
// check for default option(s) selected status
$selected_options = [];
if (is_array($options['selected_values'] ?? null) && $options['selected_values']) {
$selected_options = $options['selected_values'];
} elseif (($options['selected_value'] ?? null)) {
$selected_options[] = $options['selected_value'];
}
foreach ($selected_options as $sel_option) {
if (is_array($sel_option) || is_object($sel_option)) {
// element object expected
$sel_option = (array) $sel_option;
if (isset($sel_option['id']) && isset($sel_option['text'])) {
// push valid data source
$sel_option['selected'] = true;
$data_sources[] = $sel_option;
}
} else {
// selected ID expected, push a data source object with limited information
$data_sources[] = [
'id' => $sel_option,
'text' => $sel_option,
'selected' => true,
];
}
}
// data sources JSON encoded string
$data_sources_str = json_encode($data_sources);
// empty option tag
$empty_option = '';
if (!($options['attributes']['multiple'] ?? null)) {
$empty_option = '<option></option>';
}
// clearing allowed
$clearable = (bool) ($options['allow_clear'] ?? 1);
$clearable_str = $clearable ? 'true' : 'false';
// placeholder text
$placeholder = json_encode($options['placeholder'] ?? '');
// minimum input length for searching
$min_inp_len = 1;
// search language definitions
$lang_error_loading = json_encode($options['language']['error'] ?? JText::translate('VBO_ERR_LOAD_RESULTS'));
$lang_no_results = json_encode($options['language']['noresults'] ?? JText::translate('VBO_NO_RECORDS_FOUND'));
$lang_searching = json_encode($options['language']['searching'] ?? JText::translate('VBO_SEARCHING'));
// this language definition is NOT quoted, because it is not JSON-encoded
$lang_inptooshort = $min_inp_len > 1 && ($options['language']['inptooshort'] ?? '') ? $options['language']['inptooshort'] : '';
// selection/result with ID
$selection_with_id = ($options['selected_id'] ?? null) ? 'true' : 'false';
// selection extra class
$selection_class = $options['selection_class'] ?? '';
// selection click open widget
$selection_click_widget = $options['selection_click_widget'] ?? '';
// selection dispatch event
$selection_event = $options['selection_event'] ?? '';
// select2 width
$sel2width = $options['width'] ?? 'resolve';
// build HTML string
$html = <<<HTML
<select {$attr_str}>{$empty_option}</select>
HTML;
// template selection function
$selectionFn = '';
if ($options['style_selection'] ?? null) {
$defaultSelectionIcn = $options['default_selection_icon'] ?? '';
$selectionFn = <<<JAVASCRIPT
templateSelection: (element) => {
if (!element.id) {
return element.text;
}
let sel_elem = jQuery('<span></span>')
.addClass(('vbo-sel2-selected-search-elem $selection_class').trim())
.text(element.text + ($selection_with_id ? ' #' + element.id : ''));
if (element.img) {
let avatar_elem = jQuery('<img/>')
.addClass('vbo-sel2-selected-search-elem-avatar')
.attr('src', element.img);
if (element.img_title) {
avatar_elem.attr('title', element.img_title);
}
sel_elem.prepend(avatar_elem);
} else if (element.icon_class) {
let icn_wrap = jQuery('<span></span>')
.addClass('vbo-sel2-selected-search-elem-avatar');
let icn_elem = jQuery('<i></i>')
.addClass(element.icon_class);
icn_wrap.append(icn_elem);
sel_elem.prepend(icn_wrap);
} else if ('$defaultSelectionIcn') {
let icn_elem = jQuery('<i></i>')
.addClass('$defaultSelectionIcn')
.addClass('vbo-sel2-selected-search-elem-avatar');
sel_elem.prepend(icn_elem);
}
if ("$selection_click_widget" == 'booking_details') {
sel_elem.on('click', () => {
VBOCore.handleDisplayWidgetNotification({
widget_id: 'booking_details',
}, {
booking_id: element.id,
modal_options: {
suffix: 'vbo-booking-details-inner',
body_prepend: false,
enlargeable: false,
minimizeable: false,
},
});
});
}
return sel_elem;
}
JAVASCRIPT;
}
// build script declaration
$js_decl = <<<JAVASCRIPT
jQuery(function() {
jQuery('select#{$options['id']}').select2({
width: '$sel2width',
allowClear: $clearable_str,
data: $data_sources_str,
placeholder: $placeholder,
minimumInputLength: $min_inp_len,
language: {
errorLoading: () => {
return $lang_error_loading;
},
noResults: () => {
return $lang_no_results;
},
searching: () => {
return $lang_searching;
},
inputTooShort: () => {
return "$lang_inptooshort";
},
},
ajax: {
delay: 350,
url: "$endpoint",
dataType: 'json',
},
templateResult: (element) => {
if (!element.id) {
return element.text;
}
let search_elem = jQuery('<span></span>')
.addClass('vbo-sel2-search-elem');
let search_avatar = jQuery('<span></span>')
.addClass('vbo-sel2-search-elem-avatar');
let elem_name = jQuery('<span></span>')
.addClass('vbo-sel2-search-elem-name')
.text(element.text + ($selection_with_id ? ' #' + element.id : ''));
if (element.img) {
search_avatar.append('<img src="' + element.img + '" ' + (element.img_title ? 'title="' + element.img_title + '" ' : '') + '/>');
} else if (element.icon_class) {
search_avatar.append('<i class="' + element.icon_class + '"></i>');
}
search_avatar.append(elem_name);
search_elem.append(search_avatar);
return search_elem;
},
$selectionFn
});
if ("$selection_event") {
jQuery('select#{$options['id']}').on('select2:select', (e) => {
let element = e?.params?.data || e;
VBOCore.emitEvent("$selection_event", {
element: element,
});
});
}
});
JAVASCRIPT;
if ((VBOPlatformDetection::isWordPress() && wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && !strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
// concatenate script to HTML string when doing an AJAX request
$html .= "\n" . '<script>' . $js_decl . '</script>';
} else {
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration($js_decl);
}
// return the HTML string to be displayed
return $html;
}
/**
* Loads the assets for setting up the VBOCore JS in the site section.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.17.4 (J) - 1.7.4 (WP)
*/
public function loadCoreJS(array $options = [])
{
static $corejs_loaded = null;
if ($corejs_loaded) {
// loaded flag
return;
}
// cache loaded flag
$corejs_loaded = 1;
// add script
$this->addScript(VBO_ADMIN_URI . 'resources/vbocore.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
if ($options) {
$core_options = json_encode((object) $options, JSON_PRETTY_PRINT);
// add script declaration to document
JFactory::getDocument()->addScriptDeclaration(
<<<JS
jQuery(function() {
VBOCore.setOptions($core_options);
});
JS
);
}
}
/**
* Loads the assets for rendering the signature pad.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.17.4 (J) - 1.7.4 (WP)
*/
public function loadSignaturePad(array $options = [])
{
static $signpad_loaded = null;
if ($signpad_loaded) {
// loaded flag
return;
}
// cache loaded flag
$signpad_loaded = 1;
// add script
$this->addScript(VBO_SITE_URI . 'resources/signature_pad.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
}
/**
* Loads the assets solely needed to render the DRP calendar.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.17.3 (J) - 1.7.3 (WP)
*/
public function loadDatesRangePicker(array $options = [])
{
static $drp_loaded = null;
if ($drp_loaded) {
// loaded flag
return;
}
// cache loaded flag
$drp_loaded = 1;
// add DRP script
$this->addScript(VBO_SITE_URI . 'resources/datesrangepicker.js', ['version' => VIKBOOKING_SOFTWARE_VERSION]);
// load JS lang defs
JText::script('VBPICKUPROOM');
JText::script('VBRETURNROOM');
JText::script('VBO_MIN_STAY_NIGHTS');
JText::script('VBO_CLEAR_DATES');
JText::script('VBO_CLOSE');
}
/**
* Loads the necessary JS and CSS assets to render the jQuery UI Datepicker calendar.
* It is also possible to load the assets for the DatesRangePicker extension.
*
* @param array $options Associative list of loading options.
*
* @return void
*
* @since 1.1.0
* @since 1.15.0 (J) - 1.5.0 (WP) the lang definitions work for both front and back -ends.
* @since 1.17.3 (J) - 1.7.3 (WP) added support for the DatesRangePicker extension.
*/
public function loadDatePicker(array $options = [])
{
static $datepicker_loaded = null;
if ($datepicker_loaded) {
// loaded flag
return;
}
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_SITE_URI . 'resources/jquery-ui.min.css');
JHtml::fetch('jquery.framework', true, true);
$this->addScript(VBO_SITE_URI . 'resources/jquery-ui.min.js');
if (!strcasecmp(($options['type'] ?? ''), 'dates_range')) {
// load DRP calendar assets
$this->loadDatesRangePicker($options);
}
$vbo_df = VikBooking::getDateFormat();
$juidf = $vbo_df == "%d/%m/%Y" ? 'dd/mm/yy' : ($vbo_df == "%m/%d/%Y" ? 'mm/dd/yy' : 'yy/mm/dd');
$is_rtl_lan = false;
$day_names_min_len = 2;
$now_lang = JFactory::getLanguage();
if (method_exists($now_lang, 'isRtl')) {
$is_rtl_lan = $now_lang->isRtl();
if ($is_rtl_lan) {
// for most RTL languages, 2 chars for the week-days would not make sense
$day_names_min_len = 3;
}
}
// build default regional values for datepicker
$vbo_dp_regional_vals = [
'closeText' => JText::translate('VBJQCALDONE'),
'prevText' => JText::translate('VBJQCALPREV'),
'nextText' => JText::translate('VBJQCALNEXT'),
'currentText' => JText::translate('VBJQCALTODAY'),
'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'),
],
'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')),
],
'dayNames' => [
JText::translate('VBWEEKDAYZERO'),
JText::translate('VBWEEKDAYONE'),
JText::translate('VBWEEKDAYTWO'),
JText::translate('VBWEEKDAYTHREE'),
JText::translate('VBWEEKDAYFOUR'),
JText::translate('VBWEEKDAYFIVE'),
JText::translate('VBWEEKDAYSIX'),
],
'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')),
],
'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),
],
'weekHeader' => JText::translate('VBJQCALWKHEADER'),
'dateFormat' => $juidf,
'firstDay' => VikBooking::getFirstWeekDay(),
'isRTL' => $is_rtl_lan,
'showMonthAfterYear' => false,
'yearSuffix' => '',
];
$ldecl = '
jQuery(function($) {' . "\n" . '
$.datepicker.regional["vikbooking"] = ' . json_encode($vbo_dp_regional_vals) . ';
$.datepicker.setDefaults($.datepicker.regional["vikbooking"]);' . "\n" . '
});';
/**
* Trigger event to allow third party plugins to overwrite the JS declaration for the datepicker.
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeDeclareDatepickerRegionalVikBooking', [$is_rtl_lan, $now_lang->getTag(), &$ldecl]);
// add script declaration
$document->addScriptDeclaration($ldecl);
// cache loaded flag
$datepicker_loaded = 1;
}
/**
* Loads the CMS's native datepicker calendar.
*
* @since 1.10
*/
public function getCalendar($val, $name, $id = null, $df = null, array $attributes = array())
{
if ($df === null)
{
$df = VikBooking::getDateFormat();
}
return parent::calendar($val, $name, $id, $df, $attributes);
}
/**
* Returns a masked e-mail address. The e-mail are masked using
* a technique to encode the bytes in hexadecimal representation.
* The chunk of the masked e-mail will be also encoded to be HTML readable.
*
* @param string $email The e-mail to mask.
* @param boolean $reverse True to reverse the e-mail address.
* Only if the e-mail is not contained into an attribute.
*
* @return string The masked e-mail address.
*/
public function maskMail($email, $reverse = false)
{
if ($reverse)
{
// reverse the e-mail address
$email = strrev($email);
}
// converts the e-mail address from bin to hex
$email = bin2hex($email);
// append ;&#x sequence after every chunk of the masked e-mail
$email = chunk_split($email, 2, ";&#x");
// prepend &#x sequence before the address and trim the ending sequence
$email = "&#x" . substr($email, 0, -3);
return $email;
}
/**
* Returns a safemail tag to avoid the bots spoof a plain address.
*
* @param string $email The e-mail address to mask.
* @param boolean $mail_to True if the address should be wrapped
* within a "mailto" link.
*
* @return string The HTML tag containing the masked address.
*
* @uses maskMail()
*/
public function safeMailTag($email, $mail_to = false)
{
// include the CSS declaration to reverse the text contained in the <safemail> tags
JFactory::getDocument()->addStyleDeclaration('safemail {direction: rtl;unicode-bidi: bidi-override;}');
// mask the reversed e-mail address
$masked = $this->maskMail($email, true);
// include the address into a custom <safemail> tag
$tag = "<safemail>$masked</safemail>";
if ($mail_to)
{
// mask the address for mailto command (do not use reverse)
$mailto = $this->maskMail($email);
// wrap the safemail tag within a mailto link
$tag = "<a href=\"mailto:$mailto\" class=\"mailto\">$tag</a>";
}
return $tag;
}
/**
* Loads and echoes the script necessary to render the Fancybox
* plugin for jQuery to open images or iframes within a modal box.
* This resolves conflicts with some Bootstrap or Joomla (4) versions
* that do not support the old-native CSS class .modal with "behavior.modal".
* Mainly made to open pictures in a modal box, so the default "type" is set to "image".
* By passing a custom $opts string, the "type" property could be set to "iframe", but
* in this case it's better to use the other method of this class (Jmodal).
* The base jQuery library should be already loaded when using this method.
*
* @param string $selector The jQuery selector to trigger Fancybox.
* @param string $opts The options object for the Fancybox setup.
* @param boolean $reloadfunc If true, an additional function is included in the script
* to apply again Fancybox to newly added images to the DOM (via Ajax).
*
* @return void
*
* @uses addScript()
*/
public function prepareModalBox($selector = '.vbomodal', $opts = '', $reloadfunc = false)
{
if (empty($opts)) {
$opts = '{
"helpers": {
"overlay": {
"locked": false
}
},
"width": "70%",
"height": "75%",
"autoScale": true,
"transitionIn": "none",
"transitionOut": "none",
"padding": 0,
"type": "image"
}';
}
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_SITE_URI.'resources/jquery.fancybox.css');
$this->addScript(VBO_SITE_URI.'resources/jquery.fancybox.js');
$reloadjs = '
function reloadFancybox() {
jQuery("'.$selector.'").fancybox('.$opts.');
}
';
$js = '
<script type="text/javascript">
jQuery(function() {
jQuery("'.$selector.'").fancybox('.$opts.');
});'.($reloadfunc ? $reloadjs : '').'
</script>';
echo $js;
}
/**
* Method used to handle the reCAPTCHA events.
*
* @param string $event The reCAPTCHA event to trigger.
* Here's the list of the accepted events:
* - display Returns the HTML used to
* display the reCAPTCHA input.
* - check Validates the POST data to make sure
* the reCAPTCHA input was checked.
* @param array $options A configuration array.
*
* @return mixed The event response.
*
* @since 1.2.3
* @wponly the Joomla integration differs
*/
public function reCaptcha($event = 'display', array $options = array())
{
$response = null;
// an optional configuration array (just leave empty)
$options = array();
// trigger reCAPTCHA display event to fill $response var
do_action_ref_array('vik_recaptcha_' . $event, array(&$response, $options));
// display reCAPTCHA by echoing it (empty in case reCAPTCHA is not available)
return $response;
}
/**
* Checks if the com_user captcha is configured.
* In case the parameter is set to global, the default one
* will be retrieved.
*
* @param string $plugin The plugin name to check ('recaptcha' by default).
*
* @return boolean True if configured, otherwise false.
*
* @since 1.2.3
* @wponly the Joomla integration differs
*/
public function isCaptcha($plugin = 'recaptcha')
{
return apply_filters('vik_' . $plugin . '_on', false);
}
/**
* Checks if the global captcha is configured.
*
* @param string $plugin The plugin name to check ('recaptcha' by default).
*
* @return boolean True if configured, otherwise false.
*
* @since 1.2.3
*/
public function isGlobalCaptcha($plugin = 'recaptcha')
{
return $this->isCaptcha($plugin);
}
/**
* Method used to obtain a WordPress media form field.
*
* @return string The media in HTML.
*
* @since 1.3.0
*/
public function getMediaField($name, $value = null, array $data = array())
{
// check if WordPress is installed
if (VBOPlatformDetection::isWordPress())
{
add_action('admin_enqueue_scripts', function() {
wp_enqueue_media();
});
// import form field class
JLoader::import('adapter.form.field');
// create XML field manifest
$xml = "<field name=\"$name\" type=\"media\" modowner=\"vikbooking\" />";
// instantiate field
$field = JFormField::getInstance(simplexml_load_string($xml));
// overwrite name and value within data
$data['name'] = $name;
$data['value'] = $value;
// inject display data within field instance
foreach ($data as $k => $v)
{
$field->bind($v, $k);
}
// render field
return $field->render();
}
// fallback to Joomla
// init media field
$field = new JFormFieldMedia(null, $value);
// setup an empty form as placeholder
$field->setForm(new JForm('vikbooking.media'));
// force field attributes
$data['name'] = $name;
$data['value'] = $value;
if (empty($data['previewWidth']))
{
// there is no preview width, set a defualt value
// to make the image visible within the popover
$data['previewWidth'] = 480;
}
// render the field
return $field->render('joomla.form.field.media', $data);
}
/**
* Displays a multi-state toggle switch element with unlimited buttons.
* Custom values, contents, labels, attributes and JS events can be attached
* to each button. VCM will use this same method.
*
* @param string $name the input name equal for all radio buttons.
* @param string $value the current input field value to be pre-selected.
* @param array $values list of radio buttons with each value.
* @param array $labels list of contents for each button trigger.
* @param array $attrs list of associative array attributes for each button.
* @param array $wrap list of associative array attributes for the wrapper.
*
* @return string the necessary HTML to render the multi-state toggle switch.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
public function multiStateToggleSwitchField($name, $value, $values = array(), $labels = array(), $attrs = array(), $wrap = array())
{
static $tooltip_js_declared = null;
// whether tooltip for titles is needed
$needs_tooltip = false;
// HTML container
$multi_state_switch = '';
if (!is_array($values) || !count($values)) {
// values must be set or we don't know what buttons to display
return $multi_state_switch;
}
// build default classes for the tri-state toggle switch (with 3 buttons)
$def_tristate_cls = array(
'vik-multiswitch-radiobtn-on',
'vik-multiswitch-radiobtn-def',
'vik-multiswitch-radiobtn-off',
);
// start wrapper
$multi_state_switch .= "\n" . '<div class="vik-multiswitch-wrap' . (isset($wrap['class']) ? (' ' . $wrap['class']) : '') . '">' . "\n";
foreach ($values as $btn_k => $btn_val) {
// build default classes for button label
$btn_classes = array('vik-multiswitch-radiobtn');
if (isset($def_tristate_cls[$btn_k])) {
// push default class for a 3-state toggle switch
array_push($btn_classes, $def_tristate_cls[$btn_k]);
}
// check if additional custom classes have been defined for this button
if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['label_class']) && !empty($attrs[$btn_k]['label_class'])) {
if (is_array($attrs[$btn_k]['label_class'])) {
// list of additional classes for this button
$btn_classes = array_merge($btn_classes, $attrs[$btn_k]['label_class']);
} elseif (is_string($attrs[$btn_k]['label_class'])) {
// multiple classes should be space-separated
array_push($btn_classes, $attrs[$btn_k]['label_class']);
}
}
// check title as first thing, even though this is passed along with the labels
$label_title = '';
if (isset($labels[$btn_k]) && !is_scalar($labels[$btn_k]) && isset($labels[$btn_k]['title'])) {
$needs_tooltip = true;
$label_title = ' title="' . addslashes(htmlentities($labels[$btn_k]['title'])) . '"';
}
// start button label
$multi_state_switch .= "\t" . '<label class="' . implode(' ', $btn_classes) . '"' . $label_title . '>' . "\n";
// check button input radio
$radio_attributes = array();
if (($value !== null && $value == $btn_val) || ($value === null && $btn_k === 0)) {
// this radio button must be checked (pre-selected)
$radio_attributes['checked'] = true;
}
// check if custom attributes were specified for this input
if (isset($attrs[$btn_k]) && isset($attrs[$btn_k]['input'])) {
// must be an associative array with key = attribute name, value = attribute value
foreach ($attrs[$btn_k]['input'] as $attr_name => $attr_val) {
// javascript events could be attached like 'onchange'=>'myCallback(this.value)'
$radio_attributes[$attr_name] = $attr_val;
}
}
$radio_attr_string = '';
foreach ($radio_attributes as $attr_name => $attr_val) {
if ($attr_val === true) {
// short-attribute name, like "checked"
$radio_attr_string .= $attr_name . ' ';
continue;
}
$radio_attr_string .= $attr_name . '="' . $attr_val . '" ';
}
$multi_state_switch .= "\t\t" . '<input type="radio" name="' . $name . '" value="' . $btn_val . '" ' . $radio_attr_string . '/>' . "\n";
// add button trigger
$multi_state_switch .= "\t\t" . '<span class="vik-multiswitch-trigger"></span>' . "\n";
// check button label text
if (isset($labels[$btn_k])) {
/**
* By default, the buttons of the toggle switch use an animation,
* which requires an absolute positioning of the "label-text".
* For this reason, there cannot be a minimum width for these texts
* and so the content should fit the default width. Usually, using
* a font-awesome icon is the best content. For using literal texts,
* like "Dark", "Light" etc.. the class "vik-multiswitch-noanimation"
* should be passed to the button label text.
*/
$label_txt = '';
$label_class = '';
if (!is_scalar($labels[$btn_k])) {
// with an associative array we accept value, title and custom classes
if (isset($labels[$btn_k]['value'])) {
$label_txt = $labels[$btn_k]['value'];
}
if (isset($labels[$btn_k]['class'])) {
$label_class = ' ' . ltrim($labels[$btn_k]['class']);
}
} else {
// just a string, maybe with text or HTML mixed content
$label_txt = $labels[$btn_k];
}
if (strlen($label_txt)) {
// append button label text only if some text has been defined
$multi_state_switch .= "\t\t" . '<span class="vik-multiswitch-txt' . $label_class . '">' . $label_txt . '</span>' . "\n";
}
}
// end button label
$multi_state_switch .= "\t" . '</label>' . "\n";
}
// end wrapper
$multi_state_switch .= '</div>' . "\n";
// check tooltip JS rendering
if (!$tooltip_js_declared && $needs_tooltip) {
// turn static flag on
$tooltip_js_declared = 1;
// add script declaration for JS rendering of tooltips
$doc = JFactory::getDocument();
$doc->addScriptDeclaration(
<<<JS
jQuery(function() {
if (typeof jQuery.fn.tooltip === 'function') {
jQuery('.vik-multiswitch-wrap label').tooltip();
}
});
JS
);
}
return $multi_state_switch;
}
/**
* Returns a list of supported fonts for the third-party visual editor (Quill).
*
* @param bool $short_names whether to return short font names.
*
* @return array list of font family names or short names.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
*/
public function getVisualEditorFonts($short_names = false)
{
// supported fonts
$font_families = ['Sans Serif', 'Arial', 'Courier', 'Garamond', 'Tahoma', 'Times New Roman', 'Verdana', 'Inconsolata', 'Sailec Light', 'Monospace'];
if (!$short_names) {
// return the regular names to be displayed
return $font_families;
}
// return the "short" names of the supported fonts
return array_map(function($font) {
return str_replace(' ', '-', strtolower($font));
}, $font_families);
}
/**
* Loads the necessary language definitions for the third-party visual editor (Quill).
*
* @return void
*
* @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
*/
public function loadVisualEditorDefinitions()
{
static $loaded = null;
if ($loaded) {
return;
}
$loaded = 1;
// load language definitions for JS
JText::script('VBO_CONT_WRAPPER');
JText::script('VBO_CONT_WRAPPER_HELP');
JText::script('VBO_GEN_CONTENT');
JText::script('VBO_AI_LABEL_DEF');
JText::script('VBO_AITOOL_WRITER_DEF_PROMPT');
JText::script('VBANNULLA');
}
/**
* Loads the necessary assets for the third-party visual editor (Quill).
*
* @return void
*
* @since 1.15.0 (J) - 1.5.0 (WP)
* @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
*/
public function loadVisualEditorAssets()
{
static $loaded = null;
if ($loaded) {
return;
}
$loaded = 1;
// access the document
$doc = JFactory::getDocument();
// load JS langs
$this->loadVisualEditorDefinitions();
// build the list of font families
$font_families = $this->getVisualEditorFonts();
$font_shortfam = $this->getVisualEditorFonts(true);
$js_font_names = json_encode($font_shortfam);
// build inline CSS styles
$css_font_decl = '';
foreach ($font_families as $k => $font_name) {
$font_val = $font_shortfam[$k];
$css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="' . $font_val . '"]::before,';
$css_font_decl .= '.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="' . $font_val . '"]::before {' . "\n";
$css_font_decl .= 'content: "' . $font_name . '";' . "\n";
$css_font_decl .= 'font-family: "' . $font_name . '";' . "\n";
$css_font_decl .= '}' . "\n";
}
$css_font_decl .= '
.ql-picker.ql-specialtags .ql-picker-label {
padding-right: 18px;
}
.ql-picker.ql-specialtags .ql-picker-label:before {
content: "' . htmlspecialchars(JText::translate('VBO_CONDTEXT_TKN')) . '";
}
.ql-formats .ql-genai {
width: auto !important;
font-weight: bold;
}
';
/**
* Cache the pre-configuration CSS onto a file to allow AJAX requests
* to properly load the asset inline within the response. Before this
* change the styles used to be added as an inline style declaration.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
* @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
*/
$cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
$cached_preconfig_css_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css']);
$cached_preconfig_css_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.css';
$cached_preconfig_css_ok = is_file($cached_preconfig_css_path);
if (!$cached_preconfig_css_ok) {
// attempt to create the file
$cached_preconfig_css_ok = JFile::write($cached_preconfig_css_path, $css_font_decl);
}
if ($cached_preconfig_css_ok) {
// load cached script file
$doc->addStyleSheet($cached_preconfig_css_uri);
} else {
// revert to append CSS style declaration to document
$doc->addStyleDeclaration($css_font_decl);
}
// append theme CSS to document
$doc->addStyleSheet(VBO_ADMIN_URI . 'resources/quill/quill.snow.css');
// append JS assets to document
$this->addScript(VBO_ADMIN_URI . 'resources/quill/quill.js');
$this->addScript(VBO_ADMIN_URI . 'resources/quill/quill-image-resize.min.js');
$this->addScript(VBO_ADMIN_URI . 'resources/quill/vik-content-builder.js');
// icon for mail wrapper
$mail_wrapper_icn = '<i class="' . VikBookingIcons::i('minus-square') . '" title="' . htmlspecialchars(JText::translate('VBO_INSERT_CONT_WRAPPER')) . '"></i>';
// icon for mail preview
$mail_preview_icn = '<i class="' . VikBookingIcons::i('eye') . '" title="' . htmlspecialchars(JText::translate('VBOPREVIEW')) . '"></i>';
// icon for property logo (home icon)
$mail_homelogo_icn = '<i class="' . VikBookingIcons::i('hotel') . '" title="' . htmlspecialchars(JText::translate('VBCONFIGFOURLOGO'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401) . '"></i>';
// text for generating content through AI
$mail_genai_icn = htmlspecialchars(JText::translate('VBO_AI_LABEL_DEF'), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
// build script pre-configuration string
$quill_preconfig_script =
<<<JS
// Quill pre-configuration
(function() {
// configure Quill to use inline styles rather than classes
var AlignClass = Quill.import('attributors/class/align');
Quill.register(AlignClass, true);
var BackgroundClass = Quill.import('attributors/class/background');
Quill.register(BackgroundClass, true);
var ColorClass = Quill.import('attributors/class/color');
Quill.register(ColorClass, true);
var FontClass = Quill.import('attributors/class/font');
Quill.register(FontClass, true);
var SizeClass = Quill.import('attributors/class/size');
Quill.register(SizeClass, true);
var AlignStyle = Quill.import('attributors/style/align');
Quill.register(AlignStyle, true);
var BackgroundStyle = Quill.import('attributors/style/background');
Quill.register(BackgroundStyle, true);
var ColorStyle = Quill.import('attributors/style/color');
Quill.register(ColorStyle, true);
var SizeStyle = Quill.import('attributors/style/size');
Quill.register(SizeStyle, true);
var FontStyle = Quill.import('attributors/style/font');
Quill.register(FontStyle, true);
// set additional fonts
var Font = Quill.import('formats/font');
Font.whitelist = $js_font_names;
Quill.register(Font, true);
// register custom Blot for special tags
var Inline = Quill.import('blots/inline');
class Specialtag extends Inline {
static create(value) {
var node = super.create(value);
if (value) {
node.setAttribute('class', value);
}
return node;
}
static formats(domNode) {
return domNode.getAttribute("class");
}
format(name, value) {
if (name !== this.statics.blotName || !value) {
return super.format(name, value);
}
if (value) {
this.domNode.setAttribute('class', value);
}
}
}
Specialtag.blotName = 'specialtag';
Specialtag.tagName = 'strong';
Quill.register(Specialtag);
// register bold tag names in the proper order to avoid conflicts
var Bold = Quill.import('formats/bold');
Bold.tagName = ['B', 'STRONG'];
Quill.register(Bold, true);
// register custom Blot for mail-wrapper
var BlockEmbed = Quill.import('blots/block/embed');
class MailWrapper extends BlockEmbed { }
MailWrapper.blotName = 'mailwrapper';
MailWrapper.className = 'vbo-editor-hl-mailwrapper';
MailWrapper.tagName = 'hr';
Quill.register(MailWrapper);
// register custom Blot for preview
class Preview extends Inline { }
Preview.blotName = 'preview';
Preview.tagName = 'span';
Quill.register(Preview);
// register custom icons for mail-wrapper and preview
var icons = Quill.import('ui/icons');
icons['mailwrapper'] = '$mail_wrapper_icn';
icons['preview'] = '$mail_preview_icn';
icons['homelogo'] = '$mail_homelogo_icn';
icons['genai'] = '$mail_genai_icn';
})();
JS
;
/**
* Cache the pre-configuration script onto a file to allow AJAX requests
* to properly load the script inline within the response. Before this
* change the script used to be added as an inline script declaration.
*
* @since 1.16.7 (J) - 1.6.7 (WP)
* @since 1.17.3 (J) - 1.7.3 (WP) cached file is related to software version.
*/
$cached_preconfig_suffix = defined('VIKBOOKING_SOFTWARE_VERSION') ? '-' . VIKBOOKING_SOFTWARE_VERSION : '';
$cached_preconfig_script_path = implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'quill', 'vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.js']);
$cached_preconfig_script_uri = VBO_ADMIN_URI . 'resources/quill/vik-quill-preconfig-cache' . $cached_preconfig_suffix . '.js';
$cached_preconfig_script_ok = is_file($cached_preconfig_script_path);
if (!$cached_preconfig_script_ok) {
// attempt to create the file
$cached_preconfig_script_ok = JFile::write($cached_preconfig_script_path, $quill_preconfig_script);
}
if ($cached_preconfig_script_ok) {
// load cached script file
$this->addScript($cached_preconfig_script_uri);
} else {
// revert to append JS script declaration to document
$doc->addScriptDeclaration($quill_preconfig_script);
}
/**
* Load Context Menu assets.
*
* @since 1.17.6 (J) - 1.7.6 (WP)
* @since 1.18.0 (J) - 1.8.0 (WP) loaded only if not during an AJAX request.
*/
if ((VBOPlatformDetection::isWordPress() && !wp_doing_ajax()) || (!VBOPlatformDetection::isWordPress() && strcasecmp((string) JFactory::getApplication()->input->server->get('HTTP_X_REQUESTED_WITH', ''), 'xmlhttprequest'))) {
$this->loadContextMenuAssets();
}
}
/**
* Renders a third-party visual editor (Quill).
*
* @param string $name the input name of the textarea field.
* @param string $value the current value of the textarea field/editor.
* @param array $attrs list of associative array attributes for the textarea.
* @param array $opts associative array of options for the editor.
* @param array $btns associative array of custom buttons for the editor (special tags).
*
* @return string the necessary HTML to render the visual editor.
*
* @since 1.15.0 (J) - 1.5.0 (WP)
* @since 1.17.3 (J) - 1.7.3 (WP) added support to Generative AI text functions.
*/
public function renderVisualEditor($name, $value, array $attrs = [], array $opts = [], array $btns = [])
{
if (empty($name)) {
return '';
}
// build the AJAX endpoints for uploading files, to preview the message and more
$upload_endpoint = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=upload_media_file');
$ajax_preview_mess = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.preview_visual_editor');
$ajax_logo_url = VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=mail.get_default_logo');
// the HTML to build
$editor = "\n";
// static editor counter
static $editor_counter = 0;
// increase counter for this instance
$editor_counter++;
// build id attributes for text and visual editors
$editor_id = 'vik-contentbuilder-editor-' . $editor_counter;
if (!isset($attrs['id'])) {
$attrs['id'] = 'vik-contentbuilder-tarea-' . $editor_counter;
}
// labels for modes and editor
$text_html_lbl = JText::translate('VBO_MODE_TEXTHTML');
$visual_mode_lbl = JText::translate('VBO_MODE_VISUAL');
// allowed modes to display the visual editor
$allowed_modes = [
'text' => $text_html_lbl,
'visual' => $visual_mode_lbl,
'modal-visual' => $visual_mode_lbl,
];
// define the default buttons to display for mode switching
$modes = [
'text' => $text_html_lbl,
'modal-visual' => $visual_mode_lbl,
];
// overwrite modes to display
if (isset($opts['modes']) && is_array($opts['modes']) && $opts['modes']) {
// check if the given array is associative, hence inclusive of texts
if (array_keys($opts['modes']) != range(0, (count($opts['modes']) - 1))) {
// replace modes
$modes = $opts['modes'];
} else {
// only the allowed keys must have been passed
$new_modes = [];
foreach ($opts['modes'] as $mode_type) {
if (!isset($allowed_modes[$mode_type])) {
continue;
}
$new_modes[$mode_type] = $allowed_modes[$mode_type];
}
$modes = $new_modes ?: $modes;
}
}
// resolve conflicts for both visual modes
if (($modes['visual'] ?? '') && ($modes['modal-visual'] ?? '') && $modes['visual'] == $modes['modal-visual']) {
$modes['modal-visual'] .= ' <i class="' . VikBookingIcons::i('window-restore') . '"></i>';
}
// ensure the text mode is set
if (!isset($modes['text'])) {
$modes['text'] = $allowed_modes['text'];
}
// overwrite default mode
if (isset($opts['def_mode']) && isset($modes[$opts['def_mode']]) && count($modes) > 1) {
$def_mode_val = $modes[$opts['def_mode']];
unset($modes[$opts['def_mode']]);
// sort modes accordingly
$modes = array_merge([$opts['def_mode'] => $def_mode_val], $modes);
// reset the array pointer
reset($modes);
}
// set default mode
$default_mode = key($modes);
// ensure text-area styles are merged or set when not the default mode
if ($default_mode != 'text') {
if (!($attrs['style'] ?? '')) {
// set style attribute to hide the text-area
$attrs['style'] = 'display: none;';
} else {
// append the style instruction to hide the text-area
$attrs['style'] .= ' display: none;';
}
}
// textarea attributes
$ta_attributes = [];
foreach ($attrs as $aname => $aval) {
if ($aname == 'name') {
// skip reserved attribute name
continue;
}
$ta_attributes[] = $aname . '="' . JHtml::fetch('esc_attr', $aval) . '"';
}
// visual editor JS options for list buttons
$js_editor_opts_list_btns = [
// ordered list (ol)
['list' => 'ordered'],
// un-ordered list (ul)
['list' => 'bullet'],
];
if ($opts['list_check'] ?? null) {
// push the un-ordered list (ul) with checked data-attribute, simulating a check-boxes list
$js_editor_opts_list_btns[] = ['list' => 'check'];
}
// build visual editor JS options
$js_editor_opts = [
'snippetsyntax' => true,
'modules' => [
'toolbar' => [
'container' => [
[
[
'font' => $this->getVisualEditorFonts(true)
],
],
[
[
'header' => [1, 2, 3, 4, 5, 6, false]
]
],
[
'bold',
'italic',
'underline',
'strike',
'blockquote',
],
[
['align' => []],
['indent' => '-1'],
['indent' => '+1'],
],
[
[
'color' => []
],
[
'background' => []
],
],
$js_editor_opts_list_btns,
[
'link',
'image',
'homelogo',
],
[
'mailwrapper',
'preview',
],
],
],
'imageResize' => [
'displaySize' => true,
],
],
'theme' => 'snow',
];
// check for Generative AI support through Vik Channel Manager and E4jConnect
if (class_exists('VikChannelManager') && defined('VikChannelManagerConfig::AI')) {
// add editor button for Gen-AI
$js_editor_opts['modules']['toolbar']['container'][] = ['genai'];
}
// build the list of special tags to be added to the editor
$special_tags_btns = [];
foreach ($btns as $tag_val) {
$special_tags_btns[] = $tag_val;
}
if ($special_tags_btns) {
// add custom buttons to the editor to manage special tags
$js_editor_opts['modules']['toolbar']['container'][] = [
['specialtags' => $special_tags_btns]
];
// append CSS inline styles
$editor .= '<style type="text/css">' . "\n";
foreach ($special_tags_btns as $tag_val) {
$editor .= '.ql-picker.ql-specialtags .ql-picker-item[data-value="' . $tag_val . '"]:before {
content: "' . $tag_val . '";
}' . "\n";
}
$editor .= '</style>' . "\n";
}
if ($opts['unset_buttons'] ?? null) {
// unset toolbar container main-level buttons
$opts['unset_buttons'] = (array) $opts['unset_buttons'];
foreach ($js_editor_opts['modules']['toolbar']['container'] as $tc_index => $tc_buttons) {
if (!is_array($tc_buttons)) {
continue;
}
foreach ($opts['unset_buttons'] as $unset_btn) {
if (!is_string($unset_btn)) {
continue;
}
foreach ($tc_buttons as $tc_btn_index => $tc_btn) {
if (is_string($tc_btn) && $tc_btn === $unset_btn) {
// unset toolbar container button
unset($js_editor_opts['modules']['toolbar']['container'][$tc_index][$tc_btn_index]);
}
}
}
if (!$js_editor_opts['modules']['toolbar']['container'][$tc_index]) {
// unset the whole container
unset($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
} else {
// restore numeric (non-associative) list
$js_editor_opts['modules']['toolbar']['container'][$tc_index] = array_values($js_editor_opts['modules']['toolbar']['container'][$tc_index]);
}
}
// restore numeric (non-associative) list
$js_editor_opts['modules']['toolbar']['container'] = array_values($js_editor_opts['modules']['toolbar']['container']);
}
// attept to pretty print a JSON encoded string for the editor options
$editor_opts_str = defined('JSON_PRETTY_PRINT') ? json_encode($js_editor_opts, JSON_PRETTY_PRINT) : json_encode($js_editor_opts);
// safe default value for editor (HTML tags should not be converted to entities)
$safe_value = preg_replace("/(<\/ ?textarea>)+/i", '', $value);
// the HTML to render the visual editor
$html_visual_editor = '<div class="vik-contentbuilder-editor-container" id="' . $editor_id . '"></div>';
// build the actual HTML content
$editor .= '<div class="vik-contentbuilder-wrapper">' . "\n";
if (count($modes) > 1) {
// display buttons to switch mode only if more than one mode available
$editor .= "\t" . '<div class="vik-contentbuilder-switcher"' . (($opts['hide_modes'] ?? null) ? ' style="display: none;"' : '') . '>' . "\n";
foreach ($modes as $key => $val) {
if (!isset($allowed_modes[$key])) {
continue;
}
$editor .= '<button type="button" class="btn vik-contentbuilder-switcher-btn' . ($default_mode == $key ? ' vik-contentbuilder-switcher-btn-active' : '') . '" data-switch="' . $key . '" onclick="VikContentBuilder.switchMode(this);">' . $val . '</button>';
}
$editor .= "\t" . '</div>' . "\n";
}
$editor .= "\t" . '<div class="vik-contentbuilder-inner">' . "\n";
foreach ($modes as $key => $val) {
if ($key == 'text') {
// if text is not the default mode, the text-area will be always hid through the style attribute
$editor .= "\t\t" . '<textarea name="' . $name . '" data-switch="text" ' . implode(' ', $ta_attributes) . '>' . $safe_value . '</textarea>' . "\n";
} elseif ($key == 'visual') {
$editor .= "\t\t" . '<div class="vik-contentbuilder-container" data-switch="visual" style="' . ($default_mode != 'visual' ? 'display: none;' : '') . '">' . "\n";
$editor .= "\t\t\t" . '<div class="vik-contentbuilder-editor-wrap">' . "\n";
$editor .= "\t\t\t\t" . $html_visual_editor . "\n";
$editor .= "\t\t\t" . '</div>' . "\n";
$editor .= "\t\t" . '</div>' . "\n";
} elseif ($key == 'modal-visual') {
$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";
$editor .= "\t\t\t" . '<div class="vik-contentbuilder-editor-wrap">' . "\n";
$editor .= "\t\t\t\t" . (!isset($modes['visual']) ? $html_visual_editor : '') . "\n";
$editor .= "\t\t\t" . '</div>' . "\n";
$editor .= "\t\t" . '</div>' . "\n";
}
}
$editor .= "\t" . '</div>' . "\n";
$editor .= '</div>' . "\n";
$editor .= "\n";
// default prompt for Gen-AI
$gen_ai_use_prompt = $opts['gen_ai']['prompt'] ?? null;
if (!$gen_ai_use_prompt && ($opts['gen_ai']['customer'] ?? [])) {
$guest_name = trim(($opts['gen_ai']['customer']['first_name'] ?? '') . ' ' . ($opts['gen_ai']['customer']['last_name'] ?? ''));
if ($guest_name) {
// set proper prompt message with the guest name
$gen_ai_use_prompt = JText::sprintf('VBO_AI_DISC_WRITER_FN_TEXT_GEN_MESS_EXA', $guest_name);
if (!empty($opts['gen_ai']['booking']['lang']) && JFactory::getLanguage()->getTag() != $opts['gen_ai']['booking']['lang']) {
// write the message in the guest language
$guest_lang = $opts['gen_ai']['booking']['lang'];
$known_langs = $this->getKnownLanguages();
if ($known_langs[$guest_lang]['nativeName'] ?? '') {
$guest_lang = $known_langs[$guest_lang]['nativeName'];
}
$gen_ai_use_prompt .= ' ' . JText::sprintf('VBO_AI_GEN_MESS_LANG', $guest_lang);
}
}
}
if (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'cron')) {
// use default prompt for cron messages
$gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_CRON_DEF_PROMPT');
} elseif (!$gen_ai_use_prompt && !strcasecmp(($opts['gen_ai']['environment'] ?? ''), 'taskmanager')) {
// use default prompt for the task manager
$gen_ai_use_prompt = JText::translate('VBO_AITOOL_WRITER_TM_DEF_PROMPT');
}
if ($gen_ai_use_prompt && ($opts['gen_ai']['placeholders'] ?? 0) && $btns) {
// add prompt text for using the placeholder tags
$placeholders = array_filter($btns, function($tag) {
// remove conditional text rule tags
return !preg_match('/^\{condition\:\s?.+$/i', $tag);
});
if ($placeholders) {
$gen_ai_use_prompt .= ' ' . JText::translate('VBO_AITOOL_WRITER_USE_PLACEHOLDERS') . "\n" . implode(', ', $placeholders);
}
}
// sanitize default prompt
$gen_ai_use_prompt = json_encode((string) $gen_ai_use_prompt);
// add JS script to HTML content
$toast_icon = VikBookingIcons::i('minus-square');
$envelope_icon = VikBookingIcons::i('envelope');
$booking_icon = VikBookingIcons::i('address-card');
$preview_lbl = htmlspecialchars(JText::translate('VBOPREVIEW'));
$booking_lbl = htmlspecialchars(JText::translate('VBDASHBOOKINGID'));
$editor .= <<<HTML
<script>
jQuery(function() {
const message_preview_fn = (content, bid) => {
let use_bid = bid || (typeof window['vbo_current_bid'] !== 'undefined' ? window['vbo_current_bid'] : null);
VBOCore.doAjax('$ajax_preview_mess', {
content: content,
bid: use_bid,
}, (resp) => {
var pop_win = window.open('', '', 'width=800, height=600, scrollbars=yes');
pop_win.document.body.innerHTML = resp[0];
}, (err) => {
console.log(err);
alert(err.responseText);
});
};
var vbo_toast_mailwrapper = null;
var visual_editor_handlers = {
specialtags: function(tag) {
if (tag) {
var cursorPosition = this.quill.getSelection().index;
this.quill.insertText(cursorPosition, tag, 'specialtag', 'vbo-editor-hl-specialtag');
cursorPosition += tag.length + 1;
this.quill.setSelection(cursorPosition, 'silent');
this.quill.insertText(cursorPosition, ' ');
this.quill.setSelection(cursorPosition + 1, 'silent');
this.quill.deleteText(cursorPosition - 1, 1);
}
},
image: function(clicked) {
var img_handler = new VikContentBuilderImageHandler(this.quill);
img_handler.setEndpoint('$upload_endpoint').present();
},
mailwrapper: function(clicked) {
var range = this.quill.getSelection(true);
this.quill.insertText(range.index, "\\n", 'user');
this.quill.insertEmbed(range.index + 1, 'mailwrapper', true, 'user');
this.quill.setSelection(range.index + 2, 'silent');
if (!vbo_toast_mailwrapper) {
vbo_toast_mailwrapper = 1;
VBOToast.enqueue(new VBOToastMessage({
title: Joomla.JText._('VBO_CONT_WRAPPER'),
body: Joomla.JText._('VBO_CONT_WRAPPER_HELP'),
icon: '$toast_icon',
delay: {
min: 6000,
max: 20000,
tolerance: 4000,
},
action: () => {
VBOToast.dispose(true);
}
}));
}
},
preview: function(clicked) {
let content = this.quill.root.innerHTML;
try {
let preview_btn = this.quill.container.closest('.vik-contentbuilder-editor-wrap').querySelector('button.ql-preview');
jQuery(preview_btn).vboContextMenu({
placement: 'bottom-right',
buttons: [
{
icon: '$envelope_icon',
text: '$preview_lbl',
separator: true,
action: (root, config) => {
message_preview_fn.call(clicked, content);
setTimeout(() => {
jQuery(preview_btn).vboContextMenu('destroy');
}, 500);
},
},
{
icon: '$booking_icon',
text: '$booking_lbl',
action: (root, config) => {
let bid = prompt('$preview_lbl - $booking_lbl');
message_preview_fn.call(clicked, content, bid);
setTimeout(() => {
jQuery(preview_btn).vboContextMenu('destroy');
}, 500);
},
},
],
});
jQuery(preview_btn).vboContextMenu('show');
} catch(e) {
// fallback on regular preview
console.error(e);
message_preview_fn.call(clicked, content);
}
},
homelogo: function(clicked) {
VBOCore.doAjax('$ajax_logo_url', {}, (resp) => {
try {
this.quill.insertEmbed(this.quill.getSelection().index, 'image', resp.url);
} catch(e) {
alert('Generic logo image error');
}
}, (err) => {
console.log(err);
alert(err.responseText);
});
},
genai: function(clicked) {
let visualEditor = this.quill;
let cursorPosition = visualEditor.getSelection().index;
const vboVisualEditorGenaiGetContentFn = (e) => {
// check if any data was sent within the event
if (e && e.detail?.content) {
// set the generated and picked content to editor
let ai_content = e.detail.content;
if ((e.detail?.type || '') == 'html') {
// convert HTML content into Delta for the Visual Editor
let delta = visualEditor.clipboard.convert(ai_content);
// set (replace) editor HTML content (2nd argument "source" should be "api" so that the "text-change" event will fire)
visualEditor.setContents(delta, 'api');
} else {
// default to plain text
visualEditor.insertText(cursorPosition, ai_content, 'user');
cursorPosition += ai_content.length + 1;
visualEditor.setSelection(cursorPosition, 'silent');
}
}
};
// register event to receive the Gen-AI content picked
document.addEventListener('vbo-ai-tools-writer-content-picked', vboVisualEditorGenaiGetContentFn);
// register listener to un-register the needed events
document.addEventListener('vbo-ai-tools-writer-content-dismissed', function vboVisualEditorGenaiDismissedFn(e) {
// un-register the events asynchronously to avoid unexpected behaviors
setTimeout(() => {
// make sure the same event will not trigger again
e.target.removeEventListener(e.type, vboVisualEditorGenaiDismissedFn);
// unregister the event for getting content data
document.removeEventListener('vbo-ai-tools-writer-content-picked', vboVisualEditorGenaiGetContentFn);
});
});
// default prompt
let writer_prompt = $gen_ai_use_prompt;
// render modal widget
VBOCore.handleDisplayWidgetNotification({
widget_id: 'aitools',
}, {
scope: 'writer',
prompt: {
message: (writer_prompt || Joomla.JText._('VBO_AITOOL_WRITER_DEF_PROMPT')),
submit: 0,
},
modal_options: {
suffix: 'vbo-ai-tools-writer-inner',
title: Joomla.JText._('VBO_GEN_CONTENT') + ' - ' + Joomla.JText._('VBO_AI_LABEL_DEF'),
lock_scroll: false,
enlargeable: false,
minimizeable: false,
dismiss_event: 'vbo-ai-tools-writer-content-picked',
dismissed_event: 'vbo-ai-tools-writer-content-dismissed',
},
});
}
};
var visual_editor_ext_opts = $editor_opts_str;
visual_editor_ext_opts['modules']['toolbar']['handlers'] = visual_editor_handlers;
var visual_editor = new Quill('#$editor_id', visual_editor_ext_opts);
var editor_content = jQuery('textarea#{$attrs['id']}').val();
if (editor_content && editor_content.length) {
if (editor_content.indexOf('<') >= 0) {
// replace special tags
editor_content = editor_content.replace(/([^"']|^)({(?:condition: ?)?[a-z0-9_]{5,64}})([^"']|$)/g, function(match, before, tag, after) {
return before + '<strong class="vbo-editor-hl-specialtag">' + tag + '</strong>' + after;
});
var editor_delta = visual_editor.clipboard.convert(editor_content);
// set editor HTML content
visual_editor.setContents(editor_delta, 'api');
} else {
// set text content
visual_editor.setText(editor_content, 'silent');
}
}
visual_editor.on('text-change', function(delta, source) {
jQuery('textarea#{$attrs['id']}').val(visual_editor.root.innerHTML);
});
jQuery('textarea#{$attrs['id']}').on('change', function() {
var editor_content = jQuery(this).val();
var editor_delta = visual_editor.clipboard.convert(editor_content);
visual_editor.setContents(editor_delta, 'silent');
});
try {
// push editor instance to the pool
VikContentBuilder.pushEditor(visual_editor);
} catch(e) {
console.error('Could not push new visual editor instance', e);
}
setTimeout(() => {
jQuery('.vik-contentbuilder-switcher-btn-active').trigger('click');
});
});
</script>
HTML;
// return the necessary HTML string to be displayed
return $editor;
}
/**
* Loads the necessary assets to render context menus.
*
* @return void
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
public function loadContextMenuAssets()
{
static $loaded = null;
if ($loaded) {
return;
}
$dark_mode = 'null';
if (JFactory::getApplication()->isClient('administrator')) {
// get appearance preference
$app_pref = VikBooking::getAppearancePref();
if ($app_pref == 'light') {
$dark_mode = 'false';
} elseif ($app_pref == 'dark') {
$dark_mode = 'true';
}
}
$this->addScript(VBO_ADMIN_URI . 'resources/contextmenu.js');
$doc = JFactory::getDocument();
$doc->addStyleSheet(VBO_ADMIN_URI . 'resources/contextmenu.css');
$doc->addScriptDeclaration(
<<<JS
(function($) {
'use strict';
$(function() {
$.vboContextMenu.defaults.darkMode = {$dark_mode};
$.vboContextMenu.defaults.class = 'vbo-dropdown-cxmenu';
});
})(jQuery);
JS
);
$loaded = 1;
}
/**
* Loads the assets necessary to render a phone input field.
*
* @return void
*
* @since 1.16.0 (J) - 1.6.0 (WP)
*/
public function loadPhoneInputFieldAssets()
{
static $loaded = null;
if ($loaded) {
return;
}
$loaded = 1;
$document = JFactory::getDocument();
$document->addStyleSheet(VBO_SITE_URI . 'resources/intlTelInput.css');
$document->addScript(VBO_SITE_URI . 'resources/intlTelInput.js');
}
}