Your IP : 216.73.216.190


Current Path : /proc/1908984/root/proc/2603263/cwd/
Upload File :
Current File : //proc/1908984/root/proc/2603263/cwd/Widgets.tar

Accordion.php000064400000021555152414664110007172 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\Registry\Registry;
use Joomla\CMS\HTML\HTMLHelper;

class Accordion extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * Accordion Settings
		 */

		/**
		 * The titles and contents of the accordion items.
		 * 
		 * Format:
		 * 
		 * [
		 *  	[
		 * 			'title' => 'Title 1',
		 * 			'content' => 'Content 1'
		 * 		],
		 *  	[
		 * 			'title' => 'Title 2',
		 * 			'content' => 'Content 2'
		 * 		],
		 * ]
		 */ 
		'value' => '',

		/**
		 * Choose how spacious or compact you'd like to list accordion.
		 * Consider this the padding of each accordion item.
		 * 
		 * Available values:
		 * - none
		 * - default
		 * - comfortable
		 * - compact
		 */
		'density' => 'default',

		// Set the font size.
		'font_size' => '16px',

		/**
		 * Set the gap between the items.
		 * 
		 * Available values:
		 * - none
		 * - small
		 * - large
		 */
		'gap' => 'none',

		// Set the background color of the panel.
		'panel_background_color' => '#fff',

		// Set the color of the text and the icon.
		'text_color' => '#333',

		// Set the color of the 1px border that affects both item and container. Set to 'none' for no border color.
		'border_color' => '#ddd',

		/**
		 * Set the rounded corners of the items.
		 * 
		 * Available values:
		 * - none
		 * - small
		 * - large
		 */
		'rounded_corners' => 'small',

		/**
		 * Item Icon Settings
		 */
		/**
		 * Set whether to display a toggle icon next to the title, or not.
		 * 
		 * Available values:
		 * - none
		 * - left
		 * - right
		 */
		'show_icon' => 'left',

		/**
		 * Set the icon URL.
		 */
		'icon' => '',

		/**
		 * Behavior
		 */
		/**
		 * Define the initial state of the accordion.
		 * By default all items are initially shown collapsed.
		 * 
		 * Available values:
		 * 
		 * - collapsed: All panels appear as collapsed.
		 * - expanded: All panels appear as expanded.
		 * - expanded-first: Expand the first panel only.
		 */
		'initial_state' => 'collapsed',

		// Set whether to allow only one panel to be expanded at a time.
		'only_one_panel_expanded' => false,
		
		// Set whether to generate the FAQ Schema on the page.
		'generate_faq' => false,

		// Custom Panel CSS Class
		'panel_css_class' => ''
	];

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();
	}

	/**
	 * Prepares the FAQ.
	 * 
	 * @return  void
	 */
	private function prepare()
	{
		$this->validateValue();
		
		$this->generateFAQ();
		
		// Set density
		switch ($this->options['density'])
		{
			case 'none':
				$this->options['density'] = 0;
				break;
			case 'default':
				$this->options['density'] = '1em 1.25em';
				break;
			case 'comfortable':
				$this->options['density'] = '1.25em 1.75em';
				break;
			case 'compact':
				$this->options['density'] = '.65em 1.25em';
				break;
		}
		
		// Set gap
		switch ($this->options['gap'])
		{
			case 'none':
				$this->options['gap'] = 0;
				break;
			case 'small':
				$this->options['gap'] = '.3em';
				break;
			case 'large':
				$this->options['gap'] = '.7em';
				break;
		}
		
		// Set rounded corners
		switch ($this->options['rounded_corners'])
		{
			case 'none':
				$this->options['rounded_corners'] = 0;
				break;
			case 'small':
				$this->options['rounded_corners'] = '.3em';
				break;
			case 'large':
				$this->options['rounded_corners'] = '.7em';
				break;
		}

		if ($this->options['only_one_panel_expanded'])
		{
			$this->options['css_class'] .= ' only-one-panel-expanded';
		}
		
		if ($this->options['load_css_vars'])
		{
			$this->options['custom_css'] = $this->getWidgetCSS();
		}
	}

	/**
	 * Validates the value.
	 * 
	 * @return  void
	 */
	private function validateValue()
	{
		if (!is_array($this->options['value']))
		{
			return;
		}

		$st = new \NRFramework\SmartTags\SmartTags();
		
		foreach ($this->options['value'] as $key => &$val)
		{
			if ((isset($val['title']) && empty($val['title'])) && (isset($val['content'])) && empty($val['content']))
			{
				unset($this->options['value'][$key]);
			}

			if ($this->options['pro'])
			{
				$val['title'] = HTMLHelper::_('content.prepare', $val['title']);
				$val['content'] = HTMLHelper::_('content.prepare', $val['content']);
			}
		}
	}

	/**
	 * Generates the FAQ.
	 * 
	 * @return  void
	 */
	private function generateFAQ()
	{
		// Ensure "generate_faq" is enabled
		if (!$this->options['generate_faq'])
		{
			return;
		}
		
		// Ensure we have value
		if (!is_array($this->options['value']) && !count($this->options['value']))
		{
			return;
		}

		// Abort if FAQ cannot be compiled
		if (!$faq = $this->getFAQ())
		{
			return;
		}

		// Hook into GSD to add the FAQ
		Factory::getApplication()->registerEvent('onGSDBeforeRender', function(&$data) use ($faq)
		{
			try
			{
				// get the data
				$tmpData = $data;
				if (defined('nrJ4'))
				{
					$tmpData = $data->getArgument('0');
				}

				// Append the FAQ Schema
				$tmpData[] = $faq;

				// Ensure unique FAQ
				$tmpData = array_unique($tmpData);
				
				// Set back the new value to $data object
				if (defined('nrJ4'))
				{
					$data->setArgument(0, $tmpData);
				}
				else
				{
					$data = $tmpData;
				}

			} catch (\Throwable $th)
			{
				$this->throwError($th->getMessage());
			}
		});
	}

	/**
	 * Returns the FAQ JSON/LD code.
	 * 
	 * @return  string
	 */
	private function getFAQ()
	{
		$autoload_file = JPATH_ADMINISTRATOR . '/components/com_gsd/autoload.php';
		if (!file_exists($autoload_file))
		{
			return;
		}

		require_once $autoload_file;

		$value = $this->options['value'];
		if (is_array($value))
		{
			foreach ($value as $key => &$val)
			{
				if (isset($val['title']))
				{
					$val['question'] = $val['title'];
					unset($val['title']);
				}

				if (isset($val['content']))
				{
					$val['answer'] = $val['content'];
					unset($val['content']);
				}
			}
		}
		
		// Prepare the FAQ
		$payload = [
			'mode' => 'manual',
			'faq_repeater_fields' => json_decode(json_encode($value))
		];
		$payload = new Registry($payload);
		$faq = new \GSD\Schemas\Schemas\FAQ($payload);

		// Get the JSON/LD code of the FAQ
		$json = new \GSD\Json($faq->get());

		// Return the code
		return $json->generate();
	}

	/**
	 * Returns the CSS for the widget.
	 * 
	 * @param   array  $exclude_breakpoints   Define breakpoints to exclude their CSS
	 * 
	 * @return  string
	 */
	public function getWidgetCSS($exclude_breakpoints = [])
	{
		$border_color = $this->options['border_color'] !== 'none' ? $this->options['border_color'] : 'transparent';

		$controls = [];
		
		// If no density is set, set the padding-top to 5px
		if (!$this->options['density'])
		{
			$controls[] = [
				'property' => '--content-padding-top',
				'value' => '5px'
			];
		}

		if (!$this->options['gap'])
		{
			$controls[] = [
				'property' => '--container-border-color',
				'value' => $border_color
			];

			$this->options['css_class'] .= ' no-gap';
		}
		
		$controls = array_merge($controls, [
			// CSS Variables
            [
                'property' => '--panel-background-color',
                'value' => $this->options['panel_background_color']
			],
            [
                'property' => '--text-color',
                'value' => $this->options['text_color']
			],
            [
                'property' => '--panel-border-color',
                'value' => $border_color
			],

			// CSS
            [
                'property' => '--padding',
				'type' => 'Spacing',
                'value' => $this->options['density']
			],
            [
                'property' => '--gap',
                'value' => $this->options['gap'],
				'unit' => 'px'
			],
            [
                'property' => '--rounded-corners',
				'type' => 'Spacing',
                'value' => $this->options['rounded_corners'],
				'unit' => 'px'
			],
            [
                'property' => '--font-size',
                'value' => $this->options['font_size'],
				'unit' => 'px'
			]
		]);

		$selector = '.tf-accordion-widget.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
        
		if (!$controlsCSS = $controlsInstance->generateCSS($controls))
		{
			return;
		}

		return $controlsCSS;
	}

	/**
	 * Returns all CSS files.
	 * 
	 * @return  array
	 */
	public static function getCSS()
	{
		return [
			'plg_system_nrframework/widgets/accordion.css'
		];
	}

	/**
	 * Returns all JS files.
	 * 
	 * @param   string  $theme
	 * 
	 * @return  array
	 */
	public static function getJS()
	{
		return [
			'plg_system_nrframework/widgets/accordion.js'
		];
	}
}BingMap.php000064400000001334152414664250006604 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;

class BingMap extends Map
{
	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		parent::loadMedia();
		
		HTMLHelper::script('plg_system_nrframework/widgets/bingmap.js', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('https://www.bing.com/api/maps/mapcontrol?callback=TFBingMapsCallback&key=' . $this->options['provider_key']);
	}
}ColorPicker.php000064400000001430152414664300007474 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

/**
 *  Color picker
 */
class ColorPicker extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// The default value of the widget. 
		'value' => '#dedede',

		// The input border color
		'input_border_color' => '#dedede',

		// The input border color on focus
		'input_border_color_focus' => '#dedede',

		// The input background color 
		'input_bg_color' => '#fff',

		// Input text color
		'input_text_color' => '#333'
	];
}Countdown.php000064400000032721152414664340007253 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;

/**
 * Countdown
 */
