first commit

This commit is contained in:
2026-02-08 21:16:11 +01:00
commit e17b7026fd
8881 changed files with 1160453 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2018 Tassos Marinos 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,536 @@
<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
/**
* 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
*/
// Alignment
'align' => '',
// Padding
'padding' => null,
// Margin
'margin' => null,
// 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,
// 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' => '',
// Custom CSS printed after the widget assets
'custom_css' => '.foo {}',
// Preview HTML used prior to JS initializing the Countdown
'preview_html' => ''
];
/**
* Class constructor
*
* @param array $options
*/
public function __construct($options = [])
{
parent::__construct($options);
\JText::script('NR_AND_LC');
$this->prepare();
if ($this->options['theme'] !== 'custom')
{
$this->setCSSVars();
$this->setResponsiveCSS();
}
}
/**
* Prepares the countdown.
*
* @return void
*/
private function prepare()
{
$this->setCSSVars();
$this->options['css_class'] .= ' is-preview ' . $this->options['theme'] . ' ' . $this->options['align'];
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(\JFactory::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);
}
/**
* Set widget CSS vars
*
* @return mixed
*/
private function setCSSVars()
{
if (!$this->options['load_css_vars'])
{
return;
}
$atts = [];
if (!empty($this->options['digits_wrapper_background_color']))
{
$atts['digits-background-color'] = $this->options['digits_wrapper_background_color'];
}
if (!empty($this->options['background_color']))
{
$atts['background-color'] = $this->options['background_color'];
}
if (!empty($this->options['item_background_color']))
{
$atts['item-background-color'] = $this->options['item_background_color'];
}
if (!empty($this->options['unit_label_text_color']))
{
$atts['unit-label-text-color'] = $this->options['unit_label_text_color'];
}
if (!empty($this->options['digit_background_color']))
{
$atts['digit-background-color'] = $this->options['digit_background_color'];
}
if (!empty($this->options['digit_text_color']))
{
$atts['digit-text-color'] = $this->options['digit_text_color'];
}
if (!empty($this->options['unit_label_margin_top']))
{
$atts['unit-label-margin-top'] = $this->options['unit_label_margin_top'] . 'px';
}
if (!empty($this->options['digits_wrapper_min_width']))
{
$atts['digits-wrapper-min-width'] = $this->options['digits_wrapper_min_width'] . 'px';
}
if (!empty($this->options['digit_min_width']))
{
$atts['digit-min-width'] = $this->options['digit_min_width'] . 'px';
}
if (!empty($this->options['digits_font_weight']))
{
$atts['digits-font-weight'] = $this->options['digits_font_weight'];
}
if (!empty($this->options['label_font_weight']))
{
$atts['label-font-weight'] = $this->options['label_font_weight'];
}
if (!empty($this->options['item_border_width']) && !empty($this->options['item_border_style']) && !empty($this->options['item_border_color']))
{
$atts['item-border'] = $this->options['item_border_width'] . 'px ' . $this->options['item_border_style'] . ' ' . $this->options['item_border_color'];
}
if (empty($atts))
{
return;
}
if (!$css = \NRFramework\Helpers\CSS::cssVarsToString($atts, '.nrf-countdown.' . $this->options['id']))
{
return;
}
$this->options['custom_css'] = $css;
}
/**
* Sets the CSS for the responsive settings.
*
* @return void
*/
private function setResponsiveCSS()
{
$initial_breakpoints = [
'desktop' => [],
'tablet' => [],
'mobile' => []
];
$responsive_css = $initial_breakpoints;
// Add digits wrapper padding
if ($digits_wrapper_padding = \NRFramework\Helpers\Controls\Spacing::getResponsiveSpacingControlValue($this->options['digits_wrapper_padding'], '--digits-padding', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $digits_wrapper_padding);
}
// Add widget padding
if ($padding = \NRFramework\Helpers\Controls\Spacing::getResponsiveSpacingControlValue($this->options['padding'], 'padding', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $padding);
}
// Add widget margin
if ($margin = \NRFramework\Helpers\Controls\Spacing::getResponsiveSpacingControlValue($this->options['margin'], 'margin', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $margin);
}
// Add gap
if ($gap = \NRFramework\Helpers\Controls\Responsive::getResponsiveControlValue($this->options['gap'], '--gap', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $gap);
}
// Add digits gap
if ($gap = \NRFramework\Helpers\Controls\Responsive::getResponsiveControlValue($this->options['digits_gap'], '--digits-gap', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $gap);
}
// Add Item Size
if ($item_size = \NRFramework\Helpers\Controls\Responsive::getResponsiveControlValue($this->options['item_size'], '--item-size', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $item_size);
}
// Add Digits Font Size
if ($digits_font_size = \NRFramework\Helpers\Controls\Responsive::getResponsiveControlValue($this->options['digits_font_size'], '--digits-font-size', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $digits_font_size);
}
// Add Label Font Size
if ($label_font_size = \NRFramework\Helpers\Controls\Responsive::getResponsiveControlValue($this->options['label_font_size'], '--label-font-size', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $label_font_size);
}
// Add Digits Padding
if ($digitsPadding = \NRFramework\Helpers\Controls\Spacing::getResponsiveSpacingControlValue($this->options['digits_padding'], '--digit-padding', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $digitsPadding);
}
// Add item border radius
if ($itemBorderRadius = \NRFramework\Helpers\Controls\BorderRadius::getResponsiveSpacingControlValue($this->options['item_border_radius'], '--item-border-radius', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $itemBorderRadius);
}
// Add digits wrapper border radius
if ($borderRadius = \NRFramework\Helpers\Controls\BorderRadius::getResponsiveSpacingControlValue($this->options['digits_wrapper_border_radius'], '--digits-border-radius', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $borderRadius);
}
// Add digits border radius
if ($borderRadius = \NRFramework\Helpers\Controls\BorderRadius::getResponsiveSpacingControlValue($this->options['digit_border_radius'], '--digit-border-radius', 'px'))
{
$responsive_css = array_merge_recursive($responsive_css, $borderRadius);
}
if ($css = \NRFramework\Helpers\Responsive::renderResponsiveCSS($responsive_css, '.nrf-countdown.' . $this->options['id']))
{
$this->options['custom_css'] .= $css;
}
}
/**
* 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;
}
}

View File

@@ -0,0 +1,973 @@
<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 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 \NRFramework\Helpers\Widgets\Gallery as GalleryHelper;
use NRFramework\Mimes;
use NRFramework\File;
use NRFramework\Image;
/**
* Gallery
*/
class Gallery extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The gallery items source.
*
* This can be one or combination of the following:
*
* - Path to a relative folder (String)
* /path/to/folder
* - Path to a relative image (String)
* /path/to/folder/image.png
* - URL of an image (String)
* https://example.com/path/to/image.png
* - Array of images (Array)
* [
* 'url' => 'https://example.com/path/to/image.png',
* 'thumbnail_url' => 'https://example.com/path/to/image_thumb.png',
* 'caption' => 'This is a caption',
* 'thumbnail_size' => [
* 'width' => '200',
* 'height' => '200'
* ],
* 'module' => 'position-2'
* ]
*
* - The `url` property is required.
* - All other properties are optional.
*/
'items' => [],
/**
* Set the ordering.
*
* Available values:
* - default
* - alphabetical
* - reverse_alphabetical
* - random
*/
'ordering' => 'default',
// Set the module key to display whenever we are viewing a single item's lightbox, appearing after the image
'module' => '',
// Set the style of the gallery (masonry, grid)
'style' => 'masonry',
/**
* Define the columns per supported device.
*
* Example value:
* - An integer representing the columns for all devices: 3
* - A value for each device:
* [
* 'desktop' => 3,
* 'tablet' => 2,
* 'mobile' => 1
* ]
*/
'columns' => 4,
/**
* Define the gap per gallery item per supported device.
*
* Example value:
* - An integer representing the gap for all devices: 30
* - A value for each device:
* [
* 'desktop' => 30,
* 'tablet' => 20,
* 'mobile' => 10
* ]
*/
'gap' => 15,
/**
* Set the allowed file types.
*
* This is used to validate the files loaded via a directory or a fixed path to an image.
*
* Given URLs are not validated by this setting.
*/
'allowed_file_types' => '.jpg, .jpeg, .png',
// Gallery Items wrapper CSS classes
'gallery_items_css' => '',
// Set whether to display a lightbox
'lightbox' => true,
/**
* Source Image
*/
/**
* Should the source image be resized?
*
* If `original_image_resize` is false, then the source image will appear
* in the lightbox (also if `thumbnails` is false, the source image will also appear as the thumbnail)
*
* Issue: if this image is a raw photo, there are chances it will increase the page load in order for the browser to display the image.
*
* By enabling this, we resize the source image to our desired dimensions and reduce the page load in the above scenario.
*
* Note: Always ensure the source image is backed up to a safe place.
* Note 2: We require thumbnails or original image resize to be enabled for this to work.
* Reason: The above options if enabled generate the gallery_info.txt file in the /cache folder which helps us
* generate the source images only if necessary(image has been edited), otherwise, the source image would
* be generated on each page refresh.
*/
'source_image_resize' => false,
// Source image resize width
'source_image_resize_width' => 1920,
// Source image resize height
'source_image_resize_height' => null,
// Source image resize method (crop, stretch, fit)
'source_image_resize_method' => 'crop',
// Source image resize quality
'source_image_resize_image_quality' => 80,
/**
* Original Image
*/
// Should the original uploaded image be resized?
'original_image_resize' => false,
// Resize method (crop, stretch, fit)
'original_image_resize_method' => 'crop',
/**
* Original Image Resize Width.
*
* If `original_image_resize_height` is null, resizes via the width to keep the aspect ratio.
*/
'original_image_resize_width' => 1920,
// Original Image Resize Height
'original_image_resize_height' => null,
// Original Image Resize Quality
'original_image_resize_image_quality' => 80,
/**
* Thumbnails
*/
// Set whether to generate thumbnails on-the-fly
'thumbnails' => false,
// Resize method (crop, stretch, fit)
'thumb_resize_method' => 'crop',
// Resize quality
'thumb_resize_quality' => 80,
// Thumbnails width
'thumb_width' => 300,
// Thumbnails height
'thumb_height' => null,
// The CSS class of the thumbnail
'thumb_class' => '',
/**
* Set whether to resize the images whenever their source file changes.
*
* i.e. If we edit the source image and also need to recreate the resized original image or thumbnail.
* This is rather useful otherwise we would have to delete the resized image or thumbnail in order for it to be recreated.
*/
'force_resizing' => false,
// Destination folder
'destination_folder' => 'cache/tassos/gallery',
// The unique hash of this gallery based on its options
'hash' => null,
// Set whether to show warnings when an image that has been set to appear does not exist.
'show_warnings' => true
];
public function __construct($options = [])
{
parent::__construct($options);
$this->prepare();
}
/**
* Prepares the Gallery.
*
* @return void
*/
private function prepare()
{
$this->options['hash'] = $this->getHash();
$this->options['destination_folder'] = JPATH_ROOT . DIRECTORY_SEPARATOR . $this->options['destination_folder'] . DIRECTORY_SEPARATOR . $this->options['hash'] . DIRECTORY_SEPARATOR;
$this->parseGalleryItems();
$this->cleanDestinationFolder();
$this->resizeSourceImages();
$this->resizeOriginalImages();
$this->createThumbnails();
// Set style on the gallery items container.
$this->options['gallery_items_css'] .= ' ' . $this->getStyle();
// Set class to trigger lightbox.
if ($this->options['lightbox'])
{
$this->options['css_class'] .= ' lightbox';
}
$this->prepareItems();
$this->setCSSVariables();
$this->setOrdering();
}
/**
* 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']));
}
/**
* Get the hash of this gallery.
*
* Generate the hash with only the essential options of the Gallery widget.
* i.e. with the data that are related to the images.
*
* @return string
*/
private function getHash()
{
$opts = [
'items',
'style',
'allowed_file_types',
'source_image_resize',
'source_image_resize_width',
'source_image_resize_height',
'source_image_resize_method',
'source_image_resize_image_quality',
'original_image_resize',
'original_image_resize_method',
'original_image_resize_width',
'original_image_resize_height',
'original_image_resize_image_quality',
'thumbnails',
'thumb_resize_method',
'thumb_resize_quality',
'thumb_width',
'thumb_height',
'force_resizing',
'destination_folder'
];
$payload = [];
foreach ($opts as $opt)
{
$payload[$opt] = $this->options[$opt];
}
return md5(serialize($payload));
}
/**
* Cleans the source folder.
*
* If an image from the source folder is removed, we also remove the
* original image/thumbnail from the destination folder as well as
* from the gallery info file.
*
* @return void
*/
private function cleanDestinationFolder()
{
if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
{
return;
}
// Find all folders that we need to search
$dirs_to_search = [];
// Store all source files
$source_files = [];
foreach ($this->options['items'] as $key => $item)
{
if (!isset($item['path']))
{
continue;
}
$source_files[] = pathinfo($item['path'], PATHINFO_BASENAME);
$directory = is_dir($item['path']) ? $item['path'] : dirname($item['path']);
if (in_array($directory, $dirs_to_search))
{
continue;
}
$dirs_to_search[] = $directory;
}
if (empty($dirs_to_search))
{
return;
}
// Loop each directory found and check which files we need to delete
foreach ($dirs_to_search as $dir)
{
$source_folder_info_file = GalleryHelper::getGalleryInfoFileData($dir);
// Find all soon to be deleted files
$to_be_deleted = array_diff(array_keys($source_folder_info_file), $source_files);
if (!count($to_be_deleted))
{
continue;
}
foreach ($to_be_deleted as $source)
{
// Original image delete
if (isset($source_folder_info_file[$source]))
{
$file = $this->options['destination_folder'] . $source_folder_info_file[$source]['filename'];
if (file_exists($file))
{
unlink($file);
}
}
// Thumbnail delete
$parts = pathinfo($file);
$thumbnail = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
if (file_exists($thumbnail))
{
unlink($thumbnail);
}
// Also remove the image from the gallery info file.
GalleryHelper::removeImageFromGalleryInfoFile(rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $source);
}
}
}
/**
* Returns the gallery style.
*
* @return string
*/
private function getStyle()
{
$style = $this->options['style'];
// Get aspect ratio for source image, original image resized and thumbnail
$thumb_height = intval($this->options['thumb_height']);
$thumb_aspect_ratio = $thumb_height ? intval($this->options['thumb_width']) / $thumb_height : 0;
$source_image_height = intval($this->options['source_image_resize_height']);
$source_image_aspect_ratio = $source_image_height ? intval($this->options['source_image_resize_width']) / $source_image_height : 0;
$original_image_height = intval($this->options['original_image_resize_height']);
$original_image_aspect_ratio = $original_image_height ? intval($this->options['original_image_resize_width']) / $original_image_height : 0;
// Check whether the aspect ratio for thumb and lightbox image are the same and use `masonry` style
$checking_aspect_ratio = $this->options['original_image_resize'] ? $original_image_aspect_ratio : $source_image_aspect_ratio;
if ($thumb_aspect_ratio && $checking_aspect_ratio && $thumb_aspect_ratio === $checking_aspect_ratio)
{
return 'masonry';
}
/**
* If both thumbnail width & height are equal we use the `grid` style.
*/
if ($this->options['thumb_width'] === $this->options['thumb_height'])
{
$style = 'grid';
}
/**
* If the style is grid and we do not have a null or 0 thumb_height set the fade lightbox CSS Class.
*
* This CSS Class tells PhotoSwipe to use the fade transition.
*/
if ($style === 'grid' && (!is_null($this->options['thumb_height']) && $this->options['thumb_height'] !== '0'))
{
$this->options['css_class'] .= ' lightbox-fade';
}
return $style;
}
/**
* Parses the gallery items by finding all iamges to display from all
* different sources.
*
* @return void
*/
private function parseGalleryItems()
{
// If it's a string, we assume its a path to a folder and we convert it to an array.
$this->options['items'] = (array) $this->options['items'];
$items = [];
foreach ($this->options['items'] as $key => $value)
{
if (!$data = GalleryHelper::parseGalleryItems($value, $this->getAllowedFileTypes()))
{
continue;
}
$items = array_merge($items, $data);
}
// Ensure only unique image paths are used
$items = array_unique($items, SORT_REGULAR);
$this->options['items'] = $items;
}
/**
* Returns the allowed file types in an array format.
*
* @return array
*/
public function getAllowedFileTypes()
{
$types = explode(',', $this->options['allowed_file_types']);
$types = array_filter(array_map('trim', array_map('strtolower', $types)));
return $types;
}
/**
* Resizes the source images.
*
* @return mixed
*/
private function resizeSourceImages()
{
if (!$this->options['source_image_resize'])
{
return;
}
// We require either original image resize or thumbnails to be enabled
if (!$this->options['original_image_resize'] && !$this->options['thumbnails'])
{
return;
}
foreach ($this->options['items'] as $key => &$item)
{
if (!isset($item['path']))
{
continue;
}
// Skip if source does not exist
if (!is_file($item['path']))
{
continue;
}
$source = $item['path'];
// Find source image in the destination folder
if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
{
// If force resizing is disabled, continue
if (!$this->options['force_resizing'])
{
continue;
}
else
{
// If the destination image has not been edited and exists, abort
if (!$image_data['edited'] && file_exists($image_data['path']))
{
continue;
}
}
}
if (is_null($this->options['source_image_resize_height']))
{
Image::resizeAndKeepAspectRatio(
$source,
$this->options['source_image_resize_width'],
$this->options['source_image_resize_image_quality']
);
}
else
{
Image::resize(
$source,
$this->options['source_image_resize_width'],
$this->options['source_image_resize_height'],
$this->options['source_image_resize_image_quality'],
$this->options['source_image_resize_method']
);
}
}
}
/**
* Resizes the original images.
*
* @return mixed
*/
private function resizeOriginalImages()
{
if (!$this->options['original_image_resize'])
{
return;
}
// Create destination folder if missing
File::createDirs($this->options['destination_folder']);
foreach ($this->options['items'] as $key => &$item)
{
if (!isset($item['path']))
{
continue;
}
// Skip if source does not exist
if (!is_file($item['path']))
{
continue;
}
$source = $item['path'];
$unique = true;
// Path to resized image in destination folder
$destination = $this->options['destination_folder'] . basename($source);
// Find source image in the destination folder
if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
{
// If force resizing is disabled and the original image exists, set the URL of the destination image
if (!$this->options['force_resizing'] && file_exists($image_data['path']))
{
$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
continue;
}
else
{
// If the destination image has not been edited and exists, abort
if (!$image_data['edited'] && file_exists($image_data['path']))
{
$item['url'] = GalleryHelper::directoryImageToURL($image_data['path']);
continue;
}
// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
$unique = false;
// The destination path is the same resized image
$destination = $image_data['path'];
}
}
$original_image_file = is_null($this->options['original_image_resize_height'])
?
Image::resizeAndKeepAspectRatio(
$source,
$this->options['original_image_resize_width'],
$this->options['original_image_resize_image_quality'],
$destination,
$unique
)
:
Image::resize(
$source,
$this->options['original_image_resize_width'],
$this->options['original_image_resize_height'],
$this->options['original_image_resize_image_quality'],
$this->options['original_image_resize_method'],
$destination,
$unique
);
if (!$original_image_file)
{
continue;
}
// Set image URL
$item = array_merge($item, [
'url' => GalleryHelper::directoryImageToURL($original_image_file)
]);
// Update image data in Gallery Info File
GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
}
}
/**
* Creates thumbnails.
*
* If `force_resizing` is enabled, it will re-generate thumbsnails under the following cases:
*
* - If a thumbnail does not exist.
* - If the original image has been edited.
*
* @return mixed
*/
private function createThumbnails()
{
if (!$this->options['thumbnails'])
{
return false;
}
// Create destination folder if missing
File::createDirs($this->options['destination_folder']);
foreach ($this->options['items'] as $key => &$item)
{
// Skip items that do not have a path set
if (!isset($item['path']))
{
continue;
}
// Skip if source does not exist
if (!is_file($item['path']))
{
continue;
}
$source = $item['path'];
$unique = true;
$parts = pathinfo($source);
$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
// Find source image in the destination folder
if ($image_data = GalleryHelper::findSourceImageDetails($source, $this->options['destination_folder']))
{
/**
* Use the found original image path to produce the thumb file path.
*
* This is used as we have multiple files with the same which produce file names of _copy_X
* and thus the above $destination will not be valid. Instead, we use the original file name
* to find the thumbnail file.
*/
if ($this->options['original_image_resize'])
{
$parts = pathinfo($image_data['path']);
$destination = $this->options['destination_folder'] . $parts['filename'] . '_thumb.' . $parts['extension'];
}
// If force resizing is disabled and the thumbnail exists, set the URL of the destination image
if (!$this->options['force_resizing'] && file_exists($destination))
{
$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
continue;
}
else
{
// If the destination image has not been edited and exists, abort
if (!$image_data['edited'] && file_exists($destination))
{
$item['thumbnail_url'] = GalleryHelper::directoryImageToURL($destination);
continue;
}
// Since we are forcing resizing, overwrite the existing image, do not create a new unique image
$unique = false;
}
}
// Generate thumbnails
$thumb_file = is_null($this->options['thumb_height'])
?
Image::resizeAndKeepAspectRatio(
$source,
$this->options['thumb_width'],
$this->options['thumb_resize_quality'],
$destination,
$unique
)
:
Image::resize(
$source,
$this->options['thumb_width'],
$this->options['thumb_height'],
$this->options['thumb_resize_quality'],
$this->options['thumb_resize_method'],
$destination,
$unique
);
if (!$thumb_file)
{
continue;
}
// Set image thumbnail URL
$item = array_merge($item, [
'thumbnail_url' => GalleryHelper::directoryImageToURL($thumb_file)
]);
// Update image data in Gallery Info File
GalleryHelper::updateImageDataInGalleryInfoFile($source, $item);
}
}
/**
* Prepares the items.
*
* - Sets the thumbnails image dimensions.
* - Assures caption property exist.
*
* @return mixed
*/
private function prepareItems()
{
if (!is_array($this->options['items']) || !count($this->options['items']))
{
return;
}
foreach ($this->options['items'] as $key => &$item)
{
// Initialize image atts
$item['img_atts'] = '';
// Initializes caption if none given
if (!isset($item['caption']))
{
$item['caption'] = '';
}
$item['alt'] = !empty($item['caption']) ? mb_substr($item['caption'], 0, 100) : pathinfo($item['url'], PATHINFO_FILENAME);
// Ensure a thumbnail is given
if (!isset($item['thumbnail_url']))
{
// If no thumbnail is given, set it to the full image
$item['thumbnail_url'] = $item['url'];
continue;
}
// If the thumbnail size for this item is given, set the image attributes
if (isset($item['thumbnail_size']))
{
$item['img_atts'] = 'width="' . $item['thumbnail_size']['width'] . '" height="' . $item['thumbnail_size']['height'] . '"';
continue;
}
}
}
/**
* Sets CSS variables for the widget.
*
* @return void
*/
private function setCSSVariables()
{
$data = array_merge(
$this->getColumns(),
$this->getGap()
);
$css = '';
foreach ($data as $key => $value)
{
$css .= '--' . $key . ':' . $value . ';';
}
\JFactory::getDocument()->addStyleDeclaration('
.nrf-widget.' . $this->options['id'] . ' { ' . $css . ' }');
}
/**
* Returns the columns of the gallery.
*
* @return array
*/
private function getColumns()
{
if (is_string($this->options['columns']))
{
$this->options['columns'] = (int) $this->options['columns'];
}
if (!is_int($this->options['columns']) && !is_array($this->options['columns']))
{
return [];
}
$columns_defaults = [
'columns' => 4,
'tablet-columns' => 2,
'mobile-columns' => 1
];
$columns = [];
// String
if (is_int($this->options['columns']))
{
$columns = [
'columns' => $this->options['columns'],
'tablet-columns' => $this->options['columns'] !== 1 ? 2 : 1,
'mobile-columns' => 1
];
}
// Array
if (is_array($this->options['columns']))
{
if (isset($this->options['columns']['desktop']))
{
$columns['columns'] = $this->options['columns']['desktop'];
}
if (isset($this->options['columns']['tablet']))
{
$columns['tablet-columns'] = $this->options['columns']['tablet'];
}
if (isset($this->options['columns']['mobile']))
{
$columns['mobile-columns'] = $this->options['columns']['mobile'];
}
}
return array_merge($columns_defaults, $columns);
}
/**
* Returns the gaps of the gallery.
*
* @return array
*/
private function getGap()
{
if (is_string($this->options['gap']))
{
$this->options['gap'] = (int) $this->options['gap'];
}
if (!is_int($this->options['gap']) && !is_array($this->options['gap']))
{
return [];
}
$gap_defaults = [
'gap' => 0,
'tablet-gap' => 0,
'mobile-gap' => 0
];
$gaps = [];
// String
if (is_int($this->options['gap']))
{
$gaps = [
'gap' => $this->options['gap'] . 'px',
'tablet-gap' => $this->options['gap'] . 'px',
'mobile-gap' => $this->options['gap'] . 'px'
];
}
// Array
if (is_array($this->options['gap']))
{
if (isset($this->options['gap']['desktop']))
{
$gaps['gap'] = $this->options['gap']['desktop'] . 'px';
}
if (isset($this->options['gap']['tablet']))
{
$gaps['tablet-gap'] = $this->options['gap']['tablet'] . 'px';
}
if (isset($this->options['gap']['mobile']))
{
$gaps['mobile-gap'] = $this->options['gap']['mobile'] . 'px';
}
}
return array_merge($gap_defaults, $gaps);
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 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\Registry\Registry;
use NRFramework\Helpers\Widgets\GalleryManager as GalleryManagerHelper;
/**
* Gallery Manager
*/
class GalleryManager extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
// The input name
'name' => '',
// The field ID associated to this Gallery Manager, used to retrieve the field settings on AJAX actions
'field_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',
/**
* Original Image
*/
// Should the original uploaded image be resized?
'original_image_resize' => false,
// Original Image Resize Quality
'original_image_resize_quality' => 80,
// Main image width
'original_image_resize_width' => 1920,
/**
* Thumbnails
*/
// Thumbnails width
'thumb_width' => 300,
// Thumbnails height
'thumb_height' => null,
// Thumbnails resize method (crop, stretch, fit)
'thumb_resize_method' => 'crop',
// Thumbnails resize quality
'thumb_resize_quality' => 80
];
public function __construct($options = [])
{
parent::__construct($options);
// Set gallery items
$this->options['gallery_items'] = is_array($this->options['value']) ? $this->options['value'] : [];
// Set css class for readonly state
if ($this->options['readonly'])
{
$this->options['css_class'] .= ' readonly';
}
// Adds a css class when the gallery contains at least one item
if (count($this->options['gallery_items']))
{
$this->options['css_class'] .= ' dz-has-items';
}
// Load translation strings
\JText::script('NR_GALLERY_MANAGER_CONFIRM_REGENERATE_THUMBNAILS');
\JText::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL_SELECTED');
\JText::script('NR_GALLERY_MANAGER_CONFIRM_DELETE_ALL');
\JText::script('NR_GALLERY_MANAGER_CONFIRM_DELETE');
\JText::script('NR_GALLERY_MANAGER_FILE_MISSING');
\JText::script('NR_GALLERY_MANAGER_REACHED_FILES_LIMIT');
}
/**
* The upload task called by the AJAX hanler
*
* @return void
*/
protected function ajax_upload()
{
$input = \JFactory::getApplication()->input;
// Make sure we have a valid field id
if (!$field_id = $input->getInt('field_id'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_FIELD_ID_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 = \NRFramework\Helpers\CustomField::getData($field_id))
{
$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;
}
$uploadSettings = [
'allow_unsafe' => false,
'allowed_types' => $field_data->get('allowed_file_types', $this->widget_options['allowed_file_types'])
];
// resize image settings
$resizeSettings = [
'thumb_width' => $field_data->get('thumb_width', 300),
// Send the height only if grid is selected. We do not need it for masonry style
'thumb_height' => $field_data->get('style', 'masonry') === 'grid' ? $field_data->get('thumb_height', null) : null,
'thumb_resize_method' => $field_data->get('thumb_resize_method', 'crop'),
'thumb_resize_quality' => $field_data->get('thumb_resize_quality', 80),
'original_image_resize' => ($field_data->get('original_image_resize', false)),
'original_image_resize_width' => $field_data->get('original_image_resize_width', 1920),
'original_image_resize_quality' => $field_data->get('original_image_resize_quality', 80)
];
// Upload the file and resize the image if needed
if (!$uploaded_filenames = GalleryManagerHelper::upload($file, $uploadSettings, $media_uploader_file_data, $resizeSettings))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_CANNOT_UPLOAD_FILE');
}
echo json_encode([
'filename' => $uploaded_filenames['filename'],
'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 = \JFactory::getApplication()->input;
// Make sure we have a valid file passed
if (!$filename = $input->getString('filename'))
{
$this->exitWithMessage('NR_GALLERY_MANAGER_ERROR_INVALID_FILE');
}
// 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');
}
// Delete the uploaded file
$deleted = GalleryManagerHelper::deleteFile($filename, $input->getString('thumbnail'));
echo json_encode(['success' => $deleted]);
}
/**
* This task allows us to regenerate the thumbnails.
*
* @return void
*/
protected function ajax_regenerate_thumbs()
{
$input = \JFactory::getApplication()->input;
// Make sure we have a valid field id
if (!$field_id = $input->getInt('field_id'))
{
echo json_encode(['success' => false, 'message' => \JText::_('NR_GALLERY_MANAGER_FIELD_ID_ERROR')]);
die();
}
if (!$field_data = \NRFramework\Helpers\CustomField::getData($field_id))
{
echo json_encode(['success' => false, 'message' => \JText::_('NR_GALLERY_MANAGER_INVALID_FIELD_DATA')]);
die();
}
$resizeSettings = [
'thumb_width' => $field_data->get('thumb_width', 300),
// Send the height only if grid is selected. We do not need it for masonry style
'thumb_height' => $field_data->get('style', 'masonry') === 'grid' ? $field_data->get('thumb_height', null) : null,
'thumb_resize_method' => $field_data->get('thumb_resize_method', 'crop'),
'thumb_resize_quality' => $field_data->get('thumb_resize_quality', 80),
];
$existing = $input->get('existing', null, 'ARRAY');
$existing = json_decode($existing[0], true);
$new = $input->get('new', null, 'ARRAY');
$new = json_decode($new[0], true);
$ds = DIRECTORY_SEPARATOR;
// Check each existing file and re-create thumbs
if (is_array($existing) && count($existing))
{
foreach ($existing as $path)
{
$_path = implode($ds, [JPATH_ROOT, $path]);
if (!file_exists($_path))
{
continue;
}
\NRFramework\Helpers\Widgets\GalleryManager::generateThumbnail($_path, $resizeSettings, null, false);
}
}
// Check each newly added file in temp folder and re-create thumbs
if (is_array($new) && count($new))
{
foreach ($new as $path)
{
$_path = implode($ds, [\NRFramework\File::getTempFolder(), $path]);
if (!file_exists($_path))
{
continue;
}
\NRFramework\Helpers\Widgets\GalleryManager::generateThumbnail($_path, $resizeSettings, null, false);
}
}
echo json_encode(['success' => true, 'message' => \JText::_('NR_GALLERY_MANAGER_THUMBS_REGENERATED')]);
}
/**
* Exits the page with given message.
*
* @param string $translation_string
*
* @return void
*/
private function exitWithMessage($translation_string)
{
http_response_code('500');
die(\JText::_($translation_string));
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2021 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
namespace NRFramework\Widgets;
defined('_JEXEC') or die;
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 = \JFolder::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;
}
}

View File

@@ -0,0 +1,155 @@
<?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;
/**
* OpenStreetMap
*/
class OpenStreetMap extends Widget
{
/**
* Widget default options
*
* @var array
*/
protected $widget_options = [
/**
* The value of the widget.
* Format: latitude,longitude
*
* i.e. 36.891319,27.283480
*/
'value' => '',
// Default map width
'width' => '500px',
// Default map height
'height' => '400px',
// Default map zoon
'zoom' => 4,
// Map scale. Values: metric, imperial, false
'scale' => false,
// View mode of the map. Values: road, aerial.
'view' => 'road',
/**
* Address input above map
*/
// Whether to show the address input above the map
'showAddressInput' => false,
/**
* Map Marker
*/
// Whether to show the marker
'showMarker' => true,
// Marker image relative to Joomla installation
'markerImage' => 'media/plg_system_nrframework/img/marker.png',
// Allows marker to be dragged
'allowMarkerDrag' => false,
// Allows map to be clicked and thus allows us to select a new location
'allowMapClick' => false,
// Whether to show the marker tooltip
'showMarkerTooltip' => false,
// Set whether to display the marker tooltip textarea.
'showMarkerTooltipInput' => false,
// Marker Tooltip Textarea Name. If a value is given, then the tooltip textarea field appears below the map
'markerTooltipName' => '',
// Marker tooltip value
'markerTooltipValue' => '',
/**
* Coordinates input below map
*/
// Whether to show the coordinates input
'showCoordsInput' => false,
// Coordinates input name. If a value is given, then the coordinates input field appears below the map
'coordsInputName' => ''
];
public function __construct($options = [])
{
parent::__construct($options);
$this->options['markerImage'] = \JURI::root() . ltrim($this->options['markerImage'], DIRECTORY_SEPARATOR);
}
/**
* Renders the widget
*
* @return string
*/
public function render()
{
self::loadMedia();
return parent::render();
}
/**
* Loads media files
*
* @return void
*/
private function loadMedia()
{
\JHtml::stylesheet('https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.9.0/css/ol.css');
\JHtml::script('https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.9.0/build/ol.js');
$this->load_geocoder();
if ($this->options['load_stylesheet'])
{
\JHtml::stylesheet('plg_system_nrframework/widgets/openstreetmap.css', ['relative' => true, 'version' => 'auto']);
}
\JHtml::script('plg_system_nrframework/widgets/openstreetmap.js', ['relative' => true, 'version' => 'auto']);
}
/**
* Checks whether geocoder is enabled and loads it.
*
* @return void
*/
private function load_geocoder()
{
if (!$this->options['showAddressInput'])
{
return;
}
$lang = \JFactory::getLanguage();
$lang_tag = $lang->getTag();
$doc = \JFactory::getDocument();
$doc->addScriptOptions('nrf_osm_settings', [
'lang_tag' => $lang_tag
]);
\JText::script('NR_OSM_ADDRESS_DESC');
\JHtml::stylesheet('https://unpkg.com/ol-geocoder/dist/ol-geocoder.min.css');
\JHtml::script('https://unpkg.com/ol-geocoder');
$this->options['css_class'] .= ' geocoder';
}
}

View File

@@ -0,0 +1,65 @@
<?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;
/**
* 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'] = (int) $this->options['value'] < $this->options['min'] ? $this->options['min'] : ((int) $this->options['value'] > $this->options['max'] ? $this->options['max'] : (int) $this->options['value']);
// Calculate bar percentage
$this->options['bar_percentage'] = $this->options['max'] ? ~~(100 * ($this->options['value'] - $this->options['min']) / ($this->options['max'] - $this->options['min'])) : $this->options['value'];
}
}

View File

@@ -0,0 +1,60 @@
<?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;
/**
* 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'] = \JURI::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,92 @@
<?php
/**
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2018 Tassos Marinos 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,155 @@
<?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;
class Widget
{
/**
* 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 default widget value
'value' => '',
// Extra attributes
'atts' => '',
// A short hint that describes the expected value
'placeholder' => '',
// The name of the layout to be used to render the widget
'layout' => 'default',
// 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, \JFactory::getApplication()->getTemplate(), 'html', 'tassos']);
$layout = new \JLayoutFile('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());
}
/**
* Manages ajax requests for the widget.
*
* @param string $task
*
* @return void
*/
public function onAjax($task)
{
\JSession::checkToken('request') or die('Invalid Token');
if (!$task || !is_string($task))
{
return;
}
$method = 'ajax_' . $task;
if (!method_exists($this, $method))
{
return;
}
$this->$method();
}
}