This commit is contained in:
2024-12-31 11:07:09 +01:00
parent df7915205d
commit e089172b15
1916 changed files with 165422 additions and 271 deletions

View File

@ -0,0 +1,460 @@
<?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'
];
}
}

View File

@ -0,0 +1,30 @@
<?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']);
}
}

View File

@ -0,0 +1,40 @@
<?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'
];
}

View File

@ -0,0 +1,542 @@
<?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'
];
}
}

View File

@ -0,0 +1,119 @@
<?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']);
}
}

View File

@ -0,0 +1,392 @@
<?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'
];
}
}

View File

@ -0,0 +1,83 @@
<?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']);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,609 @@
<?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 Joomla\CMS\Uri\Uri;
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' => [],
// Open AI API Key
'openai_api_key' => '',
// The widget name
'widget' => 'GalleryManager'
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
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';
}
// Get the Open AI API key
$this->options['openai_api_key'] = \NRFramework\Helpers\Settings::getValue('openai_api_key');
// 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');
Text::script('NR_GENERATE_IMAGE_DESC_TO_ALL_IMAGES_CONFIRM');
$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));
}
public function ajax_generate_caption()
{
set_time_limit(300); // 5 Minutes
ini_set('memory_limit', '-1');
$fullURL = Uri::root() . Factory::getApplication()->input->getString('image');
$imageToText = new \NRFramework\AI\TextGeneration\ImageToText();
$generated = $imageToText->generate($fullURL);
echo json_encode($generated);
}
}

View File

@ -0,0 +1,320 @@
<?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\Uri\Uri;
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' => '.jpg, .jpeg, .png, .webp, 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' => [],
// Open AI API Key
'openai_api_key' => '',
// The widget name
'widget' => 'GalleryManager2'
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
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';
}
// Get the Open AI API key
$this->options['openai_api_key'] = \NRFramework\Helpers\Settings::getValue('openai_api_key');
// 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');
Text::script('NR_GENERATE_IMAGE_DESC_TO_ALL_IMAGES_CONFIRM');
$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]);
}
public function ajax_generate_caption()
{
set_time_limit(300); // 5 Minutes
ini_set('memory_limit', '-1');
$fullURL = Uri::root() . Factory::getApplication()->input->getString('image');
$imageToText = new \NRFramework\AI\TextGeneration\ImageToText();
$generated = $imageToText->generate($fullURL);
echo json_encode($generated);
}
/**
* 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));
}
}

View File

@ -0,0 +1,30 @@
<?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]);
}
}

View File

@ -0,0 +1,125 @@
<?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]);
}
}

View File

@ -0,0 +1,278 @@
<?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' => '',
// Set whether to replace marker label/description smart tags
'replaceSmartTags' => false
];
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;
}
$st = new \NRFramework\SmartTags();
foreach ($this->options['markers'] as &$marker)
{
if (isset($marker['label']) && $marker['label'])
{
$marker['label'] = Text::_($marker['label']);
// Replace Smart tags
if ($this->options['replaceSmartTags'])
{
$marker['label'] = $st->replace($marker['label']);
}
}
if (isset($marker['description']) && $marker['description'])
{
$marker['description'] = Text::_($marker['description']);
$marker['description'] = \Joomla\CMS\HTML\HTMLHelper::_('content.prepare', $marker['description'], null, 'tassos.widget.map');
// Replace Smart tags
if ($this->options['replaceSmartTags'])
{
$marker['description'] = $st->replace($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
{
if (!defined('nrJ4'))
{
\JLoader::register('ContactHelperRoute', JPATH_SITE . '/components/com_contact/helpers/route.php');
}
$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']);
}
}
}

View File

@ -0,0 +1,171 @@
<?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();
}
}

View File

@ -0,0 +1,177 @@
<?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']);
}
}

View File

@ -0,0 +1,108 @@
<?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']);
}
}

View File

@ -0,0 +1,145 @@
<?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']);
}
}

View File

@ -0,0 +1,43 @@
<?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']);
}
}

View File

@ -0,0 +1,65 @@
<?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'];
}
}

View File

@ -0,0 +1,62 @@
<?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'];
}
}

View File

@ -0,0 +1,77 @@
<?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']);
}
}

View File

@ -0,0 +1,92 @@
<?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';
}
}
}

View File

@ -0,0 +1,292 @@
<?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;
}
$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']);
// 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);
}
}

View File

@ -0,0 +1,113 @@
<?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']);
}
}

View File

@ -0,0 +1,152 @@
<?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']);
}
}

View File

@ -0,0 +1,209 @@
<?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]);
}
}

View File

@ -0,0 +1,161 @@
<?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']);
}
}