class Countdown extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * The Countdown type:
		 * 
		 * - static: Counts down to a specific date and time. Universal deadline for all visitors.
		 * - evergreen: Set-and-forget solution. The countdown starts when your visitor sees the offer.
		 */
		'countdown_type' => 'static',

		// The Static Countdown Date
		'value' => '',

		/**
		 * The timezone that will be used.
		 * 
		 * - server - Use server's timezone
		 * - client - Use client's timezone
		 */
		'timezone' => 'server',

		// Dynamic Days
		'dynamic_days' => 0,

		// Dynamic Hours
		'dynamic_hours' => 0,

		// Dynamic Minutes
		'dynamic_minutes' => 0,

		// Dynamic Seconds
		'dynamic_seconds' => 0,
		
		/**
		 * The countdown format.
		 * 
		 * Available tags:
		 * {years}
		 * {months}
		 * {days}
		 * {hours}
		 * {minutes}
		 * {seconds}
		 */
		'format' => '{days} days, {hours} hours, {minutes} minutes and {seconds} seconds',

		/**
		 * The countdown theme.
		 * 
		 * Available themes:
		 * default
		 * oneline
		 * custom
		 */
		'theme' => 'default',

		/**
		 * Set the action once countdown finishes.
		 * 
		 * Available values:
		 * keep 	- Keep the countdown visible
		 * hide 	- Hide the countdown
		 * restart 	- Restart the countdown
		 * message	- Show a message
		 * redirect	- Redirect to a URL
		 */
		'countdown_action' => 'keep',

		/**
		 * The message appearing after the countdown has finished.
		 * 
		 * Requires `countdown_action` to be set to `message`
		 * 
		 * Example: Countdown finished.
		 */
		'finish_text' => '',

		/**
		 * The redirect URL once the countdown expires.
		 * 
		 * Requires `countdown_action` to be set to `redirect`
		 */
		'redirect_url' => '',

		/**
		 * Widget Settings
		 */
		// Gap
		'gap' => 20,
		
		// Background Color
		'background_color' => '',

		/**
		 * Unit Display Settings
		 */
		// Whether to display Days
		'days' => true,

		// Days Label
		'days_label' => 'Days',
		
		// Whether to display Hours
		'hours' => true,

		// Hours Label
		'hours_label' => 'Hrs',
		
		// Whether to display Minutes
		'minutes' => true,

		// Minutes Label
		'minutes_label' => 'Mins',
		
		// Whether to display Seconds
		'seconds' => true,
		
		// Seconds Label
		'seconds_label' => 'Secs',
		
		// Whether to display a separator between the units
		'separator' => false,
		
		// Whether to display numbers in 00 or 0 format
		'double_zeroes_format' => true,

		/**
		 * Unit Item Settings
		 */
		// The size (width, height) of the unit item in pixels
		'item_size' => null,

		// Each item padding
		'item_padding' => null,
		
		// The unit item border width
		'item_border_width' => '',

		// The unit item border style
		'item_border_style' => '',

		// The unit item border color
		'item_border_color' => '',

		// The unit item border radius
		'item_border_radius' => null,

		// Item Background Color
		'item_background_color' => '',

		/**
		 * Unit Digits Container Settings
		 */
		// Digits wrapper Min Width
		'digits_wrapper_min_width' => 0,

		// The digits wrapper padding
		'digits_wrapper_padding' => null,

		// The digits wrapper border radius
		'digits_wrapper_border_radius' => null,

		// The digits wrapper background color.
		'digits_wrapper_background_color' => '',

		/**
		 * Unit Digit Settings
		 */
		// Digits Font Size
		'digits_font_size' => 25,

		// Digits Font Weight
		'digits_font_weight' => '400',

		// Digit Min Width
		'digit_min_width' => 0,

		// The digits padding
		'digits_padding' => null,

		// The digits border radius
		'digit_border_radius' => null,

		// Digits Gap
		'digits_gap' => null,

		// Digit Item Background Color. This applies for each of the 2 digits on a unit.
		'digit_background_color' => '',

		// Digit Item Text Color
		'digit_text_color' => '',

		/**
		 * Unit Label Settings
		 */
		// Label Font Size
		'label_font_size' => 13,

		// Label Font Weight
		'label_font_weight' => '400',

		// Unit Label Margin Top. The spacing between the unit and its label.
		'unit_label_margin_top' => 5,

		// Unit Label Color
		'unit_label_text_color' => '',

		// Extra attributes added to the widget
		'atts' => '',

		// TODO: Remove in the future
		'css_vars' => [],

		// Preview HTML used prior to JS initializing the Countdown
		'preview_html' => ''
	];

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		parent::__construct($options);

        Text::script('NR_AND_LC');

		$this->prepare();
		
		if ($this->options['load_css_vars'] && $this->options['theme'] !== 'custom')
		{
			$this->options['custom_css'] .= $this->getWidgetCSS();

			/**
			 * TODO: Remove in the future
			 * 
			 * For compatibility purposes for old customers using old
			 * ACF version used by ACF Previewer which is required to
			 * style the Countdown in the previewer.
			 */
			$this->options['css_vars'] = $this->options['custom_css'];
		}
	}

	/**
	 * Prepares the countdown.
	 * 
	 * @return  void
	 */
	private function prepare()
	{
		$this->options['css_class'] .= ' is-preview ' . $this->options['theme'];

		if (!empty($this->options['value']) && $this->options['value'] !== '0000-00-00 00:00:00')
		{
			if ($this->options['countdown_type'] === 'static' && $this->options['timezone'] === 'server')
			{
				// Get timezone
				$tz = new \DateTimeZone(Factory::getApplication()->getCfg('offset', 'UTC'));

				// Convert given date time to UTC
				$this->options['value'] = date_create($this->options['value'], $tz)->setTimezone(new \DateTimeZone('UTC'))->format('c');
				
				// Apply server timezone
				$this->options['value'] = (new \DateTime($this->options['value']))->setTimezone($tz)->format('c');
			}
		}

		$this->options['preview_html'] = $this->getPreviewHTML();

		// Set countdown payload
		$payload = [
			'data-countdown-type="' . $this->options['countdown_type'] . '"',
			'data-value="' . $this->options['value'] . '"',
			'data-timezone="' . $this->options['timezone'] . '"',
			'data-separator="' . (json_decode($this->options['separator']) ? 'true' : 'false') . '"',
			'data-double-zeroes-format="' . (json_decode($this->options['double_zeroes_format']) ? 'true' : 'false') . '"',
			'data-dynamic-days="' . $this->options['dynamic_days'] . '"',
			'data-dynamic-hours="' . $this->options['dynamic_hours'] . '"',
			'data-dynamic-minutes="' . $this->options['dynamic_minutes'] . '"',
			'data-dynamic-seconds="' . $this->options['dynamic_seconds'] . '"',
			'data-finish-text="' . htmlspecialchars($this->options['finish_text']) . '"',
			'data-redirect-url="' . $this->options['redirect_url'] . '"',
			'data-theme="' . $this->options['theme'] . '"',
			'data-countdown-action="' . $this->options['countdown_action'] . '"',
			'data-days="' . (json_decode($this->options['days']) ? 'true' : 'false') . '"',
			'data-days-label="' . $this->options['days_label'] . '"',
			'data-hours="' . (json_decode($this->options['hours']) ? 'true' : 'false') . '"',
			'data-hours-label="' . $this->options['hours_label'] . '"',
			'data-minutes="' . (json_decode($this->options['minutes']) ? 'true' : 'false') . '"',
			'data-minutes-label="' . $this->options['minutes_label'] . '"',
			'data-seconds="' . (json_decode($this->options['seconds']) ? 'true' : 'false') . '"',
			'data-seconds-label="' . $this->options['seconds_label'] . '"'
		];

		// Only set the format for custom-themed countdown instances
		if ($this->options['theme'] === 'custom')
		{
			$payload[] = 'data-format="' . htmlspecialchars($this->options['format']) . '"';
		}

		$this->options['atts'] = implode(' ', $payload);
	}

	/**
	 * Returns the CSS for the widget.
	 * 
	 * @param   array  $exclude_breakpoints   Define breakpoints to exclude their CSS
	 * 
	 * @return  string
	 */
	public function getWidgetCSS($exclude_breakpoints = [])
	{
		$controls = [
			// CSS Variables
            [
                'property' => '--digits-background-color',
                'value' => $this->options['digits_wrapper_background_color']
			],
            [
                'property' => '--background-color',
                'value' => $this->options['background_color']
			],
            [
                'property' => '--item-background-color',
                'value' => $this->options['item_background_color']
			],
            [
                'property' => '--unit-label-text-color',
                'value' => $this->options['unit_label_text_color']
			],
            [
                'property' => '--digit-background-color',
                'value' => $this->options['digit_background_color']
			],
            [
                'property' => '--digit-text-color',
                'value' => $this->options['digit_text_color']
			],
            [
                'property' => '--unit-label-margin-top',
                'value' => $this->options['unit_label_margin_top'],
				'unit' => 'px'
			],
            [
                'property' => '--digits-wrapper-min-width',
                'value' => $this->options['digits_wrapper_min_width'],
				'unit' => 'px'
			],
            [
                'property' => '--digit-min-width',
                'value' => $this->options['digit_min_width'],
				'unit' => 'px'
			],
            [
                'property' => '--digits-font-weight',
                'value' => $this->options['digits_font_weight']
			],
            [
                'property' => '--label-font-weight',
                'value' => $this->options['label_font_weight']
			],
            [
                'property' => '--item-border',
				'type' => 'Border',
                'value' => [
					'width' => $this->options['item_border_width'],
					'style' => $this->options['item_border_style'],
					'color' => $this->options['item_border_color'],
					'unit' => 'px'
				]
			],

			// CSS
            [
                'type' => 'Spacing',
                'property' => '--item-padding',
                'value' => $this->options['item_padding'],
				'unit' => 'px'
			],
            [
                'type' => 'Spacing',
                'property' => '--digits-padding',
                'value' => $this->options['digits_wrapper_padding'],
				'unit' => 'px'
			],
            [
                'property' => '--gap',
                'value' => $this->options['gap'],
				'unit' => 'px'
			],
            [
                'property' => '--digits-gap',
                'value' => $this->options['digits_gap'],
				'unit' => 'px'
			],
            [
                'property' => '--item-size',
                'value' => $this->options['item_size'],
				'unit' => 'px'
			],
            [
                'property' => '--digits-font-size',
                'value' => $this->options['digits_font_size'],
				'unit' => 'px'
			],
            [
                'property' => '--label-font-size',
                'value' => $this->options['label_font_size'],
				'unit' => 'px'
			],
            [
                'type' => 'Spacing',
                'property' => '--digit-padding',
                'value' => $this->options['digits_padding'],
				'unit' => 'px'
			],
            [
                'type' => 'Spacing',
                'property' => '--item-border-radius',
                'value' => $this->options['item_border_radius'],
				'unit' => 'px'
			],
            [
                'type' => 'Spacing',
                'property' => '--digits-border-radius',
                'value' => $this->options['digits_wrapper_border_radius'],
				'unit' => 'px'
			],
            [
                'type' => 'Spacing',
                'property' => '--digit-border-radius',
                'value' => $this->options['digit_border_radius'],
				'unit' => 'px'
			],
		];

		$selector = '.nrf-countdown.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
        
		if (!$controlsCSS = $controlsInstance->generateCSS($controls))
		{
			return;
		}

		return $controlsCSS;
	}

	/**
	 * Returns preview HTML.
	 * 
	 * @return  string
	 */
	private function getPreviewHTML()
	{
		if ($this->options['theme'] === 'custom')
		{
			return $this->options['format'];
		}

		$format_items = [
			'days' => $this->options['days'],
			'hours' => $this->options['hours'],
			'minutes' => $this->options['minutes'],
			'seconds' => $this->options['seconds']
		];

		$html = '';

		foreach ($format_items as $key => $value)
		{
			$labelStr = !empty($this->options[$key . '_label']) ? '<span class="countdown-digit-label">' . $this->options[$key . '_label'] . '</span>' : '';
			$html .= '<span class="countdown-item"><span class="countdown-digit ' . $key . '"><span class="digit-number digit-1">0</span><span class="digit-number digit-2">0</span></span>' . $labelStr . '</span>';
		}
		
		return $html;
	}

	/**
	 * Returns all CSS files.
	 * 
	 * @param   string  $theme
	 * 
	 * @return  array
	 */
	public static function getCSS($theme = 'default')
	{
		$css = [];
		
		if ($theme !== 'custom')
		{
			$css[] = 'plg_system_nrframework/widgets/countdown.css';
		}
		else
		{
			$css[] = 'plg_system_nrframework/widgets/widget.css';
		}

		return $css;
	}

	/**
	 * Returns all JS files.
	 * 
	 * @param   string  $theme
	 * 
	 * @return  array
	 */
	public static function getJS()
	{
		return [
			'plg_system_nrframework/widgets/countdown.js'
		];
	}
}Dailymotion.php000064400000005335152414664430007564 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;

class Dailymotion extends Video
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $video_widget_options = [
		// Start the video from X seconds
		'start' => null,

		// End the video at X seconds
		'end' => null,

		// Loop
		'loop' => false,

		// Mute
		'mute' => false,

		// Whether controls will appear in the video
		'controls' => false,

		/**
		 * Set the cover image type.
		 * 
		 * Allowed Values:
		 * - none
		 * - auto
		 * - custom
		 */
		'coverImageType' => 'none',

		// The Cover Image URL when coverImage="custom"
		'coverImage' => '',
	];


	/**
	 * Prepares the widget.
	 * 
	 * @return  void
	 */
	protected function prepare()
	{
		$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
		$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';
		
		// Abort
		if ($videoProvider !== 'dailymotion')
		{
			$this->options['value'] = null;
			return;
		}

		$this->options['css_class'] .= ' dailymotion';

		$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';

		if ($this->options['coverImageType'] === 'auto')
		{
			$this->options['coverImage'] = 'url("https://www.dailymotion.com/thumbnail/video/' . $videoID . '")';
		}
		else if ($this->options['coverImageType'] === 'custom' && !empty($this->options['coverImage']))
		{
			$coverImage = explode('#', $this->options['coverImage']);
			$this->options['coverImage'] = 'url("' . Uri::base() . reset($coverImage) . '")';
		}
		
		$atts = [
			'data-video-id="' . $videoID . '"',
			'data-video-type="' . $videoProvider . '"',
			'data-video-mute="' . var_export($this->options['mute'], true) . '"',
			'data-video-loop="' . var_export($this->options['loop'], true) . '"',
			'data-video-start="' . $this->options['start'] . '"',
			'data-video-end="' . $this->options['end'] . '"',
			'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
			'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
		];

		$this->options['atts'] = implode(' ', $atts);
	}

	/**
	 * We use the video widget layout file.
	 * 
	 * @return  string
	 */
	public function getName()
	{
		return 'video';
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function videoAssets()
	{
		HTMLHelper::script('plg_system_nrframework/widgets/video/dailymotion.js', ['relative' => true, 'version' => 'auto']);
	}
}FAQ.php000064400000017512152414664470005707 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\Registry\Registry;

class FAQ extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * FAQ Settings
		 */

		/**
		 * The questions and answers.
		 * 
		 * Format:
		 * 
		 * [
		 *  	[
		 * 			'question' => 'Question 1',
		 * 			'answer' => 'Answer 1'
		 * 		],
		 *  	[
		 * 			'question' => 'Question 2',
		 * 			'answer' => 'Answer 2'
		 * 		],
		 * ]
		 */ 
		'value' => '',

		/**
		 * Requires "show_toggle_icon" to be enabled to work.
		 * 
		 * Define the initial state of the FAQ.
		 * 
		 * Available values:
		 * 
		 * - first-open: Open the first question
		 * - all-open: Opens all questions
		 * - all-closed: Closes all questions
		 */
		'initial_state' => 'first-open',

		// Set whether to have one question open at a time
		'keep_one_question_open' => true,

		// Set the columns.
		'columns' => 1,

		// Set the gap between the items.
		'item_gap' => 16,

		// Set the gap between the columns.
		'column_gap' => 16,

		// Set whether to display a separator between items
		'separator' => false,

		// Set the separator color
		'separator_color' => '',

		/**
		 * Item Settings
		 */
		// Each item background color.
		'item_background_color' => null,

		// Each item border radius.
		'item_border_radius' => null,

		// Each item padding.
		'item_padding' => null,

		/**
		 * Question
		 */
		// Question font size
		'question_font_size' => null,

		// Each question text color.
		'question_text_color' => null,

		/**
		 * Answer
		 */
		// Answer font size
		'answer_font_size' => null,

		// Each answer text color.
		'answer_text_color' => null,

		/**
		 * Icon Settings
		 */
		/**
		 * Whether to show an icon that can toggle the open/close state of the answer.
		 * 
		 * If disabled, all answers will appear by default.
		 * If enabled, all answers will be hidden by default.
		 */
		'show_toggle_icon' => false,

		/**
		 * Set the icon that will be used.
		 * 
		 * Available values:
		 * - arrow
		 * - plus_minus
		 * - circle_arrow
		 * - circle_plus_minus
		 */
		'icon' => 'arrow',

		/**
		 * Set the icon position.
		 * 
		 * Available values:
		 * 
		 * - right
		 * - left
		 */
		'icon_position' => 'right',

		/**
		 * FAQ Schema
		 */
		// Set whether to generate the FAQ Schema on the page.
		'generate_faq' => false,

		// Custom Item CSS Classes
		'item_css_class' => ''
	];

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();
	}

	/**
	 * Prepares the FAQ.
	 * 
	 * @return  void
	 */
	private function prepare()
	{
		if ($this->options['show_toggle_icon'])
		{
			$this->options['show_toggle_icon'] = true;
			$this->options['css_class'] .= ' has-icons';
			$this->options['css_class'] .= ' position-icon-' . $this->options['icon_position'];
			$this->options['css_class'] .= ' has-icon-' . $this->options['icon'];
		}

		if (!empty($this->options['item_background_color']) && $this->options['item_background_color'] !== 'none')
		{
			$this->options['css_class'] .= ' has-item-bg-color';
		}

		if ($this->options['separator'])
		{
			$this->options['css_class'] .= ' has-separator';
		}

		$this->options['css_class'] .= ' ' . $this->options['initial_state'];

		if ($this->options['keep_one_question_open'])
		{
			$this->options['css_class'] .= ' keep-one-question-open';
		}

		if ((int) $this->options['columns'] > 1)
		{
			$this->options['css_class'] .= ' has-columns';
		}

		$this->generateFAQ();
		
		if ($this->options['load_css_vars'])
		{
			$this->options['custom_css'] = $this->getWidgetCSS();
		}
	}

	private function generateFAQ()
	{
		// Ensure "generate_faq" is enabled
		if (!$this->options['generate_faq'])
		{
			return;
		}
		
		// Ensure we have questions and answers
		if (!is_array($this->options['value']) && !count($this->options['value']))
		{
			return;
		}

		// Abort if FAQ cannot be compiled
		if (!$faq = $this->getFAQ())
		{
			return;
		}

		// Hook into GSD to add the FAQ
		Factory::getApplication()->registerEvent('onGSDBeforeRender', function(&$data) use ($faq)
		{
			try
			{
				// get the data
				$tmpData = $data;
				if (defined('nrJ4'))
				{
					$tmpData = $data->getArgument('0');
				}

				// Append the FAQ Schema
				$tmpData[] = $faq;

				// Ensure unique FAQ
				$tmpData = array_unique($tmpData);
				
				// Set back the new value to $data object
				if (defined('nrJ4'))
				{
					$data->setArgument(0, $tmpData);
				}
				else
				{
					$data = $tmpData;
				}

			} catch (\Throwable $th)
			{
				$this->throwError($th->getMessage());
			}
		});
	}

	/**
	 * Returns the FAQ JSON/LD code.
	 * 
	 * @return  string
	 */
	private function getFAQ()
	{
		$autoload_file = JPATH_ADMINISTRATOR . '/components/com_gsd/autoload.php';
		if (!file_exists($autoload_file))
		{
			return;
		}

		require_once $autoload_file;
		
		// Prepare the FAQ
		$payload = [
			'mode' => 'manual',
			'faq_repeater_fields' => json_decode(json_encode($this->options['value']))
		];
		$payload = new Registry($payload);
		$faq = new \GSD\Schemas\Schemas\FAQ($payload);

		// Get the JSON/LD code of the FAQ
		$json = new \GSD\Json($faq->get());

		// Return the code
		return $json->generate();
	}

	/**
	 * Returns the CSS for the widget.
	 * 
	 * @param   array  $exclude_breakpoints   Define breakpoints to exclude their CSS
	 * 
	 * @return  string
	 */
	public function getWidgetCSS($exclude_breakpoints = [])
	{
		$controls = [
			// CSS Variables
            [
                'property' => '--item-background-color',
                'value' => $this->options['item_background_color']
			],
            [
                'property' => '--question-text-color',
                'value' => $this->options['question_text_color']
			],
            [
                'property' => '--answer-text-color',
                'value' => $this->options['answer_text_color']
			],
            [
                'property' => '--separator-color',
                'value' => $this->options['separator_color']
			],

			// CSS
            [
                'property' => '--item-padding',
				'type' => 'Spacing',
                'value' => $this->options['item_padding'],
				'unit' => 'px'
			],
            [
                'property' => '--item-gap',
                'value' => $this->options['item_gap'],
				'unit' => 'px'
			],
            [
                'property' => '--column-gap',
                'value' => $this->options['column_gap'],
				'unit' => 'px'
			],
            [
                'property' => '--item-border-radius',
				'type' => 'Spacing',
                'value' => $this->options['item_border_radius'],
				'unit' => 'px'
			],
            [
                'property' => '--question-font-size',
                'value' => $this->options['question_font_size'],
				'unit' => 'px'
			],
            [
                'property' => '--answer-font-size',
                'value' => $this->options['answer_font_size'],
				'unit' => 'px'
			],
		];

		$selector = '.tf-faq-widget.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
        
		if (!$controlsCSS = $controlsInstance->generateCSS($controls))
		{
			return;
		}

		return $controlsCSS;
	}

	/**
	 * Returns all CSS files.
	 * 
	 * @return  array
	 */
	public static function getCSS()
	{
		return [
			'plg_system_nrframework/widgets/faq.css'
		];
	}

	/**
	 * Returns all JS files.
	 * 
	 * @param   string  $theme
	 * 
	 * @return  array
	 */
	public static function getJS()
	{
		return [
			'plg_system_nrframework/widgets/faq.js'
		];
	}
}FacebookVideo.php000064400000004173152414664530007774 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;

class FacebookVideo extends Video
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $video_widget_options = [
		// Whether we allow fullscreen
		'fs' => false,

		// Set to include the text from the Facebook post associated with the video, if any. Only available for desktop sites.
		'show_text' => false,

		// Set to show captions (if available) by default. Captions are only available on desktop.
		'show_captions' => false,
	];

	/**
	 * We use the video widget layout file.
	 * 
	 * @return  string
	 */
	public function getName()
	{
		return 'video';
	}

	protected function prepare()
	{
		$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
		$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';

		// Abort
		if ($videoProvider !== 'facebookvideo')
		{
			$this->options['value'] = null;
			return;
		}

		$this->options['css_class'] .= ' facebookvideo';

		$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';

		$atts = [
			'data-video-id="' . $videoID . '"',
			'data-video-type="' . $videoProvider . '"',
			'data-video-width="auto"',
			'data-video-show-text="' . var_export($this->options['show_text'], true) . '"',
			'data-video-show-captions="' . var_export($this->options['show_captions'], true) . '"',
			'data-video-fs="' . var_export($this->options['fs'], true) . '"',
			'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
			'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"'
		];

		$this->options['atts'] = implode(' ', $atts);
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function videoAssets()
	{
		HTMLHelper::script('plg_system_nrframework/widgets/video/facebookvideo.js', ['relative' => true, 'version' => 'auto']);
	}
}Gallery.php000064400000063164152414664570006704 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;
use \NRFramework\Helpers\Widgets\Gallery as GalleryHelper;
use NRFramework\Mimes;
use NRFramework\File;
use NRFramework\Image;

/**
 *  Gallery
 */
class Gallery extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * The gallery items source.
		 * 
		 * This can be one or combination of the following:
		 * 
		 * - Path to a relative folder (String)
		 * 		/path/to/folder
		 * - Path to a relative image (String)
		 * 		/path/to/folder/image.png
		 * - URL of an image (String)
		 * 		https://example.com/path/to/image.png
		 * - Array of images (Array)
		 * 		[
		 * 			'url' => 'https://example.com/path/to/image.png',
		 * 			'thumbnail_url' => 'https://example.com/path/to/image_thumb.png',
		 * 			'caption' => 'This is a caption',
		 * 			'thumbnail_size' => [
		 * 				'width' => '200',
		 * 				'height' => '200'
		 * 			],
		 * 			'module' => 'position-2'
		 * 		]
		 * 
		 * 		- The `url` property is required.
		 * 		- All other properties are optional.
		 */
		'items' => [],

		/**
		 * Set the ordering.
		 * 
		 * Available values:
		 * - default
		 * - alphabetical
		 * - reverse_alphabetical
		 * - random
		 */
		'ordering' => 'default',

		// Set the module key to display whenever we are viewing a single item's lightbox, appearing after the image
		'module' => '',

		/**
		 * Set the style of the gallery:
		 * 
		 * - masonry
		 * - grid
		 * - justified
		 */
		'style' => 'masonry',

		// Each item height (in pixels) in Justified layout
		'justified_item_height' => null,

		/**
		 * Define the columns per supported device.
		 * 
		 * Example value:
		 * - An integer representing the columns for all devices: 3
		 * - A value for each device:
		 * [
		 * 	'desktop' => 3,
		 * 	'tablet' => 2,
		 * 	'mobile' => 1
		 * ]
		 */
		'columns' => 4,

		/**
		 * Define the gap per gallery item per supported device.
		 * 
		 * Example value:
		 * - An integer representing the gap for all devices: 30
		 * - A value for each device:
		 * [
		 * 	'desktop' => 30,
		 * 	'tablet' => 20,
		 * 	'mobile' => 10
		 * ]
		 */
		'gap' => 15,

		/**
		 * Set the allowed file types.
		 * 
		 * This is used to validate the files loaded via a directory or a fixed path to an image.
		 * 
		 * Given URLs are not validated by this setting.
		 */
		'allowed_file_types' => '.jpg, .jpeg, .png',

		// Gallery Items wrapper CSS classes
		'gallery_items_css' => '',

		// Set whether to display a lightbox
		'lightbox' => false,

		/**
		 * Source Image
		 */
		/**
		 * Should the source image be resized?
		 * 
		 * If `original_image_resize` is false, then the source image will appear
		 * in the lightbox (also if `thumbnails` is false, the source image will also appear as the thumbnail)
		 * 
		 * Issue: if this image is a raw photo, there are chances it will increase the page load in order for the browser to display the image.
		 * 
		 * By enabling this, we resize the source image to our desired dimensions and reduce the page load in the above scenario.
		 * 
		 * Note: Always ensure the source image is backed up to a safe place.
		 * Note 2: We require thumbnails or original image resize to be enabled for this to work.
		 * 		 Reason: The above options if enabled generate the gallery_info.txt file in the /cache folder which helps us
		 * 				 generate the source images only if necessary(image has been edited), otherwise, the source image would
		 * 				 be generated on each page refresh.
		 */
		'source_image_resize' => false,

		// Source image resize width
		'source_image_resize_width' => 1920,
		
		// Source image resize height
		'source_image_resize_height' => null,

		// Source image resize method (crop, stretch, fit)
		'source_image_resize_method' => 'crop',

		// Source image resize quality
		'source_image_resize_image_quality' => 80,

		/**
		 * Original Image
		 */
		// Should the original uploaded image be resized?
		'original_image_resize' => false,

		// Resize method (crop, stretch, fit)
		'original_image_resize_method' => 'crop',

		/**
		 * Original Image Resize Width.
		 * 
		 * If `original_image_resize_height` is null, resizes via the width to keep the aspect ratio.
		 */
		'original_image_resize_width' => 1920,

		// Original Image Resize Height
		'original_image_resize_height' => null,

		// Original Image Resize Quality
		'original_image_resize_image_quality' => 80,

		/**
		 * Thumbnails
		 */
		// Set whether to generate thumbnails on-the-fly
		'thumbnails' => false,

		// Resize method (crop, stretch, fit)
		'thumb_resize_method' => 'crop',

		// Thumbnails width
		'thumb_width' => 300,

		// Thumbnails height
		'thumb_height' => null,

		// The CSS class of the thumbnail
		'thumb_class' => '',

		/**
		 * Set whether to resize the images whenever their source file changes.
		 * 
		 * i.e. If we edit the source image and also need to recreate the resized original image or thumbnail.
		 * This is rather useful otherwise we would have to delete the resized image or thumbnail in order for it to be recreated.
		 */
		'force_resizing' => false,

		// Destination folder
		'destination_folder' => 'cache/tassos/gallery',

		// Attributes set to the wrapper
		'atts' => '',

		// The unique hash of this gallery based on its options
		'hash' => null,

		// Set whether to show warnings when an image that has been set to appear does not exist.
		'show_warnings' => true,

		/**
		 * This is a list that's populated
		 * automatically by looking for tags
		 * in each gallery item.
		 */
		'tags' => [],

		/**
		 * Set the tags position.
		 * 
		 * Available values:
		 * - disabled (No tags will appear in the gallery)
		 * - above
		 * - below
		 */
		'tags_position' => 'disabled',

		/**
		 * Set the tags ordering.
		 * 
		 * Available values:
		 * - default
		 * - alphabetical
		 * - reverse_alphabetical
		 * - random
		 */
		'tags_ordering' => 'default',

		// Set the label of the "All Tags" option
		'all_tags_item_label' => 'All',

		/**
		 * Set whether to show the tags filter on mobile devices,
		 * show them as a dropdown or disable them.
		 * 
		 * Available values:
		 * - show
		 * - dropdown
		 * - disabled
		 */
		'tags_mobile' => 'show',

		'tags_text_color' => '#555',
		
		'tags_text_color_hover' => '#fff',

		'tags_bg_color_hover' => '#1E3148',

		// Widget Custom CSS
		'custom_css' => ''
	];

	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();
	}

	/**
	 * Prepares the Gallery.
	 * 
	 * @return  void
	 */
	private function prepare()
	{
		$this->options['hash'] = $this->getHash();

		$this->options['destination_folder'] = JPATH_ROOT . DIRECTORY_SEPARATOR . $this->options['destination_folder'] . DIRECTORY_SEPARATOR . $this->options['hash'] . DIRECTORY_SEPARATOR;

		$this->parseGalleryItems();

		$this->cleanDestinationFolder();
		
		$this->resizeSourceImages();
		$this->resizeOriginalImages();
		$this->createThumbnails();
		
		// Set style on the gallery items container.
		$this->options['gallery_items_css'] .= ' ' . $this->getStyle();

		// Set class to trigger lightbox.
		if ($this->options['lightbox'])
		{
			$this->options['css_class'] .= ' lightbox';
		}

		$this->setAtts();

		$this->prepareItems();

		$this->setOrdering();

		if ($this->options['load_css_vars'])
		{
			$this->options['custom_css'] = $this->getWidgetCSS();
		}

		$this->prepareTags();
	}

	/**
	 * Sets the data attributes.
	 * 
	 * @return  void
	 */
	private function setAtts()
	{
		$atts = [];

		$atts[] = 'data-id="' . $this->options['id'] . '"';

		if ($this->options['style'] === 'justified' && $this->options['justified_item_height'])
		{
			$atts[] = 'data-item-height="' . $this->options['justified_item_height'] . '"';
		}

		$this->options['atts'] = implode(' ', $atts);
	}

	/**
	 * Sets the ordering of the gallery.
	 * 
	 * @return  void
	 */
	private function setOrdering()
	{
		switch ($this->options['ordering']) {
			case 'random':
				shuffle($this->options['items']);
				break;
			case 'alphabetical':
				usort($this->options['items'], [$this, 'compareByThumbnailASC']);
				break;
			case 'reverse_alphabetical':
				usort($this->options['items'], [$this, 'compareByThumbnailDESC']);
				break;
		}
	}

	/**
	 * Compares tag names in ASC order
	 * 
	 * @param   array  $a
	 * @param   array  $b
	 * 
	 * @return  bool
	 */
	public function compareByTagNameASC($a, $b)
	{
		return strcmp($a, $b);
	}

	/**
	 * Compares tag names in DESC order
	 * 
	 * @param   array  $a
	 * @param   array  $b
	 * 
	 * @return  bool
	 */
	public function compareByTagNameDESC($a, $b)
	{
		return strcmp($b, $a);
	}

	/**
	 * Compares thumbnail file names in ASC order
	 * 
	 * @param   array  $a
	 * @param   array  $b
	 * 
	 * @return  bool
	 */
	public function compareByThumbnailASC($a, $b)
	{
		return strcmp(basename($a['thumbnail']), basename($b['thumbnail']));
	}

	/**
	 * Compares thumbnail file names in DESC order
	 * 
	 * @param   array  $a
	 * @param   array  $b
	 * 
	 * @return  bool
	 */
	public function compareByThumbnailDESC($a, $b)
	{
		return strcmp(basename($b['thumbnail']), basename($a['thumbnail']));
	}

	/**
	 * Get the hash of this gallery.
	 * 
	 * Generate the hash with only the essential options of the Gallery widget.
	 * i.e. with the data that are related to the images.
	 * 
	 * @return  string
	 */
	private function getHash()
	{
		$opts = [
			'items',
			'style',
			'allowed_file_types',
			'source_image_resize',
			'source_image_resize_width',
			'source_image_resize_height',
			'source_image_resize_method',
			'source_image_resize_image_quality',
			'original_image_resize',
			'original_image_resize_method',
			'original_image_resize_width',
			'original_image_resize_height',
			'original_image_resize_image_quality',
			'thumbnails',
			'thumb_resize_method',
			'thumb_width',
			'thumb_height',
			'force_resizing',
			'destination_folder'
		];

		$payload = [];
		
		foreach ($opts as $opt)
		{
			$payload[$opt] = $this->options[$opt];
		}
		
		return md5(serialize($payload));
	}

	/**
	 * Cleans the source folder.
	 * 
	 * If an image from the source folder is removed, we also remove the
	 * original image/thumbnail from the destination folder as well as
	 * from the gallery info file.
	 * 
	 * @return  void
	 */
	private function cleanDestinationFolder()
	{
		if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
		{
			return;
		}

		// Find all folders that we need to search
		$dirs_to_search = [];

		// Store all source files
		$source_files = [];

		foreach ($this->options['items'] as $key => $item)
		{
			if (!isset($item['path']))
			{
				continue;
			}

			$source_files[] = pathinfo($item['path'], PATHINFO_BASENAME);

			$directory = is_dir($item['path']) ? $item['path'] : dirname($item['path']);

			if (in_array($directory, $dirs_to_search))
			{
				continue;
			}
			
			$dirs_to_search[] = $directory;
		}

		if (empty($dirs_to_search))
		{
			return;
		}

		// Loop each directory found and check which files we need to delete
		foreach ($dirs_to_search as $dir)
		{
			$source_folder_info_file = GalleryHelper::getGalleryInfoFileData($dir);

			// Find all soon to be deleted files
			$to_be_deleted = array_diff(array_keys($source_folder_info_file), $source_files);

			if (!count($to_be_deleted))
			{
				continue;
			}
			
			foreach ($to_be_deleted as $source)
			{
				// Original image delete
				if (isset($source_folder_info_file[$source]))
				{
					$file = $this->options['destination_folder'] . $source_folder_info_file[$source]['filename'];
					if (file_exists($file))
					{
						unlink($file);
					}
				}
				
				// Thumbnail delete
				$parts = pathinfo($file);
				$thumbnail = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
				if (file_exists($thumbnail))
				{
					unlink($thumbnail);
				}

				// Also remove the image from the gallery info file.
				GalleryHelper::removeImageFromGalleryInfoFile(rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $source);
			}
		}
	}

	/**
	 * Returns the gallery style.
	 * 
	 * @return  string
	 */
	private function getStyle()
	{
		$style = $this->options['style'];

		if ($style === 'justified')
		{
			return $style;
		}

		// Get aspect ratio for source image, original image resized and thumbnail
		$thumb_height = intval($this->options['thumb_height']);
		$thumb_aspect_ratio = $thumb_height ? intval($this->options['thumb_width']) / $thumb_height : 0;
		
		$source_image_height = intval($this->options['source_image_resize_height']);
		$source_image_aspect_ratio = $source_image_height ? intval($this->options['source_image_resize_width']) / $source_image_height : 0;
		
		$original_image_height = intval($this->options['original_image_resize_height']);
		$original_image_aspect_ratio = $original_image_height ? intval($this->options['original_image_resize_width']) / $original_image_height : 0;

		// Check whether the aspect ratio for thumb and lightbox image are the same and use `masonry` style
		$checking_aspect_ratio = $this->options['original_image_resize'] ? $original_image_aspect_ratio : $source_image_aspect_ratio;
		if ($thumb_aspect_ratio && $checking_aspect_ratio && $thumb_aspect_ratio === $checking_aspect_ratio)
		{
			return 'masonry';
		}

		/**
		 * If both thumbnail width & height are equal we use the `grid` style.
		 */
		if ($this->options['thumb_width'] === $this->options['thumb_height'])
		{
			$style = 'grid';
		}
		
		/**
		 * If the style is grid and we do not have a null or 0 thumb_height set the fade lightbox CSS Class.
		 * 
		 * This CSS Class tells PhotoSwipe to use the fade transition.
		 */
		if ($style === 'grid' && (!is_null($this->options['thumb_height']) && $this->options['thumb_height'] !== '0'))
		{
			$this->options['css_class'] .= ' lightbox-fade';
		}
		
		return $style;
	}

	/**
	 * Prepare the tags.
	 * 
	 * @return  void
	 */
	private function prepareTags()
	{
		if ($this->options['tags_position'] === 'disabled')
		{
			return;
		}

		if (!is_array($this->options['items']))
		{
			return;
		}

		if ($this->options['all_tags_item_label'])
		{
			$this->options['all_tags_item_label'] = Text::_($this->options['all_tags_item_label']);
		}

		$tags = $this->options['tags'];

		if (count($tags) === 0)
		{
			foreach ($this->options['items'] as $key => &$item)
			{
				if (!isset($item['tags']))
				{
					continue;
				}
	
				if (!is_array($item['tags']))
				{
					continue;
				}
	
				$tags = array_merge($tags, $item['tags']);
			}
	
			$tags = array_unique($tags);
		}

		// Sort tags
		switch ($this->options['tags_ordering'])
		{
			case 'random':
				shuffle($tags);
				break;
			case 'alphabetical':
				usort($tags, [$this, 'compareByTagNameASC']);
				break;
			case 'reverse_alphabetical':
				usort($tags, [$this, 'compareByTagNameDESC']);
				break;
		}

		$this->options['tags'] = $tags;
	}

	/**
	 * Parses the gallery items by finding all iamges to display from all
	 * different sources.
	 * 
	 * @return  void
	 */
	private function parseGalleryItems()
	{
		// If it's a string, we assume its a path to a folder and we convert it to an array.
		$this->options['items'] = (array) $this->options['items'];

		$items = [];

		foreach ($this->options['items'] as $key => $value)
		{
			if (!$data = GalleryHelper::parseGalleryItems($value, $this->getAllowedFileTypes()))
			{
				continue;
			}

			$items = array_merge($items, $data);
		}

		// Ensure only unique image paths are used
		$items = array_unique($items, SORT_REGULAR);

		$this->options['items'] = $items;
	}

	/**
	 * Returns the allowed file types in an array format.
	 * 
	 * @return  array
	 */
	public function getAllowedFileTypes()
	{
		$types = explode(',', $this->options['allowed_file_types']);
		$types = array_filter(array_map('trim', array_map('strtolower', $types)));
		return $types;
	}

	/**
	 * Resizes the source images.
	 * 
	 * @return  mixed
	 */
	private function resizeSourceImages()
	{
		if (!$this->options['source_image_resize'])
		{
			return;
		}

		// We require either original image resize or thumbnails to be enabled
		if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
		{
			return;
		}

		foreach ($this->options['items'] as $key => &$item)
		{
			if (!isset($item['path']))
			{
				continue;
			}

			// Skip if source does not exist
			if (!is_file($item['path']))
			{
				continue;
			}

			$source = $item['path'];

			// Find source image in the destination folder
			if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
			{
				// If force resizing is disabled, continue
				if (!$this->options['force_resizing'])
				{
					continue;
				}
				else
				{
					// If the destination image has not been edited and exists, abort
					if (!$image_data['edited'] && file_exists($image_data['path']))
					{
						continue;
					}
				}
			}

			if (is_null($this->options['source_image_resize_height']))
			{
				Image::resizeAndKeepAspectRatio(
					$source,
					$this->options['source_image_resize_width'],
					$this->options['source_image_resize_image_quality']
				);
			}
			else
			{
				Image::resize(
					$source,
					$this->options['source_image_resize_width'],
					$this->options['source_image_resize_height'],
					$this->options['source_image_resize_image_quality'],
					$this->options['source_image_resize_method']
				);
			}
		}
	}

	/**
	 * Resizes the original images.
	 * 
	 * @return  mixed
	 */
	private function resizeOriginalImages()
	{
		if (!$this->options['original_image_resize'])
		{
			return;
		}

		// Create destination folder if missing
		File::createDirs($this->options['destination_folder']);
		
		foreach ($this->options['items'] as $key => &$item)
		{
			if (!isset($item['path']))
			{
				continue;
			}

			// Skip if source does not exist
			if (!is_file($item['path']))
			{
				continue;
			}

			$source = $item['path'];

			$unique = true;

			// Path to resized image in destination folder
			$destination = $this->options['destination_folder'] . basename($source);
			
			// Find source image in the destination folder
			if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
			{
				// If force resizing is disabled and the original image exists, set the URL of the destination image
				if (!$this->options['force_resizing'] && file_exists($image_data['path']))
				{
					$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
					continue;
				}
				else
				{
					// If the destination image has not been edited and exists, abort
					if (!$image_data['edited'] && file_exists($image_data['path']))
					{
						$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
						continue;
					}
					
					// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
					$unique = false;

					// The destination path is the same resized image
					$destination = $image_data['path'];
				}
			}

			$original_image_file = is_null($this->options['original_image_resize_height'])
				?
				Image::resizeAndKeepAspectRatio(
					$source,
					$this->options['original_image_resize_width'],
					$this->options['original_image_resize_image_quality'],
					$destination,
					$unique
				)
				:
				Image::resize(
					$source,
					$this->options['original_image_resize_width'],
					$this->options['original_image_resize_height'],
					$this->options['original_image_resize_image_quality'],
					$this->options['original_image_resize_method'],
					$destination,
					$unique
				);

			if (!$original_image_file)
			{
				continue;
			}

			// Set image URL
			$item = array_merge($item, [
				'url' => GalleryHelper::directoryImageToURL($original_image_file)
			]);

			// Update image data in Gallery Info File
			GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
		}
	}

	/**
	 * Creates thumbnails.
	 * 
	 * If `force_resizing` is enabled, it will re-generate thumbnails under the following cases:
	 * 
	 * - If a thumbnail does not exist.
	 * - If the original image has been edited.
	 * 
	 * @return  mixed
	 */
	private function createThumbnails()
	{
		if (!$this->options['thumbnails'])
		{
			return false;
		}
		
		// Create destination folder if missing
		File::createDirs($this->options['destination_folder']);

		foreach ($this->options['items'] as $key => &$item)
		{
			// Skip items that do not have a path set
			if (!isset($item['path']))
			{
				continue;
			}

			// Skip if source does not exist
			if (!is_file($item['path']))
			{
				continue;
			}

			$source = $item['path'];

			$unique = true;

			$parts = pathinfo($source);
			$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
			
			// Find source image in the destination folder
			if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
			{
				/**
				 * Use the found original image path to produce the thumb file path.
				 * 
				 * This is used as we have multiple files with the same which produce file names of _copy_X
				 * and thus the above $destination will not be valid. Instead, we use the original file name
				 * to find the thumbnail file.
				 */
				if ($this->options['original_image_resize'])
				{
					$parts = pathinfo($image_data['path']);
					$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
				}
				
				// If force resizing is disabled and the thumbnail exists, set the URL of the destination image
				if (!$this->options['force_resizing'] && file_exists($destination))
				{
					$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
					continue;
				}
				else
				{
					// If the destination image has not been edited and exists, abort
					if (!$image_data['edited'] && file_exists($destination))
					{
						$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
						continue;
					}
					
					// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
					$unique = false;
				}
			}

			// Generate thumbnails
			$thumb_file = is_null($this->options['thumb_height'])
				?
				Image::resizeAndKeepAspectRatio(
					$source,
					$this->options['thumb_width'],
					100,
					$destination,
					$unique,
					true,
					'resize'
				)
				:
				Image::resize(
					$source,
					$this->options['thumb_width'],
					$this->options['thumb_height'],
					100,
					$this->options['thumb_resize_method'],
					$destination,
					$unique,
					true,
					'resize'
				);

			if (!$thumb_file)
			{
				continue;
			}

			// Set image thumbnail URL
			$item = array_merge($item, [
				'thumbnail_url' => GalleryHelper::directoryImageToURL($thumb_file)
			]);

			// Update image data in Gallery Info File
			GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
		}
	}
	
	/**
	 * Prepares the items.
	 * 
	 * - Sets the thumbnails image dimensions.
	 * - Assures caption property exist.
	 * 
	 * @return  mixed
	 */
	private function prepareItems()
	{
		if (!is_array($this->options['items']) || !count($this->options['items']))
		{
			return;
		}
		
		$smartTagsInstance = \NRFramework\SmartTags::getInstance();

		foreach ($this->options['items'] as $key => &$item)
		{
			// Initialize image atts
			$item['img_atts'] = '';

			// Initializes caption if none given
			if (!isset($item['caption']))
			{
				$item['caption'] = '';
			}

			if (!isset($item['alt']) || empty($item['alt']))
			{
				$item['alt'] = !empty($item['caption']) ? mb_substr($item['caption'], 0, 100) : pathinfo($item['url'], PATHINFO_FILENAME);
			}

			// Replace Smart Tags in alt
			$item['alt'] = $smartTagsInstance->replace($item['alt']);

			if ($item['caption'])
			{
				$item['caption'] = $smartTagsInstance->replace($item['caption']);
			}
			
			// Ensure a thumbnail is given
			if (!isset($item['thumbnail_url']))
			{
				// If no thumbnail is given, set it to the full image
				$item['thumbnail_url'] = $item['url'];
				continue;
			}

			// If the thumbnail size for this item is given, set the image attributes
			if (isset($item['thumbnail_size']))
			{
				$item['img_atts'] = 'width="' . $item['thumbnail_size']['width'] . '" height="' . $item['thumbnail_size']['height'] . '"';
				continue;
			}
		}
	}


	/**
	 * Returns the CSS for the widget.
	 * 
	 * @param   array  $exclude_breakpoints   Define breakpoints to exclude their CSS
	 * 
	 * @return  string
	 */
	public function getWidgetCSS($exclude_breakpoints = [])
	{
		$controls = [
            [
                'property' => '--gap',
                'value' => $this->options['gap'],
				'unit' => 'px'
			],
			[
				'property' => '--tags-text-color',
				'value' => $this->options['tags_text_color']
			],
			[
				'property' => '--tags-text-color-hover',
				'value' => $this->options['tags_text_color_hover']
			],
			[
				'property' => '--tags-bg-color-hover',
				'value' => $this->options['tags_bg_color_hover']
			]
		];

		if ($this->options['style'] !== 'justified')
		{
			$controls[] = [
                'property' => [
                    '--columns' => '%value_raw%',
					'--display-items' => 'grid',
					'--image-width' => '100%'
                ],
				'fallback_value' => [
					'--display-items' => 'flex',
					'--display-items-flex-wrap' => 'wrap',
					'--image-width' => 'auto'
				],
				'value' => $this->options['columns'],
			];
		}

		$selector = '.nrf-widget.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
        
		if (!$controlsCSS = $controlsInstance->generateCSS($controls))
		{
			return;
		}

		return $controlsCSS;
	}
}GalleryManager.php000064400000043434152414664730010173 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\Registry\Registry;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\Factory;
use NRFramework\Helpers\Widgets\GalleryManager as GalleryManagerHelper;
use NRFramework\Image;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File;

/**
 *  Gallery Manager
 */
class GalleryManager extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// The input name
		'name' => '',

		// Context of the field
		// module, default
		'context' => 'default',

		// The field ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
		'field_id' => null,

		// The item ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
		'item_id' => null,

		/**
		 * Max file size in MB.
		 * 
		 * Defults to 0 (no limit).
		 */
		'max_file_size' => 0,

		/**
		 * How many files we can upload.
		 * 
		 * Defaults to 0 (no limit).
		 */
		'limit_files' => 0,

		// Allowed upload file types
		'allowed_file_types' => '.jpg, .jpeg, .png, .gif, .webp, image/webp',

		/**
		 * Original Image
		 */
		// Original image resize width
		'original_image_resize_width' => null,

		// Original image resize height
		'original_image_resize_height' => null,

		/**
		 * Thumbnails
		 */
		// Thumbnails width
		'thumb_width' => null,

		// Thumbnails height
		'thumb_height' => null,

		// Thumbnails resize method (crop, stretch, fit)
		'thumb_resize_method' => 'crop',

		// The list of tags already available for this gallery
		'tags' => [],

		// The widget name
		'widget' => 'GalleryManager'
	];

	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();

		// Load translation strings
        Text::script('NR_GALLERY_MANAGER_CONFIRM_REGENERATE_IMAGES');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
        Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
        Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
	}

	private function prepare()
	{
		// Set gallery items
		$this->options['gallery_items'] = is_array($this->options['value']) ? $this->options['value'] : [];

		// Set css class for readonly state
		if ($this->options['readonly'])
		{
			$this->options['css_class'] .= ' readonly';
		}

		// Adds a css class when the gallery contains at least one item
		if (count($this->options['gallery_items']))
		{
			$this->options['css_class'] .= ' dz-has-items';
		}

		// Load translation strings
        Text::script('NR_GALLERY_MANAGER_CONFIRM_REGENERATE_IMAGES');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
        Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
        Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
		$this->prepareTags();
	}

	private function prepareTags()
	{
		if (!is_array($this->options['gallery_items']))
		{
			return;
		}

		$db    = Factory::getDbo();
		$query = $db->getQuery(true)
			->select([$db->quoteName('id'), $db->quoteName('title')])
			->from($db->quoteName('#__tags'))
			->where($db->quoteName('published') . ' = 1')
			->where($db->quoteName('level') . ' > 0');

		$db->setQuery($query);
		$tags = $db->loadAssocList('id', 'title');

		$this->options['tags'] = $tags;
	}

	private function getSettings($context)
	{
		// Make sure we have a valid context
		if (!$context)
		{
			return false;
		}

		$field_data = [];

		$input = Factory::getApplication()->input;

		if ($context === 'default')
		{
			// Make sure we have a valid field id
			if (!$field_id = $input->getInt('field_id'))
			{
				$this->exitWithMessage('NR_GALLERY_MANAGER_FIELD_ID_ERROR');
			}

			if (!$field_data = \NRFramework\Helpers\CustomField::getData($field_id))
			{
				$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
			}
		}
		else if ($context === 'module')
		{
			// Make sure we have a valid item id
			if (!$item_id = $input->getInt('item_id'))
			{
				$this->exitWithMessage('NR_GALLERY_MANAGER_ITEM_ID_ERROR');
			}
			
			if (!$field_data = \NRFramework\Helpers\Module::getData($item_id))
			{
				$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
			}

			$field_data->set('style', $field_data->get('provider', 'grid'));
		}

		return $field_data;
	}

	/**
	 * The upload task called by the AJAX hanler
	 *
	 * @return  void
	 */
	protected function ajax_upload()
	{
		$input = Factory::getApplication()->input;

		// Make sure we have a valid context
		if (!$context = $input->get('context'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
		}

		// Make sure we have a valid file passed
		if (!$file = $input->files->get('file'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
		}

		if (!$field_data = $this->getSettings($context))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
		}

		// get the media uploader file data, values are passed when we upload a file using the Media Uploader
		$media_uploader_file_data = [
			'is_media_uploader_file' => $input->get('media_uploader', false) == '1',
			'media_uploader_filename' => $input->getString('media_uploader_filename', '')
		];

		// In case we allow multiple uploads the file parameter is a 2 levels array.
		$first_property = array_pop($file);
		if (is_array($first_property))
		{
			$file = $first_property;
		}

		$style = $field_data->get('style', 'grid');

		$uploadSettings = [
			'allow_unsafe' => false,
			'allowed_types' => $field_data->get('allowed_file_types', $this->widget_options['allowed_file_types']),
			'style' => $style
		];

		// Add watermark
		if ($field_data->get('watermark.type', 'disabled') !== 'disabled')
		{
			$uploadSettings['watermark'] = (array) $field_data->get('watermark', []);
			$uploadSettings['watermark']['image'] = !empty($uploadSettings['watermark']['image']) ? explode('#', JPATH_SITE . DIRECTORY_SEPARATOR . $uploadSettings['watermark']['image'])[0] : null;
			$uploadSettings['watermark']['apply_on_thumbnails'] = $field_data->get('watermark.apply_on_thumbnails', false) === '1';
		}

		$field_data_array = $field_data->toArray();

		$resize_method = $field_data->get('resize_method', 'crop');
		$thumb_height = $field_data->get('thumb_height', null);

		switch ($style)
		{
			case 'slideshow':
				if (isset($field_data_array['slideshow_thumb_height']))
				{
					$thumb_height = $field_data_array['slideshow_thumb_height'];
				}

				if ($slideshow_resize_method = $field_data->get('slideshow_resize_method'))
				{
					$resize_method = $slideshow_resize_method;
				}
				break;
			case 'masonry':
				$thumb_height = null;
				break;
			case 'zjustified':
			case 'justified':
				$thumb_height = $field_data->get('justified_item_height', 200);
				break;
		}

		// resize image settings
		$resizeSettings = [
			'thumb_height' => $thumb_height,
			'thumb_resize_method' => $resize_method,
			
			// TODO: Remove this line when ACF is also updated, so we don't rely on this to resize the original image
			'original_image_resize' => false,

			'original_image_resize_width' => $field_data->get('original_image_resize_width'),
			'original_image_resize_height' => $field_data->get('original_image_resize_height')
		];

		/**
		 * For backwards compatibility.
		 * 
		 * TODO: Update this code block to not rely on "original_image_resize" to resize original image when removed from ACF.
		 */
		$resize_original_image_setting_value = $field_data->get('original_image_resize', null);
		if ($style === 'slideshow' && ($resizeSettings['original_image_resize_width'] || $resizeSettings['original_image_resize_height']))
		{
			$resize_original_image_setting_value = true;
		}

		if ($resize_original_image_setting_value)
		{
			$resizeSettings['original_image_resize_height'] = $style === 'slideshow' ? $resizeSettings['original_image_resize_height'] : null;
			$resizeSettings['original_image_resize'] = $style === 'slideshow' ? true : $resize_original_image_setting_value;
		}
		else if (is_null($resize_original_image_setting_value) && ($resizeSettings['original_image_resize_width'] || $resizeSettings['original_image_resize_height']))
		{
			$resizeSettings['original_image_resize'] = true;
		}
		if (!$resizeSettings['original_image_resize'])
		{
			$resizeSettings['original_image_resize_width'] = null;
			$resizeSettings['original_image_resize_height'] = null;
		}
		
		if (in_array($style, ['grid', 'masonry', 'slideshow']))
		{
			$resizeSettings['thumb_width'] = $field_data->get('thumb_width');
			
			$slideshow_thumb_width = $field_data->get('slideshow_thumb_width');
			if (!is_null($slideshow_thumb_width) && $style === 'slideshow')
			{
				$resizeSettings['thumb_width'] = $slideshow_thumb_width;
			}
		}

		// Upload the file and resize the images as required
		if (!$uploaded_filenames = GalleryManagerHelper::upload($file, $uploadSettings, $media_uploader_file_data, $resizeSettings))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_CANNOT_UPLOAD_FILE');
		}

		echo json_encode([
			'source' => $uploaded_filenames['source'],
			'original' => $uploaded_filenames['original'],
			'thumbnail' => $uploaded_filenames['thumbnail'],
			'is_media_uploader_file' => $media_uploader_file_data['is_media_uploader_file']
		]);
	}

	/**
	 * The delete task called by the AJAX hanlder
	 *
	 * @return void
	 */
	protected function ajax_delete()
	{
		$input = Factory::getApplication()->input;

		// Get source image path.
		$source = $input->getString('source');

		// Make sure we have a valid file passed
		if (!$original = $input->getString('original'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
		}

		// Make sure we have a valid file passed
		if (!$thumbnail = $input->getString('thumbnail'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
		}

		if (!$context = $input->get('context'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
		}

		if (!$field_data = $this->getSettings($context))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
		}

		// Delete the source, original, and thumbnail file
		$deleted = GalleryManagerHelper::deleteFile($source, $original, $thumbnail);
		
		echo json_encode(['success' => $deleted]);
	}

	/**
	 * This task allows us to regenerate the images.
	 *
	 * @return void
	 */
	protected function ajax_regenerate_images()
	{
		$input = Factory::getApplication()->input;

		// Make sure we have a valid context
		if (!$context = $input->get('context'))
		{
			echo json_encode(['success' => false, 'message' => Text::_('NR_GALLERY_MANAGER_CONTEXT_ERROR')]);
			die();
		}

		if (!$field_data = $this->getSettings($context))
		{
			echo json_encode(['success' => false, 'message' => Text::_('NR_GALLERY_MANAGER_INVALID_FIELD_DATA')]);
			die();
		}

		$field_id = $input->getInt('field_id');
		$item_id = $input->getInt('item_id');

		$field_data_array = $field_data->toArray();

		$style = $field_data->get('style', 'grid');
		
		$resize_method = $field_data->get('resize_method', 'crop');
		$thumb_height = $field_data->get('thumb_height', null);

		switch ($style)
		{
			case 'slideshow':
				if (isset($field_data_array['slideshow_thumb_height']))
				{
					$thumb_height = $field_data_array['slideshow_thumb_height'];
				}

				if ($slideshow_resize_method = $field_data->get('slideshow_resize_method'))
				{
					$resize_method = $slideshow_resize_method;
				}
				break;
			case 'masonry':
				$thumb_height = null;
				break;
			case 'zjustified':
			case 'justified':
				$thumb_height = $field_data->get('justified_item_height', 200);
				break;
		}

		$resizeSettings = [
			'thumb_height' => $thumb_height,
			'thumb_resize_method' => $resize_method
		];

		if (in_array($style, ['grid', 'masonry', 'slideshow']))
		{
			$resizeSettings['thumb_width'] = $field_data->get('thumb_width');
			
			$slideshow_thumb_width = $field_data->get('slideshow_thumb_width');
			if (!is_null($slideshow_thumb_width) && $style === 'slideshow')
			{
				$resizeSettings['thumb_width'] = $slideshow_thumb_width;
			}
		}

		// TODO: Remove this line when ACF is also updated, so we don't rely on this to resize the original image
		$original_image_resize = false;

		$original_image_resize_width = $field_data->get('original_image_resize_width');
		$original_image_resize_height = $field_data->get('original_image_resize_height');

		/**
		 * For backwards compatibility.
		 * 
		 * TODO: Update this code block to not rely on "original_image_resize" to resize original image when removed from ACF.
		 */
		$resize_original_image_setting_value = $field_data->get('original_image_resize', null);
		if ($style === 'slideshow' && ($original_image_resize_width || $original_image_resize_height))
		{
			$resize_original_image_setting_value = true;
		}

		if ($resize_original_image_setting_value)
		{
			$original_image_resize_height = $style === 'slideshow' ? $original_image_resize_height : null;
			$original_image_resize = $style === 'slideshow' ? true : $resize_original_image_setting_value;
		}
		else if (is_null($resize_original_image_setting_value) && ($original_image_resize_width || $original_image_resize_height))
		{
			$original_image_resize = true;
		}
		if (!$original_image_resize)
		{
			$original_image_resize_width = null;
			$original_image_resize_height = null;
		}

		$watermarkSettings = [];
		// Add watermark
		if ($field_data->get('watermark.type', 'disabled') !== 'disabled')
		{
			$watermarkSettings = (array) $field_data->get('watermark', []);
			$watermarkSettings['image'] = !empty($watermarkSettings['image']) ? explode('#', JPATH_SITE . DIRECTORY_SEPARATOR . $watermarkSettings['image'])[0] : null;
			$watermarkSettings['apply_on_thumbnails'] = $field_data->get('watermark.apply_on_thumbnails', false) === '1';
		}
		$watermarkEnabled = isset($watermarkSettings['type']) && $watermarkSettings['type'] !== 'disabled';
		$thumbnailWatermarkEnabled = isset($watermarkSettings['type']) && $watermarkSettings['type'] !== 'disabled' && $watermarkSettings['apply_on_thumbnails'];

		$items = $input->get('items', null, 'ARRAY');
		$items = json_decode($items[0], true);

		$ds = DIRECTORY_SEPARATOR;

		// Parse all images
		if (is_array($items) && count($items))
		{
			foreach ($items as &$item)
			{
				$sourceImage = isset($item['source']) ? $item['source'] : '';
				$originalImage = isset($item['original']) ? $item['original'] : '';
				$thumbnailImage = isset($item['thumbnail']) ? $item['thumbnail'] : '';
				$thumbnailImagePath = implode($ds, [JPATH_ROOT, $thumbnailImage]);
				
				$sourceImagePath = $sourceImage ? implode($ds, [JPATH_ROOT, $sourceImage]) : false;
				$sourceImageExists = $sourceImagePath && file_exists($sourceImagePath);
				$originalImagePath = implode($ds, [JPATH_ROOT, $originalImage]);
				$originalImageExists = $originalImagePath && file_exists($originalImagePath);

				// If source image does not exist, watermark is enabled, create it by clothing the original image
				if (!$sourceImageExists && $watermarkEnabled && $originalImage && file_exists($originalImagePath))
				{
					// Create source from original image
					$sourceImagePath = \NRFramework\File::copy($originalImagePath, $originalImagePath, false, true);
					$sourceImageExists = true;

					// Modify the database entry and add "source" image to item
					// We just need the relative path to file
					$_sourceImagePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $sourceImagePath);
					$item['source'] = $_sourceImagePath;
					$_originalImagePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $originalImagePath);
					GalleryManagerHelper::setItemFieldSource($item_id, $field_id, $_sourceImagePath, $_originalImagePath);
				}
				
				if (!$originalImageExists)
				{
					continue;
				}

				if (!$sourceImageExists)
				{
					$sourceImagePath = $originalImagePath;
				}

				/**
				 * Handle original image.
				 */
				// Generate original image by using the source image
				if ($original_image_resize_width && $original_image_resize_height)
				{
					$originalImagePath = Image::resize($sourceImagePath, $original_image_resize_width, $original_image_resize_height, 70, 'crop', $originalImagePath);
				}
				else if ($original_image_resize_width)
				{
					$originalImagePath = Image::resizeAndKeepAspectRatio($sourceImagePath, $original_image_resize_width, 70, $originalImagePath);
				}
				else if ($original_image_resize_height)
				{
					$originalImagePath = Image::resizeByHeight($sourceImagePath, $original_image_resize_height, $originalImagePath, 70);
				}

				$originalImageSourcePath = $originalImagePath;
				
				if ($watermarkEnabled)
				{
					$payload = array_merge($watermarkSettings, ['source' => $sourceImagePath, 'destination' => $originalImagePath]);
					Image::applyWatermark($payload);
				}

				/**
				 * Handle thumbnail image.
				 */
				// Generate thumbnail image by using the source image
				GalleryManagerHelper::generateThumbnail($sourceImagePath, $thumbnailImagePath, $resizeSettings, null, false);

				// Apply watermark to thumbnail image
				if ($watermarkEnabled && $thumbnailWatermarkEnabled)
				{
					$payload = array_merge($watermarkSettings, ['source' => $thumbnailImagePath]);
					Image::applyWatermark($payload);
				}
			}
		}

		echo json_encode(['success' => true, 'message' => Text::_('NR_GALLERY_MANAGER_IMAGES_REGENERATED'), 'items' => $items]);
	}

	/**
	 * Exits the page with given message.
	 * 
	 * @param   string  $translation_string
	 * 
	 * @return  void
	 */
	private function exitWithMessage($translation_string)
	{
		http_response_code('500');
		die(Text::_($translation_string));
	}
}GalleryManager2.php000064400000016537152414665110010252 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\Registry\Registry;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\Factory;
use NRFramework\Helpers\Widgets\GalleryManager2 as GalleryManagerHelper;
use NRFramework\Image;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;

/**
 *  Gallery Manager
 */
class GalleryManager2 extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// The uploaded images
		'value' => [],
		
		// The input name
		'name' => '',

		// Context of the field
		// module, default
		'context' => 'default',

		// The field ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
		'field_id' => null,

		// The item ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
		'item_id' => null,

		/**
		 * Max file size in MB.
		 * 
		 * Defults to 0 (no limit).
		 */
		'max_file_size' => 0,

		/**
		 * How many files we can upload.
		 * 
		 * Defaults to 0 (no limit).
		 */
		'limit_files' => 0,

		// Allowed upload file types
		'allowed_file_types' => 'image/*',

		/**
		 * Original Image
		 */
		// Original image resize width
		'original_image_resize_width' => null,

		// Original image resize height
		'original_image_resize_height' => null,

		/**
		 * Thumbnails
		 */
		// Thumbnails width
		'thumb_width' => null,

		// Thumbnails height
		'thumb_height' => null,

		// Thumbnails resize method (crop, stretch, fit)
		'thumb_resize_method' => 'crop',

		// The list of tags already available for this gallery
		'tags' => [],

		// The widget name
		'widget' => 'GalleryManager2'
	];

	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();

		// Load translation strings
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
        Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
        Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
	}

	private function prepare()
	{
		$this->includeTempFiles();
		
		// Set css class for readonly state
		if ($this->options['readonly'])
		{
			$this->options['css_class'] .= ' readonly';
		}

		// Adds a css class when the gallery contains at least one item
		if (is_array($this->options['value']) && count($this->options['value']))
		{
			$this->options['css_class'] .= ' dz-has-items';
		}

		// Load translation strings
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
        Text::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
        Text::script('NR_GALLERY_MANAGER_FILE_MISSING');
        Text::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');

		$this->prepareTags();
	}

	/**
	 * Find and include temp files in the gallery.
	 * 
	 * @return  void
	 */
	private function includeTempFiles()
	{
		$ds = DIRECTORY_SEPARATOR;
		
		$tempFolder = GalleryManagerHelper::getFullTempFolder($this->options['context'], $this->options['field_id'], $this->options['item_id']);

		if (!is_dir($tempFolder))
		{
			return;
		}
		
		$files = Folder::files($tempFolder, '.', false, false, ['.', '..', 'index.html', 'index.php']);

		if (!$files)
		{
			return;
		}

		$relativeTempFolder = ltrim(str_replace(JPATH_ROOT, '', $tempFolder), $ds);

		foreach ($files as $filename)
		{
			$this->options['value'][] = [
                'source' => implode($ds, [$relativeTempFolder, $filename]),
                'original' =>'',
                'exists' => true,
                'caption' => '',
                'thumbnail' => '',
                'slideshow' => '',
                'alt' => '',
                'tags' => json_encode([]),
				'temp' => true
			];
		}
	}

	private function prepareTags()
	{
		if (!is_array($this->options['value']))
		{
			return;
		}

		$db    = Factory::getDbo();
		$query = $db->getQuery(true)
			->select([$db->quoteName('id'), $db->quoteName('title')])
			->from($db->quoteName('#__tags'))
			->where($db->quoteName('published') . ' = 1')
			->where($db->quoteName('level') . ' > 0');

		$db->setQuery($query);
		$tags = $db->loadAssocList('id', 'title');

		$this->options['tags'] = $tags;
	}

	/**
	 * The upload task called by the AJAX hanler
	 *
	 * @return  void
	 */
	protected function ajax_upload()
	{
        // Increase memory size and execution time to prevent PHP errors on datasets > 20K
        set_time_limit(300); // 5 Minutes
        ini_set('memory_limit', '-1');
		
		$input = Factory::getApplication()->input;

		$random_suffix = $input->get('random_suffix', 'false') === 'true' ? true : false;

		// Make sure we have a valid context
		if (!$context = $input->get('context'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
		}

		// Make sure we have a valid file passed
		if (!$file = $input->files->get('file'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
		}

		// In case we allow multiple uploads the file parameter is a 2 levels array.
		$first_property = array_pop($file);
		if (is_array($first_property))
		{
			$file = $first_property;
		}

		$uploadSettings = [
			'context' => $context,
			'field_id' => $input->getInt('field_id'),
			'item_id' => $input->getInt('item_id'),
			'allow_unsafe' => false,
			'allowed_types' => $this->widget_options['allowed_file_types'],
			'random_suffix' => $random_suffix
		];

		// Upload the file and resize the images as required
		if (!$source = GalleryManagerHelper::upload($file, $uploadSettings))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_CANNOT_UPLOAD_FILE');
		}

		echo json_encode([
			'source' => $source
		]);
	}

	/**
	 * The delete task called by the AJAX hanlder
	 *
	 * @return void
	 */
	protected function ajax_delete()
	{
        // Increase memory size and execution time to prevent PHP errors on datasets > 20K
        set_time_limit(300); // 5 Minutes
        ini_set('memory_limit', '-1');
		
		$input = Factory::getApplication()->input;

		// Get source image path.
		$source = $input->getString('source');

		// Get the slideshow image path.
		$slideshow = $input->getString('slideshow', '');

		// Get the original image
		$original = $input->getString('original');

		// Get the thumbnail image
		$thumbnail = $input->getString('thumbnail');

		if (!$context = $input->get('context'))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_CONTEXT_ERROR');
		}

		$field_id = $input->getInt('field_id');
		$item_id = $input->getInt('item_id');
		
		if (!$field_data = GalleryManagerHelper::getSettings($context, $field_id, $item_id))
		{
			$this->exitWithMessage('NR_GALLERY_MANAGER_INVALID_FIELD_DATA');
		}

		// Delete the source, original, and thumbnail file
		$deleted = GalleryManagerHelper::deleteFile($source, $slideshow, $original, $thumbnail);
		
		echo json_encode(['success' => $deleted]);
	}

	/**
	 * Exits the page with given message.
	 * 
	 * @param   string  $translation_string
	 * 
	 * @return  void
	 */
	private function exitWithMessage($translation_string)
	{
		http_response_code('500');
		die(Text::_($translation_string));
	}
}GoogleMap.php000064400000001410152414665200007130 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;

class GoogleMap extends Map
{
	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		parent::loadMedia();

		HTMLHelper::script('plg_system_nrframework/widgets/googlemap.js', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('https://maps.googleapis.com/maps/api/js?callback=Function.prototype&key=' . $this->options['provider_key'], ['relative' => false, 'version' => false]);
	}
}Helper.php000064400000005341152414665240006510 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\Filesystem\Folder;

class Helper
{
    /**
     * This is a map with all widgets used for caching the widget's class name
     *
     * @var array
     */
    public static $widgets_map = [];

	/**
	 * Renders a Widget and returns 
	 * 
	 * @param   array   $options	A list of attributes passed to the layout
	 * 
	 * @return  string  The widget's final HTML layout
	 */
	public static function render($widget_name, $options = [])
	{
        if (!$widgetClass = self::find($widget_name))
        {
            return;
        }

		$class = __NAMESPACE__ . '\\' . $widgetClass;

		// ensure class exists
		if (!class_exists($class))
		{
			return;
		}

		return (new $class($options))->render();
    }

    /**
     * Return the real class name of a widget by a case-insensitive name.
     *
     * @param   string   $name   The widget's name
     * 
     * @return  mixed    Null when the class name is not found, string when the class name is found.
     */
    public static function find($name)
    {
        if (!$name)
        {
            return;
        }

        $name = strtolower($name);

        if (empty(self::$widgets_map) || !isset(self::$widgets_map[$name]))
        {
            $widgetClasses = Folder::files(__DIR__);
            foreach ($widgetClasses as $widgetClass)
            {
                $widgetClass = str_replace('.php', '', $widgetClass);
    
                self::$widgets_map[strtolower($widgetClass)] = $widgetClass;
            }
        }

        return isset(self::$widgets_map[$name]) ? self::$widgets_map[$name] : null;
    }

	/**
	 * Returns all layout overrides of a widget by its name.
	 * 
	 * @param   string  $name
	 * 
	 * @return  array
	 */
	public static function getLayoutOverrides($name = '')
	{
		if (!$name)
		{
			return;
		}

		$path = self::getLayoutOverridePath($name);

		if (!is_dir($path))
		{
			return;
		}

		$labels = array_diff(scandir($path), ['.', '..', '.DS_Store']);
		$values = array_map(function($value) {
			return rtrim($value, '.php');
		}, $labels);

		return array_combine($values, $labels);
	}

	/**
	 * Returns the layout override path of a widget by its name.
	 * 
	 * @param   string  $name
	 * 
	 * @return  string
	 */
	public static function getLayoutOverridePath($name = '')
	{
		if (!$name)
		{
			return;
		}

		return implode(DIRECTORY_SEPARATOR, [JPATH_SITE, 'templates', \NRFramework\Helpers\Template::getTemplateName(), 'html', 'tassos', 'widgets', $name]);
	}
}Map.php000064400000014016152414665300006002 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;

abstract class Map extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * The value of the widget.
		 * Format: latitude,longitude
		 * 
		 * i.e. 36.891319,27.283480
		 * 
		 * Otherwise, set the markers property
		 */
		'value' => '',

		// Map tile provider key (if needed) to use the provider tiles
		'provider_key' => null,

		// Default map width
		'width' => 500,

		// Default map height
		'height' => 400,

		/**
		 * The Zoom Level.
		 * 
		 * - preset: Set a fixed zoom.
		 * - fitbounds: Allow the map provider to auto-zoom and center the map around the markers.
		 */
		'zoom_level' => 'preset',

		// Default map zoon
		'zoom' => 4,

		// Define lat,long format which will be used to center the map when zoom_level=preset is used.
		'map_center' => null,

		// Map scale. Values: metric, imperial, false
		'scale' => false,

		// View mode of the map.
		'view' => '',

		/**
		 * Set whether to show or not the map marker info window.
		 * 
		 * Defaults to the map marker address (if not empty).
		 * If a map makrer label and/or description is set, these will be used.
		 */
		'enable_info_window' => true,
		
		/**
		 * Map Marker
		 */
		/**
		 * The markers.
		 * 
		 * An array of markers.
		 * 
		 * [
		 * 	  [
		 * 	  	  latitude: 36.891319,
		 * 	  	  longitude: 27.283480,
		 * 	  	  label: 'Marker label',
		 * 	  	  description: 'Marker description'
		 * 	  ]
		 * ]
		 */
		'markers' => [],

		// Marker image relative to Joomla installation
		'markerImage' => ''
	];

	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();
	}

	private function prepare()
	{
		$this->options['markerImage'] = $this->options['markerImage'] ? Uri::root() . ltrim($this->options['markerImage'], DIRECTORY_SEPARATOR) : '';

		// Set the marker if a single value was given
		if ($this->options['value'] && empty($this->options['markers']))
		{
			$coords = array_filter(array_map('trim', explode(',', $this->options['value'])));
			if (count($coords) === 2)
			{
				$this->options['markers'] = [
					[
						'id' => 1,
						'latitude' => $coords[0],
						'longitude' => $coords[1]
					]
				];
			}
		}

		// Make markers an array if a JSON string was given
		if (is_string($this->options['markers']))
		{
			$this->options['markers'] = json_decode($this->options['markers'], true);
		}

		// Set as value the first marker so the JS library can have an initial center of the map
		if (is_array($this->options['markers']))
		{
			$latitude = isset($this->options['markers'][0]['latitude']) ? $this->options['markers'][0]['latitude'] : false;
			$longitude = isset($this->options['markers'][0]['longitude']) ? $this->options['markers'][0]['longitude'] : false;

			if ($latitude && $longitude)
			{
				$this->options['value'] = implode(',', [$latitude, $longitude]);
			}
		}

		if ($this->options['load_css_vars'])
		{
			$this->options['custom_css'] = $this->getWidgetCSS();
		}

		// Transform title/description translation strings
		$this->prepareMarkerText();
	}

	private function prepareMarkerText()
	{
		if (!is_array($this->options['markers']) || !count($this->options['markers']))
		{
			return;
		}

		foreach ($this->options['markers'] as &$marker)
		{
			if (isset($marker['label']) && $marker['label'])
			{
				$marker['label'] = Text::_($marker['label']);
			}

			if (isset($marker['description']) && $marker['description'])
			{
				$marker['description'] = Text::_($marker['description']);
			}

			if (empty($marker['label']) && isset($marker['address']))
			{
				$marker['label'] = $marker['address'];
			}

			// Link to the item
			if (!empty($marker['label']) && isset($marker['item_id']) && $marker['item_id'] && isset($marker['context']) && $marker['context'] && $marker['context'] !== 'com_users.user')
			{
				$context = explode('.', $marker['context']);

				$routerHelper = $routerMethod = null;

				// Content
				if ($marker['context'] === 'com_content.article')
				{
					$routerHelper = defined('nrJ4') ? 'Joomla\Component\Content\Site\Helper\RouteHelper' : 'ContentHelperRoute';
					$routerMethod = 'getArticleRoute';
				}
				// Contact
				else
				{
					$routerHelper = defined('nrJ4') ? 'Joomla\Component\Contact\Site\Helper\RouteHelper' : 'ContactHelperRoute';
					$routerMethod = 'getContactRoute';
				}
				
				$url = Route::_($routerHelper::$routerMethod($marker['item_id'], $marker['cat_id'], $marker['language']));
				
				$marker['label'] = '<a href="' . $url . '">' . $marker['label'] . '</a>';
			}
		}
	}

	/**
	 * Returns the CSS for the widget.
	 * 
	 * @param   array  $exclude_breakpoints   Define breakpoints to exclude their CSS
	 * 
	 * @return  string
	 */
	public function getWidgetCSS($exclude_breakpoints = [])
	{
		$controls = [
			// CSS Variables
            [
                'property' => '--width',
                'value' => $this->options['width'],
				'unit' => 'px'
			],
            [
                'property' => '--height',
                'value' => $this->options['height'],
				'unit' => 'px'
			],
		];

		$selector = '.nrf-widget.map-widget.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector, $exclude_breakpoints);
        
		if (!$controlsCSS = $controlsInstance->generateCSS($controls))
		{
			return;
		}

		return $controlsCSS;
	}

	public function render()
	{
		$this->loadMedia();

		return parent::render();
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		if ($this->options['load_stylesheet'])
		{
			HTMLHelper::stylesheet('plg_system_nrframework/widgets/map.css', ['relative' => true, 'version' => 'auto']);
		}
	}
}MapAddress.php000064400000010074152414665340007314 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
* @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

class MapAddress extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * The map coordinates.
		 * Format: latitude,longitude
		 * 
		 * i.e. 36.891319,27.283480
		 */
		'value' => '',

		/**
		 * Set whether and where to show the map.
		 * 
		 * Available values:
		 * 
		 * false
		 * backend
		 * frontend
		 * both
		 */
		'show_map' => false,

		// The map HTML (If can be rendered)
		'map' => false,

		// Set what information the user can see.
		'showAddressDetails' => [
			'address' => true,
			'latitude' => false,
			'longitude' => false,
			'country' => true,
			'country_code' => false,
			'city' => false,
			'postal_code' => true,
			'county' => false,
			'state' => false,
			'municipality' => false,
			'town' => false,
			'road' => false,
		],

		/**
		 * The address details.
		 * 
		 * Supported data:
		 * 
		 * address
		 * latitude
		 * longitude
		 * country
		 * country_code
		 * city
		 * postal_code
		 * county
		 * state
		 * municipality
		 * town
		 * road
		 */
		'address' => [
			'address' => '',
			'latitude' => '',
			'longitude' => '',
			'country' => '',
			'country_code' => '',
			'city' => '',
			'postal_code' => '',
			'county' => '',
			'state' => '',
			'municipality' => '',
			'town' => '',
			'road' => '',
		],

		/**
		 * The layout type of the output.
		 * 
		 * Available values:
		 * 
		 * - default
		 * - custom
		 */
		'layout_type' => 'default',

		/**
		 * The custom layout code (HTML + Smart Tags).
		 * 
		 * Available Smart Tags:
		 * 
		 * Allowed Smart Tags:
		 * 
		 * {address.map}
		 * {address.address} - {address.address.label}
		 * {address.latitude} - {address.latitude.label}
		 * {address.longitude} - {address.longitude.label}
		 * {address.country} - {address.country.label}
		 * {address.country_code} - {address.country_code.label}
		 * {address.city} - {address.city.label}
		 * {address.county} - {address.county.label}
		 * {address.postal_code} - {address.postal_code.label}
		 * {address.state} - {address.state.label}
		 * {address.municipality} - {address.municipality.label}
		 * {address.town} - {address.town.label}
		 * {address.road} - {address.road.label}
		 */
		'custom_layout' => '{address.address.label}: {address.address}',
		
		/**
		 * Map location in correlation with the address details.
		 * 
		 * Note: This takes effect only if no custom layout is used.
		 * 
		 * Available values:
		 * 
		 * - above (Above the address details)
		 * - below (Below the address details)
		 */
		'map_location' => 'below',

		// The map HTML which will return the map HTML only if a map is set and current layout is not custom
		'map_html' => ''
	];
	
	/**
	 * Renders the widget
	 * 
	 * @return  string
	 */
	public function render()
	{
		$this->options['enable_info_window'] = false;

		if (in_array($this->options['show_map'], ['frontend', 'both']) || (in_array($this->options['show_map'], ['frontend', 'both']) && $this->options['layout_type'] === 'custom' && !empty($this->options['custom_layout']) && strpos($this->options['custom_layout'], '{address.map}') !== false))
		{
			$this->options['markerImage'] = $this->options['marker_image'];
			// Get the map
			$map_options = $this->options;
			// Remove unneeded props.
			// The custom layout can include Smart Tags and this breaks the Smart Tags replace method as it tries to replace inner Smart Tags too
			unset($map_options['custom_layout']);
			unset($map_options['showAddressDetails']);
			unset($map_options['address']);
			$map = new OpenStreetMap($map_options);
			$map->loadMedia();
			$this->options['map'] = $map->render();
		}

		$this->options['map_html'] = in_array($this->options['show_map'], ['frontend', 'both']) && $this->options['layout_type'] !== 'custom' ? $this->options['map'] : '';

		return parent::render();
	}
}MapAddressEditor.php000064400000007037152414665400010465 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;

class MapAddressEditor extends Widget
{
	/**
	 * Default latitude.
	 * 
	 * @var  string
	 */
	private $default_lat = '38.24921060739844';

	/**
	 * Default longitude.
	 * 
	 * @var  string
	 */
	private $default_long = '25.314512745029823';
	
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * The map coordinates.
		 * Format: latitude,longitude
		 * 
		 * i.e. 36.891319,27.283480
		 */
		'value' => '0,0',

		/**
		 * Set whether and where to show the map.
		 * 
		 * Available values:
		 * 
		 * false
		 * backend
		 * frontend
		 * both
		 */
		'show_map' => false,

		// The actual map HTML
		'map' => false,

		// Whether autocomplete is enabled for the address field
		'autocomplete' => false,

		// Set what information the user can see/edit when selecting an address.
		'showAddressDetails' => [
			'address' => false,
			'latitude' => false,
			'longitude' => false,
			'country' => false,
			'country_code' => false,
			'city' => false,
			'postal_code' => false,
			'county' => false,
			'state' => false,
			'municipality' => false,
			'town' => false,
			'road' => false,
		],

		/**
		 * The address details.
		 * 
		 * Supported data:
		 * 
		 * address
		 * latitude
		 * longitude
		 * country
		 * country_code
		 * city
		 * postal_code
		 * county
		 * state
		 * municipality
		 * town
		 * road
		 */
		'address' => [
			'address' => '',
			'latitude' => '',
			'longitude' => '',
			'country' => '',
			'country_code' => '',
			'city' => '',
			'postal_code' => '',
			'county' => '',
			'state' => '',
			'municipality' => '',
			'town' => '',
			'road' => '',
		],
		
		/**
		 * Map location in correlation with the address details.
		 * 
		 * Note: This takes effect only if no custom layout is used.
		 * 
		 * Available values:
		 * 
		 * - above (Above the address details)
		 * - below (Below the address details)
		 */
		'map_location' => 'below'
	];

	public function __construct($options = [])
	{
		parent::__construct($options);
		
		if (isset($options['_showAddressDetails']))
		{
			$this->options['showAddressDetails'] = array_merge($this->options['showAddressDetails'], $this->options['_showAddressDetails']);
		}

		if ($options['required'])
		{
			$this->options['css_class'] = ' is-required';
		}

		$this->options['enable_info_window'] = false;
	}
	
	/**
	 * Renders the widget
	 * 
	 * @return  string
	 */
	public function render()
	{
		$this->loadMedia();

		$show_map = in_array($this->options['show_map'], ['backend', 'both']);
		
		// Get the map editor
		$map_options = array_merge($this->options, [
			'show_map' => $show_map,
			'autocomplete' => $this->options['autocomplete'],
			'address' => isset($this->options['address']['address']) ? $this->options['address']['address'] : ''
		]);
		$map = new MapAddressEditorView($map_options);
		$map->loadMedia();
		$this->options['map'] = $map->render();

		return parent::render();
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	private function loadMedia()
	{
		HTMLHelper::stylesheet('plg_system_nrframework/widgets/mapaddresseditor.css', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('plg_system_nrframework/widgets/mapaddresseditor.js', ['relative' => true, 'version' => 'auto']);
	}
}MapAddressEditorView.php000064400000005324152414665440011321 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;

class MapAddressEditorView extends OpenStreetMap
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $_widget_options = [
		// Whether autocomplete is enabled for the address field
		'autocomplete' => false,

		// Whether to show the map
		'show_map' => true,

		/**
		 * Set whether & where to display the address input.
		 * 
		 * Available values:
		 * - before_map: Show it before the map
		 * - true/after_map: Show it after the map
		 * - false: Hide the address field
		 */
		'show_address' => 'after_map',

		// The address value
		'address' => '',

		/**
		 * Markers
		 */
		// Show the markers list
		'show_markers_list' => false,

		// Max markers allowed
		'max_markers' => 1
	];

	public function __construct($options = [])
	{
		$this->widget_options = array_merge($this->widget_options, $this->_widget_options);

		parent::__construct($options);

		$this->prepare();
	}

	private function prepare()
	{
		// We do not show the map
		if (!$this->options['show_map'])
		{
			$this->options['css_class'] .= ' no-map';
		}

		// Hide "Clear" button if no markers exists
		if (empty($this->options['markers']))
		{
			$this->options['css_class'] .= ' clear-is-hidden';
		}

		if ((!$this->options['pro'] && count($this->options['markers']) >= 1) || ($this->options['max_markers'] !== 0 && count($this->options['markers']) >= $this->options['max_markers']))
		{
			$this->options['css_class'] .= ' markers-limit-reached';
		}

		Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_ALL_MARKERS');
		Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_MARKER');
		Text::script('NR_ADD_MARKER');
		Text::script('NR_EDIT_MARKER');
		Text::script('NR_DELETE_MARKER');
		Text::script('NR_UNKNOWN_LOCATION');
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		if ($this->options['show_map'])
		{
			parent::loadMedia();

			HTMLHelper::stylesheet('plg_system_nrframework/vendor/leaflet.contextmenu.min.css', ['relative' => true, 'version' => 'auto']);
			HTMLHelper::script('plg_system_nrframework/vendor/leaflet.contextmenu.min.js', ['relative' => true, 'version' => 'auto']);
		}

		HTMLHelper::stylesheet('plg_system_nrframework/widgets/mapaddresseditorview.css', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('plg_system_nrframework/widgets/mapaddresseditorview.js', ['relative' => true, 'version' => 'auto']);
	}
}MapEditor.php000064400000010541152414665500007152 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Language\Text;

class MapEditor extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		/**
		 * The list of markers added to the map.
		 * 
		 * Example:
		 * 
		 * [
		 * 	  'lat' => 37.9838,
		 * 	  'lng' => 23.7275,
		 * 	  'title' => 'Athens',
		 * 	  'description' => 'The capital of Greece',
		 * ]
		 */
		'value' => [],

		// The default map latitude. Where it points when no markers are added.
		'lat' => null,
		
		// The default map longitude. Where it points when no markers are added.
		'lng' => null,
		
		// Max markers allowed
		'maxMarkers' => 1,

		// Set whether to show the map editor sidebar
		'showSidebar' => true,

		// Set the marker image, relative path to an image file
		'markerImage' => '',

		// TODO: Remove this once ACF is updated and after a reasonable time
		'hide_input' => false
	];

	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();

		$this->loadMedia();
	}

	private function prepare()
	{
		if (!$this->options['pro'] && is_array($this->options['value']) && count($this->options['value']) >= 1)
		{
			$this->options['css_class'] .= ' markers-limit-reached';
		}

		if ($this->options['markerImage'])
		{
			$markerImage = explode('#', ltrim($this->options['markerImage'], DIRECTORY_SEPARATOR));
			$this->options['markerImage'] = Uri::root() . reset($markerImage);
		}

		Text::script('NR_ENTER_AN_ADDRESS_OR_COORDINATES');
		Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_ALL_SELECTED_MARKERS');
		Text::script('NR_ARE_YOU_SURE_YOU_WANT_TO_DELETE_THIS_MARKER');
		Text::script('NR_ADD_MARKER');
		Text::script('NR_EDIT_MARKER');
		Text::script('NR_DELETE_MARKER');
		Text::script('NR_UNKNOWN_LOCATION');
		Text::script('NR_UNLIMITED_MARKERS');
		Text::script('NR_ADD_MORE_MARKERS_UPGRADE_TO_PRO');
		Text::script('NR_MARKERS');
		Text::script('NR_YOU_HAVENT_ADDED_ANY_MARKERS_YET');
		Text::script('NR_ADD_YOUR_FIRST_MARKER');
		Text::script('NR_NO_MARKERS_FOUND');
		Text::script('NR_LOCATION_ADDRESS');
		Text::script('NR_ADD_TO_MAP');
		Text::script('NR_COORDINATES');
		Text::script('NR_ADDRESS_ADDRESS_HINT');
		Text::script('NR_LATITUDE');
		Text::script('NR_LONGITUDE');
		Text::script('NR_MARKER_INFO');
		Text::script('NR_LABEL');
		Text::script('NR_DESCRIPTION');
		Text::script('NR_MARKER_LABEL');
		Text::script('NR_MARKER_DESCRIPTION');
		Text::script('NR_SAVE');
		Text::script('NR_PLEASE_SELECT_A_LOCATION');
		Text::script('NR_IMPORT');
		Text::script('NR_IMPORT_MARKERS');
		Text::script('NR_IMPORT_LOCATIONS_DESC');
		Text::script('NR_IMPORT_LOCATIONS_DESC2');
		Text::script('NR_PLEASE_ENTER_LOCATIONS_TO_IMPORT');
		Text::script('NR_COULDNT_IMPORT_LOCATIONS');
		Text::script('NR_ADDING_MARKERS');
		Text::script('NR_SAVE_YOUR_FIRST_MARKER');
		Text::script('NR_OUT_OF');
		Text::script('NR_MARKERS_ADDED');
		Text::script('NR_MARKERS_LIMIT_REACHED_DELETE_MARKER_TO_ADD');
		Text::script('NR_EXPORT_MARKERS');
		Text::script('NR_EXPORT_MARKERS_DESC');
		Text::script('NR_THERE_ARE_NO_LOCATIONS_TO_EXPORT');
		Text::script('NR_LOCATIONS_IMPORTED');

		Factory::getDocument()->addScriptOptions('TFMapEditor', [
			'images_url' => Uri::root() . 'media/plg_system_nrframework/css/vendor/images/',
		]);
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		HTMLHelper::script('plg_system_nrframework/vendor/react.min.js', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('plg_system_nrframework/vendor/react-dom.min.js', ['relative' => true, 'version' => 'auto']);

		HTMLHelper::stylesheet('plg_system_nrframework/vendor/leaflet.min.css', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('plg_system_nrframework/vendor/leaflet.min.js', ['relative' => true, 'version' => 'auto']);
		
		HTMLHelper::stylesheet('plg_system_nrframework/widgets/mapeditor.css', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('plg_system_nrframework/mapeditor.js', ['relative' => true, 'version' => 'auto']);
	}
}OpenStreetMap.php000064400000002446152414665540010025 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;

class OpenStreetMap extends Map
{
	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		parent::loadMedia();
		
		HTMLHelper::stylesheet('plg_system_nrframework/vendor/leaflet.min.css', ['relative' => true, 'version' => 'auto']);
		HTMLHelper::script('plg_system_nrframework/vendor/leaflet.min.js', ['relative' => true, 'version' => 'auto']);
		
		if ($this->options['load_stylesheet'])
		{
			HTMLHelper::stylesheet('plg_system_nrframework/widgets/openstreetmap.css', ['relative' => true, 'version' => 'auto']);
		}

		if ($this->options['view'] === 'satellite')
		{
			HTMLHelper::script('plg_system_nrframework/vendor/esri-leaflet.min.js', ['relative' => true, 'version' => 'auto']);
			HTMLHelper::script('plg_system_nrframework/vendor/esri-leaflet-vector.min.js', ['relative' => true, 'version' => 'auto']);
		}

		HTMLHelper::script('plg_system_nrframework/widgets/openstreetmap.js', ['relative' => true, 'version' => 'auto']);
	}
}RangeSlider.php000064400000003150152414665600007464 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

/**
 *  The Range Slider widget
 */
class RangeSlider extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// The default value of the widget. 
		'value' => 0,

		// The minimum value of the slider
		'min' => 0,

		// The maximum value of the slider
		'max' => 100,

		// The step of the slider
		'step' => 1,

		// The main slider color
		'color' => '#1976d2',

		// The input border color of the slider inputs
		'input_border_color' => '#bdbdbd',

		// The input background color of the slider inputs
		'input_bg_color' => 'transparent'
	];

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		parent::__construct($options);

		// Base color is 20% of given color
		$this->options['base_color'] = $this->options['color'] . '33';

		// Calculate value
		$this->options['value'] = (float) $this->options['value'] < $this->options['min'] ? $this->options['min'] : ((float) $this->options['value'] > $this->options['max'] ? $this->options['max'] : (float) $this->options['value']);

		// Calculate bar percentage
		$this->options['bar_percentage'] = $this->options['max'] ? floor(100 * ($this->options['value'] - $this->options['min']) / ($this->options['max'] - $this->options['min'])) : $this->options['value'];
	}
}Rating.php000064400000003057152414665640006523 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Uri\Uri;

/**
 *  The Rating Widget
 */
class Rating extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// The SVG icon representing the rating icon. Available values: check, circle, flag, heart, smiley, square, star, thumbs_up
		'icon' => 'star',

		// The default value of the widget. 
		'value' => 0,

		// How many stars to show?
		'max_rating' => 5,

		// Whether to show half ratings
		'half_ratings' => false,

		// The size of the rating icon in pixels.
		'size' => 24,

		// The color of the icon in the default state
		'selected_color' => '#f6cc01',

		// The color of the icon in the selected and hover state
		'unselected_color' => '#bdbdbd'
	];

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->options['value'] = $this->options['value'] > $this->options['max_rating'] ? $this->options['max_rating'] : $this->options['value'];
		$this->options['icon_url'] = Uri::root() . 'media/plg_system_nrframework/svg/rating/' . $this->options['icon'] . '.svg';
		$this->options['max_rating'] = $this->options['half_ratings'] ? 2 * $this->options['max_rating'] : $this->options['max_rating'];
	}
}SelfHostedVideo.php000064400000003660152414665700010323 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;

class SelfHostedVideo extends Video
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $video_widget_options = [
		/**
		 * Specify how the video should be loaded when the page loads.
		 * 
		 * Allowed values:
		 * - metadata
		 * - auto
		 * - none
		 */
		'preload' => 'auto',

		// Whether to mute the video
		'mute' => false,

		// Whether to display controls on the video
		'controls' => true,

		// Whether to loop the video
		'loop' => false,

		// Stores the given video details
		'video' => ''
	];

	protected function prepare()
	{
		if (isset($this->options['value']) && !empty($this->options['value']))
		{
			$videos = \NRFramework\Helpers\File::getFileSources($this->options['value'], ['mp4', 'webm', 'ogg', 'mov']);
			$this->options['video'] = is_array($videos) && isset($videos[0]) ? $videos[0] : false;
		}

		$atts = [
			'data-video-id="' . $this->options['value'] . '"',
			'data-video-type="selfhostedvideo"',
			'data-video-mute="' . var_export($this->options['mute'], true) . '"',
			'data-video-controls="' . var_export($this->options['controls'], true) . '"',
			'data-video-loop="' . var_export($this->options['loop'], true) . '"',
			'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
			'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
		];

		$this->options['atts'] = implode(' ', $atts);
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function videoAssets()
	{
		HTMLHelper::script('plg_system_nrframework/widgets/video/selfhostedvideo.js', ['relative' => true, 'version' => 'auto']);
	}
}Signature.php000064400000003407152414665740007240 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

/**
 *  Signature
 */
class Signature extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// The base64 image data of the signature. 
		'value' => '',

		// The width of the signature in pixels or empty for auto width. The width will be taken from the signature container.
		'width' => '',

		// The height of the signature in pixels.
		'height' => '300px',

		// The background color of the signature.
		'background_color' => '#ffffff',

		// The border color of the canvas.
		'border_color' => '#dedede',

		/**
		 * The border radius of the canvas.
		 * 
		 * Example values: 0, 0px, 50px, 50%
		 */
		'border_radius' => 0,

		/**
		 * The border width of the canvas.
		 * 
		 * Example values: 0, 1px, 5px
		 */
		'border_width' => '1px',

		// Whether to show the horizontal line within the canvas
		'show_line' => true,
		
		/**
		 * The line color.
		 * 
		 * If `null`, retrieves the value from `border_color`
		 */
		'line_color' => null,
		
		// The pen color
		'pen_color' => '#000'
	];

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		parent::__construct($options);

		if ($this->options['readonly'])
		{
			$this->options['css_class'] .= ' readonly';
		}

		if (!empty($this->options['value']))
		{
			$this->options['css_class'] .= ' painted has-value';
		}
		
		if ($this->options['show_line'])
		{
			$this->options['css_class'] .= ' show-line';
		}
	}
}Slideshow.php000064400000014271152414666000007227 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use \NRFramework\Helpers\Widgets\Gallery as GalleryHelper;
use NRFramework\Mimes;
use NRFramework\File;
use NRFramework\Image;
use Joomla\CMS\Factory;

class Slideshow extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// Slides per view
		'slides_per_view' => [
			'desktop' => 1
		],

		// Space between slides in px
		'space_between_slides' => [
			'desktop' => 10
		],

		// Enable Infinite Loop
		'infinite_loop' => false,

		// Enable Keyboard Control
		'keyboard_control' => false,

		/**
		 * Set the ordering.
		 * 
		 * Available values:
		 * - default
		 * - alphabetical
		 * - reverse_alphabetical
		 * - random
		 */
		'ordering' => 'default',

		/**
		 * The transition effect.
		 * 
		 * Available values:
		 * - slide
		 * - fade
		 * - cube
		 * - coverflow
		 * - flip
		 */
		'transition_effect' => 'slide',

		// Enable Autoplay
		'autoplay' => false,

		// Autoplay delay
		'autoplay_delay' => 3000,

		// Enable autoplay circular progress
		'autoplay_progress' => false,

		// Show thumbnails below the slideshow
		'show_thumbnails' => false,

		// Set whether to show arrows in the thumbnails slider
		'show_thumbnails_arrows' => false,

		/**
		 * Navigation controls.
		 * 
		 * Accepted values:
		 * - arrows
		 * - dots
		 * - arrows_dots
		 */
		'nav_controls' => false,
		
		// Theme color
		'theme_color' => '#007aff',

		// Set whether to display a lightbox
		'lightbox' => false,
		
		// Set the module key to display whenever we are viewing a single item's lightbox, appearing after the image
		'module' => '',

		// Options
		'options' => []
	];

	public function __construct($options = [])
	{
		parent::__construct($options);

		$this->prepare();

		$this->prepareItems();

		$this->setOrdering();

		$this->setCSSVars();
	}

	private function prepare()
	{
		if ($this->options['lightbox'])
		{
			$this->options['css_class'] .= ' lightbox';
		}
		
		$options = [
			'transition_effect' => $this->options['transition_effect'],
			'infinite_loop' => $this->options['infinite_loop'],
			'keyboard_control' => $this->options['keyboard_control'],
			'autoplay' => $this->options['autoplay'],
			'autoplay_delay' => $this->options['autoplay_delay'],
			'autoplay_progress' => $this->options['autoplay_progress'],
			'show_thumbnails' => $this->options['show_thumbnails'],
			'show_thumbnails_arrows' => $this->options['show_thumbnails_arrows'],
			'lightbox' => $this->options['lightbox'],
			'breakpoints' => \NRFramework\Helpers\Responsive::getBreakpointsSettings(),
			'slides_per_view' => $this->options['slides_per_view'],
			'space_between_slides' => $this->getSpaceBetweenSlides(),
			'nav_controls' => $this->options['nav_controls']
		];

		$this->options['options'] = $options;
	}

	private function getSpaceBetweenSlides()
	{
		$space_between_slides = $this->options['space_between_slides'];

		if (is_array($space_between_slides))
		{
			foreach ($space_between_slides as $key => &$value)
			{
				$value = \NRFramework\Helpers\Controls\Control::getCSSValue($value['value']);
			}
		}

		return $space_between_slides;
	}

	/**
	 * Prepares the items.
	 * 
	 * - Sets the thumbnails image dimensions.
	 * - Assures caption property exist.
	 * 
	 * @return  mixed
	 */
	private function prepareItems()
	{
		if (!is_array($this->options['items']) || !count($this->options['items']))
		{
			return;
		}

		foreach ($this->options['items'] as $key => &$item)
		{
			// Initialize image atts
			$item['img_atts'] = '';

			// Initializes caption if none given
			if (!isset($item['caption']))
			{
				$item['caption'] = '';
			}

            $item['alt'] = !empty($item['caption']) ? mb_substr($item['caption'], 0, 100) : pathinfo($item['url'], PATHINFO_FILENAME);
			
			// Ensure a thumbnail is given
			if (!isset($item['thumbnail_url']))
			{
				// If no thumbnail is given, set it to the full image
				$item['thumbnail_url'] = $item['url'];
				continue;
			}

			// If the thumbnail size for this item is given, set the image attributes
			if (isset($item['thumbnail_size']))
			{
				$item['img_atts'] = 'width="' . $item['thumbnail_size']['width'] . '" height="' . $item['thumbnail_size']['height'] . '"';
				continue;
			}
		}
	}
	
	/**
	 * Sets the ordering of the gallery.
	 * 
	 * @return  void
	 */
	private function setOrdering()
	{
		switch ($this->options['ordering']) {
			case 'random':
				shuffle($this->options['items']);
				break;
			case 'alphabetical':
				usort($this->options['items'], [$this, 'compareByThumbnailASC']);
				break;
			case 'reverse_alphabetical':
				usort($this->options['items'], [$this, 'compareByThumbnailDESC']);
				break;
		}
	}

	/**
	 * Compares thumbnail file names in ASC order
	 * 
	 * @param   array  $a
	 * @param   array  $b
	 * 
	 * @return  bool
	 */
	private function compareByThumbnailASC($a, $b)
	{
		return strcmp(basename($a['thumbnail']), basename($b['thumbnail']));
	}

	/**
	 * Compares thumbnail file names in DESC order
	 * 
	 * @param   array  $a
	 * @param   array  $b
	 * 
	 * @return  bool
	 */
	private function compareByThumbnailDESC($a, $b)
	{
		return strcmp(basename($b['thumbnail']), basename($a['thumbnail']));
	}

	/**
	 * Sets the CSS variables.
	 * 
	 * @return  void
	 */
	private function setCSSVars()
	{
		if (!$this->options['load_css_vars'])
		{
			return;
		}
	
		$controls = [
            [
                'property' => '--slideshow-slides-per-view',
                'value' => $this->options['slides_per_view']
            ],
            [
                'property' => '--slideshow-space-between-slides',
                'value' => $this->options['space_between_slides']
            ]
		];

		$selector = '.nrf-widget.tf-slideshow-wrapper.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector);

        if (!$controlsCSS = $controlsInstance->generateCSS($controls))
        {
            return;
        }

        Factory::getDocument()->addStyleDeclaration($controlsCSS);
	}
}Video.php000064400000004503152414666100006332 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;

abstract class Video extends Widget
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $widget_options = [
		// Video URL
		'value' => '',

		// Video width
		'width' => '480px',

		// Video height
		'height' => '270px',

		// Whether the video will autoplay
		'autoplay' => false,

		// Whether the video will autopause whenever we scroll and it hides from our viewport
		'autopause' => false
	];

	public function __construct($options = [])
	{
		$this->widget_options = array_merge($this->widget_options, $this->video_widget_options);

		parent::__construct($options);

		$this->prepare();

		$this->styles();
	}

	protected function prepare()
	{}

	public function render()
	{
		$this->loadMedia();

		return parent::render();
	}

	public function styles()
	{
		if (!$this->options['load_css_vars'])
		{
			return;
		}
	
		$controls = [
            [
                'property' => '--video-width',
                'value' => $this->options['width']
            ],
            [
                'property' => '--video-height',
                'value' => $this->options['height']
            ]
		];

		$selector = '.nrf-widget.tf-video.' . $this->options['id'];
		
		$controlsInstance = new \NRFramework\Controls\Controls(null, $selector);

        if (!$controlsCSS = $controlsInstance->generateCSS($controls))
        {
            return;
        }

        Factory::getDocument()->addStyleDeclaration($controlsCSS);
	}

	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function loadMedia()
	{
		if ($this->options['load_stylesheet'])
		{
			HTMLHelper::stylesheet('plg_system_nrframework/widgets/video.css', ['relative' => true, 'version' => 'auto']);
		}

		HTMLHelper::script('plg_system_nrframework/widgets/video.js', ['relative' => true, 'version' => 'auto']);

		if (method_exists($this, 'videoAssets'))
		{
			$this->videoAssets();
		}
		
		HTMLHelper::script('plg_system_nrframework/widgets/videos.js', ['relative' => true, 'version' => 'auto']);
	}
}Vimeo.php000064400000007563152414666200006355 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;

class Vimeo extends Video
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $video_widget_options = [
		/**
		 * Set the cover image type.
		 * 
		 * Allowed Values:
		 * - none
		 * - auto
		 * - custom
		 */
		'coverImageType' => 'none',

		// The Cover Image URL when coverImage="custom"
		'coverImage' => '',

		// Whether controls will appear in the video
		'controls' => true,

		// Loop
		'loop' => false,

		// Mute
		'mute' => false,

		/**
		 * Set whether to load the video in privacy-enhanced mode.
		 * 
		 * When this is enabled, Vimeo will block the player from tracking
		 * any session data, including all cookies and analytics.
		 */
		'privacy' => false,

		// Whether to show the video title
		'title' => false,

		// Whether to show the author of the video
		'byline' => false,

		// Whether to show the author's profile image
		'portrait' => false,

		// The color of the video controls
		'color' => '#00adef',

		// Whether to allow keyboard inputs
		'keyboard' => false,

		// Enable to show the picture-in-picture button in the control bar
		'pip' => false,

		// Set the start time
		'start' => null,

		// Set the end time
		'end' => null,
	];

	/**
	 * Prepares the widget.
	 * 
	 * @return  void
	 */
	protected function prepare()
	{
		$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
		$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';

		// Abort
		if ($videoProvider !== 'vimeo')
		{
			$this->options['value'] = null;
			return;
		}

		$this->options['css_class'] .= ' vimeo';

		$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';

		if ($this->options['coverImageType'] === 'auto')
		{
			$this->options['coverImage'] = 'url("https://vumbnail.com/' . $videoID . '.jpg")';
		}
		else if ($this->options['coverImageType'] === 'custom' && !empty($this->options['coverImage']))
		{
			$coverImage = explode('#', $this->options['coverImage']);
			$this->options['coverImage'] = 'url("' . Uri::base() . reset($coverImage) . '")';
		}
		
		$atts = [
			'data-video-id="' . $videoID . '"',
			'data-video-type="' . $videoProvider . '"',
			'data-video-mute="' . var_export($this->options['mute'], true) . '"',
			'data-video-controls="' . var_export($this->options['controls'], true) . '"',
			'data-video-loop="' . var_export($this->options['loop'], true) . '"',
			'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
			'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
			'data-video-privacy="' . var_export($this->options['privacy'], true) . '"',
			'data-video-title="' . var_export($this->options['title'], true) . '"',
			'data-video-byline="' . var_export($this->options['byline'], true) . '"',
			'data-video-portrait="' . var_export($this->options['portrait'], true) . '"',
			'data-video-keyboard="' . var_export($this->options['keyboard'], true) . '"',
			'data-video-pip="' . var_export($this->options['pip'], true) . '"',
			'data-video-color="' . $this->options['color'] . '"',
			'data-video-start="' . $this->options['start'] . '"',
			'data-video-end="' . $this->options['end'] . '"',
		];

		$this->options['atts'] = implode(' ', $atts);
	}

	/**
	 * We use the video widget layout file.
	 * 
	 * @return  string
	 */
	public function getName()
	{
		return 'video';
	}
	
	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function videoAssets()
	{
		HTMLHelper::script('plg_system_nrframework/widgets/video/vimeo.js', ['relative' => true, 'version' => 'auto']);
	}
}Widget.php000064400000010712152414666240006513 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Session\Session;

class Widget
{
	protected $widget_options = [];
	
	/**
	 * Widget's default options
	 *
	 * @var array
	 */
	protected $options = [
		// Set whether to load the CSS variables
		'load_css_vars' => true,

		// Set whether to load the default stylesheet
		'load_stylesheet' => true,

		// If true, the widget will be rended in read-only mode.
		'readonly' => false,

		// If true, the widget will be rended in disabled mode.
		'disabled' => false,

		// Indicates the widget's input field must be filled out before submitting the form.
		'required' => false,

		// The CSS class to be used on the widget's wrapper
		'css_class' => '',

		// The CSS class to be used on the input
		'input_class' => '',

		// The input name
		'name' => '',

		// The default widget value
		'value' => '',
		
		// Extra attributes
		'atts' => '',
		
		// Custom CSS
		'custom_css' => '',

		// A short hint that describes the expected value
		'placeholder' => '',

		// The name of the layout to be used to render the widget
		'layout' => 'default',

		// The extension name where the widget is loaded from.
		'extension' => '',

		// The aria-label attribute
		'aria_label' => '',
		
		// Whether we are rendering the Pro version of the widget
		'pro' => false
	];

	/**
	 * If no name is provided, this counter is appended to the widget's name to prevent name conflicts 
	 *
	 * @var int
	 */
	protected static $counter = 0;

	/**
	 * Class constructor
	 *
	 * @param array $options
	 */
	public function __construct($options = [])
	{
		// Merge Widget class default options with given Widget default options
		$this->options = array_merge($this->options, $this->widget_options, $options);

		// Set ID if none given
		if (!isset($this->options['id']))
		{
			$this->options['id'] = $this->getName() . self::$counter;
		}

		// Help developers target the whole widget by applying the widget's ID to the CSS class list.
		// Do not use the id="xx" attribute in the HTML to prevent conflicts with the input's ID.
		$this->options['css_class'] .= ' ' . $this->options['id'];
		
		// Set name if none given
		if (!isset($this->options['name']))
		{
			$this->options['name'] = $this->options['id'];
		}
		
		// Set disabled class if widget is disabled
		if ($this->options['disabled'])
		{
			$this->options['css_class'] .= ' disabled';
		}

		self::$counter++;
	}

	/**
	 * Renders the widget with the given layout
	 * 
	 * Layouts can be overriden in the following folder: /templates/TEMPLATE_NAME/html/tassos/WIDGET_NAME/LAYOUT_NAME.php
	 * 
	 * @return  string
	 */
	public function render()
	{
		$defaultPath  = implode(DIRECTORY_SEPARATOR, [JPATH_PLUGINS, 'system', 'nrframework', 'layouts']);
		$overridePath = implode(DIRECTORY_SEPARATOR, [JPATH_THEMES, Factory::getApplication()->getTemplate(), 'html', 'tassos']);

		$layout = new FileLayout('widgets.' . $this->getName() . '.' . $this->options['layout'], null, ['debug' => false]);
		$layout->addIncludePaths($defaultPath);
		$layout->addIncludePaths($overridePath);

		return $layout->render($this->options);
	}

	/**
	 * Get the name of the widget
	 *
	 * @return void
	 */
	public function getName()
	{
		return strtolower((new \ReflectionClass($this))->getShortName());
	}

	/**
	 * Returns the options key value.
	 * 
	 * @param   string  $key
	 * 
	 * @return  mixed
	 */
	public function getOption($key)
	{
		return isset($this->options[$key]) ? $this->options[$key] : null;
	}

	/**
	 * Manages ajax requests for the widget.
	 * 
	 * @param   string  $task
	 * 
	 * @return  void
	 */
	public function onAjax($task)
	{
        Session::checkToken('request') or die('Invalid Token');

		if (!$task || !is_string($task))
		{
			return;
		}

		$method = 'ajax_' . $task;

		if (!method_exists($this, $method))
		{
			return;
		}

		$this->$method();
	}
	
	/**
	 * Sets the options key value.
	 * 
	 * @param   string  $key
	 * 
	 * @return  void
	 */
	public function setOption($key, $value)
	{
		$this->options[$key] = $value;
	}

	/**
	 * Checks if an option exists.
	 * 
	 * @param   string  $key
	 * 
	 * @return  bool
	 */
	public function optionExists($key)
	{
		return isset($this->options[$key]);
	}
}YouTube.php000064400000007723152414666300006671 0ustar00<?php

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2020 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace NRFramework\Widgets;

defined('_JEXEC') or die;

use Joomla\CMS\Uri\Uri;
use Joomla\CMS\HTML\HTMLHelper;

class YouTube extends Video
{
	/**
	 * Widget default options
	 *
	 * @var array
	 */
	protected $video_widget_options = [
		/**
		 * Set the cover image type.
		 * 
		 * Allowed Values:
		 * - none
		 * - auto
		 * - custom
		 */
		'coverImageType' => 'none',

		// The Cover Image URL when coverImage="custom"
		'coverImage' => '',

		// Whether we allow fullscreen
		'fs' => false,

		// Whether controls will appear in the video
		'controls' => true,

		// Loop
		'loop' => false,

		// Mute
		'mute' => false,

		// Closed Captions
		'cc_load_policy' => false,

		/**
		 * The color that will be used in the player's video progress bar to highlight
		 * the amount of the video that the viewer has already seen.
		 * 
		 * Allowed Values:
		 * - red
		 * - white
		 */
		'color' => 'red',

		// Whether to allow or not keyboard shortcuts
		'disablekb' => false,

		// Start the video from X seconds
		'start' => null,

		// End the video at X seconds
		'end' => null,

		/**
		 * Set whether to show related videos.
		 * 
		 * Allowed Values:
		 * 0: Don't show related videos
		 * 1: Show related videos from anywhere
		 */
		'rel' => '1',

		/**
		 * Set whether to load the video in privacy-enhanced mode.
		 * 
		 * When this is enabled, YouTube won't store information about
		 * visitors unless they play the video.
		 */
		'privacy' => false
	];

	/**
	 * Prepares the widget.
	 * 
	 * @return  void
	 */
	protected function prepare()
	{
		$videoDetails = \NRFramework\Helpers\Video::getDetails($this->options['value']);
		$videoProvider = isset($videoDetails['provider']) ? $videoDetails['provider'] : '';
		
		// Abort
		if ($videoProvider !== 'youtube')
		{
			$this->options['value'] = null;
			return;
		}

		$this->options['css_class'] .= ' youtube';

		$videoID = isset($videoDetails['id']) ? $videoDetails['id'] : '';

		if ($this->options['coverImageType'] === 'auto')
		{
			$this->options['coverImage'] = 'url("https://img.youtube.com/vi/' . $videoID . '/maxresdefault.jpg")';
		}
		else if ($this->options['coverImageType'] === 'custom' && !empty($this->options['coverImage']))
		{
			$coverImage = explode('#', $this->options['coverImage']);
			$this->options['coverImage'] = 'url("' . Uri::base() . reset($coverImage) . '")';
		}
		
		$atts = [
			'data-video-id="' . $videoID . '"',
			'data-video-controls="' . var_export($this->options['controls'], true) . '"',
			'data-video-type="' . $videoProvider . '"',
			'data-video-mute="' . var_export($this->options['mute'], true) . '"',
			'data-video-loop="' . var_export($this->options['loop'], true) . '"',
			'data-video-start="' . $this->options['start'] . '"',
			'data-video-end="' . $this->options['end'] . '"',
			'data-video-autoplay="' . var_export($this->options['autoplay'], true) . '"',
			'data-video-fs="' . var_export($this->options['fs'], true) . '"',
			'data-video-autopause="' . var_export($this->options['autopause'], true) . '"',
			'data-video-cc="' . var_export($this->options['cc_load_policy'], true) . '"',
			'data-video-disablekb="' . var_export($this->options['disablekb'], true) . '"',
			'data-video-privacy="' . var_export($this->options['privacy'], true) . '"',
			'data-video-rel="' . $this->options['rel'] . '"',
			'data-video-color="' . $this->options['color'] . '"'
		];

		$this->options['atts'] = implode(' ', $atts);
	}

	/**
	 * We use the video widget layout file.
	 * 
	 * @return  string
	 */
	public function getName()
	{
		return 'video';
	}
	
	/**
	 * Loads media files
	 * 
	 * @return  void
	 */
	public function videoAssets()
	{
		HTMLHelper::script('plg_system_nrframework/widgets/video/youtube.js', ['relative' => true, 'version' => 'auto']);
	}
}