primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,923 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Html\FEFHelper;
defined('_JEXEC') || die;
use FOF40\Container\Container;
use FOF40\Html\SelectOptions;
use FOF40\Model\DataModel;
use FOF40\Utils\ArrayHelper;
use FOF40\View\DataView\DataViewInterface;
use FOF40\View\View;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
/**
* An HTML helper for Browse views.
*
* It reintroduces a FEF-friendly of some of the functionality found in FOF 3's Header and Field classes. These
* helpers are also accessible through Blade, making the transition from XML forms to Blade templates easier.
*
* @since 3.3.0
*/
abstract class BrowseView
{
/**
* Caches the results of getOptionsFromModel keyed by a hash. The hash is computed by the model
* name, the model state and the options passed to getOptionsFromModel.
*
* @var array
*/
private static $cacheModelOptions = [];
/**
* Get the translation key for a field's label
*
* @param string $fieldName The field name
*
* @return string
*
* @since 3.3.0
*/
public static function fieldLabelKey(string $fieldName): string
{
$view = self::getViewFromBacktrace();
try
{
$inflector = $view->getContainer()->inflector;
$viewName = $inflector->singularize($view->getName());
$altViewName = $inflector->pluralize($view->getName());
$componentName = $view->getContainer()->componentName;
$keys = [
strtoupper($componentName . '_' . $viewName . '_FIELD_' . $fieldName),
strtoupper($componentName . '_' . $altViewName . '_FIELD_' . $fieldName),
strtoupper($componentName . '_' . $viewName . '_' . $fieldName),
strtoupper($componentName . '_' . $altViewName . '_' . $fieldName),
];
foreach ($keys as $key)
{
if (Text::_($key) !== $key)
{
return $key;
}
}
return $keys[0];
}
catch (\Exception $e)
{
return ucfirst($fieldName);
}
}
/**
* Returns the label for a field (translated)
*
* @param string $fieldName The field name
*
* @return string
*/
public static function fieldLabel(string $fieldName): string
{
return Text::_(self::fieldLabelKey($fieldName));
}
/**
* Return a table field header which sorts the table by that field upon clicking
*
* @param string $field The name of the field
* @param string|null $langKey (optional) The language key for the header to be displayed
*
* @return string
*/
public static function sortgrid(string $field, ?string $langKey = null): string
{
/** @var DataViewInterface $view */
$view = self::getViewFromBacktrace();
if (is_null($langKey))
{
$langKey = self::fieldLabelKey($field);
}
return HTMLHelper::_('FEFHelp.browse.sort', $langKey, $field, $view->getLists()->order_Dir, $view->getLists()->order, $view->getTask());
}
/**
* Create a browse view filter from values returned by a model
*
* @param string $localField Field name
* @param string $modelTitleField Foreign model field for drop-down display values
* @param null $modelName Foreign model name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function modelFilter(string $localField, string $modelTitleField = 'title', ?string $modelName = null,
?string $placeholder = null, array $params = []): string
{
/** @var DataModel $model */
$model = self::getViewFromBacktrace()->getModel();
if (empty($modelName))
{
$modelName = $model->getForeignModelNameFor($localField);
}
if (is_null($placeholder))
{
$placeholder = self::fieldLabelKey($localField);
}
$params = array_merge([
'list.none' => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
'value_field' => $modelTitleField,
'fof.autosubmit' => true,
], $params);
return self::modelSelect($localField, $modelName, $model->getState($localField), $params);
}
/**
* Display a text filter (search box)
*
* @param string $localField The name of the model field. Used when getting the filter state.
* @param string $searchField The INPUT element's name. Default: "filter_$localField".
* @param string $placeholder The Text language key for the placeholder. Default: extrapolate from $localField.
* @param array $attributes HTML attributes for the INPUT element.
*
* @return string
*
* @since 3.3.0
*/
public static function searchFilter(string $localField, ?string $searchField = null, ?string $placeholder = null,
array $attributes = []): string
{
/** @var DataModel $model */
$view = self::getViewFromBacktrace();
$model = $view->getModel();
$searchField = empty($searchField) ? $localField : $searchField;
$placeholder = empty($placeholder) ? self::fieldLabelKey($localField) : $placeholder;
$attributes['type'] = $attributes['type'] ?? 'text';
$attributes['name'] = $searchField;
$attributes['id'] = !isset($attributes['id']) ? "filter_$localField" : $attributes['id'];
$attributes['placeholder'] = !isset($attributes['placeholder']) ? $view->escape(Text::_($placeholder)) : $attributes['placeholder'];
$attributes['title'] = $attributes['title'] ?? $attributes['placeholder'];
$attributes['value'] = $view->escape($model->getState($localField));
if (!isset($attributes['onchange']))
{
$attributes['class'] = trim(($attributes['class'] ?? '') . ' akeebaCommonEventsOnChangeSubmit');
$attributes['data-akeebasubmittarget'] = $attributes['data-akeebasubmittarget'] ?? 'adminForm';
}
// Remove null attributes and collapse into a string
$attributes = array_filter($attributes, function ($v) {
return !is_null($v);
});
$attributes = ArrayHelper::toString($attributes);
return "<input $attributes />";
}
/**
* Create a browse view filter with dropdown values
*
* @param string $localField Field name
* @param array $options The HTMLHelper options list to use
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function selectFilter(string $localField, array $options, ?string $placeholder = null,
array $params = []): string
{
/** @var DataModel $model */
$model = self::getViewFromBacktrace()->getModel();
if (is_null($placeholder))
{
$placeholder = self::fieldLabelKey($localField);
}
$params = array_merge([
'list.none' => '&mdash; ' . Text::_($placeholder) . ' &mdash;',
'fof.autosubmit' => true,
], $params);
return self::genericSelect($localField, $options, $model->getState($localField), $params);
}
/**
* View access dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function accessFilter(string $localField, ?string $placeholder = null, array $params = []): string
{
return self::selectFilter($localField, SelectOptions::getOptions('access', $params), $placeholder, $params);
}
/**
* Published state dropdown filter
*
* @param string $localField Field name
* @param string $placeholder Placeholder for no selection
* @param array $params Generic select display parameters
*
* @return string
*
* @since 3.3.0
*/
public static function publishedFilter(string $localField, ?string $placeholder = null, array $params = []): string
{
return self::selectFilter($localField, SelectOptions::getOptions('published', $params), $placeholder, $params);
}
/**
* Create a select box from the values returned by a model
*
* @param string $name Field name
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param mixed $currentValue The currently selected value
* @param array $params Passed to optionsFromModel and genericSelect
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
* values
*
* @return string
*
* @see self::getOptionsFromModel
* @see self::getOptionsFromSource
* @see self::genericSelect
*
* @since 3.3.0
*/
public static function modelSelect(string $name, string $modelName, $currentValue, array $params = [],
array $modelState = [], array $options = []): string
{
$params = array_merge([
'fof.autosubmit' => true,
], $params);
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
return self::genericSelect($name, $options, $currentValue, $params);
}
/**
* Get a (human readable) title from a (typically numeric, foreign key) key value using the data
* returned by a DataModel.
*
* @param string $value The key value
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param array $params Passed to getOptionsFromModel
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
* values
*
* @return string
*
* @see self::getOptionsFromModel
* @see self::getOptionsFromSource
* @see self::genericSelect
*
* @since 3.3.0
*/
public static function modelOptionName(string $value, ?string $modelName = null, array $params = [],
array $modelState = [], array $options = []): ?string
{
if (!isset($params['cache']))
{
$params['cache'] = true;
}
if (!isset($params['none_as_zero']))
{
$params['none_as_zero'] = true;
}
$options = self::getOptionsFromModel($modelName, $params, $modelState, $options);
return self::getOptionName($value, $options);
}
/**
* Gets the active option's label given an array of HTMLHelper options
*
* @param mixed $selected The currently selected value
* @param array $data The HTMLHelper options to parse
* @param string $optKey Key name, default: value
* @param string $optText Value name, default: text
* @param bool $selectFirst Should I automatically select the first option? Default: true
*
* @return mixed The label of the currently selected option
*/
public static function getOptionName($selected, array $data, string $optKey = 'value', string $optText = 'text', bool $selectFirst = true): ?string
{
$ret = null;
foreach ($data as $elementKey => &$element)
{
if (is_array($element))
{
$key = $optKey === null ? $elementKey : $element[$optKey];
$text = $element[$optText];
}
elseif (is_object($element))
{
$key = $optKey === null ? $elementKey : $element->$optKey;
$text = $element->$optText;
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
if (is_null($ret) && $selectFirst && ($selected == $key))
{
$ret = $text;
}
elseif ($selected == $key)
{
$ret = $text;
}
}
return $ret;
}
/**
* Create a generic select list based on a bunch of options. Option sources will be merged into the provided
* options automatically.
*
* Parameters:
* - format.depth The current indent depth.
* - format.eol The end of line string, default is linefeed.
* - format.indent The string to use for indentation, default is tab.
* - groups If set, looks for keys with the value "<optgroup>" and synthesizes groups from them. Deprecated.
* Default: true.
* - list.select Either the value of one selected option or an array of selected options. Default: $currentValue.
* - list.translate If true, text and labels are translated via Text::_(). Default is false.
* - list.attr HTML element attributes (key/value array or string)
* - list.none Placeholder for no selection (creates an option with an empty string key)
* - option.id The property in each option array to use as the selection id attribute. Defaults: null.
* - option.key The property in each option array to use as the Default: "value". If set to null, the index of the
* option array is used.
* - option.label The property in each option array to use as the selection label attribute. Default: null
* - option.text The property in each option array to use as the displayed text. Default: "text". If set to null,
* the option array is assumed to be a list of displayable scalars.
* - option.attr The property in each option array to use for additional selection attributes. Defaults: null.
* - option.disable: The property that will hold the disabled state. Defaults to "disable".
* - fof.autosubmit Should I auto-submit the form on change? Default: true
* - fof.formname Form to auto-submit. Default: adminForm
* - class CSS class to apply
* - size Size attribute for the input
* - multiple Is this a multiple select? Default: false.
* - required Is this a required field? Default: false.
* - autofocus Should I focus this field automatically? Default: false
* - disabled Is this a disabled field? Default: false
* - readonly Render as a readonly field with hidden inputs? Overrides 'disabled'. Default: false
* - onchange Custom onchange handler. Overrides fof.autosubmit. Default: NULL (use fof.autosubmit).
*
* @param string $name
* @param array $options
* @param mixed $currentValue
* @param array $params
*
* @return string
*
* @since 3.3.0
*/
public static function genericSelect(string $name, array $options, $currentValue, array $params = []): string
{
$params = array_merge([
'format.depth' => 0,
'format.eol' => "\n",
'format.indent' => "\t",
'groups' => true,
'list.select' => $currentValue,
'list.translate' => false,
'option.id' => null,
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
'option.attr' => null,
'option.disable' => 'disable',
'list.attr' => '',
'list.none' => '',
'id' => null,
'fof.autosubmit' => true,
'fof.formname' => 'adminForm',
'class' => '',
'size' => '',
'multiple' => false,
'required' => false,
'autofocus' => false,
'disabled' => false,
'onchange' => null,
'readonly' => false,
], $params);
$currentValue = $params['list.select'];
$classes = $params['class'] ?? '';
$classes = is_array($classes) ? implode(' ', $classes) : $classes;
// If fof.autosubmit is enabled and onchange is not set we will add our own handler
if ($params['fof.autosubmit'] && is_null($params['onchange']))
{
$formName = $params['fof.formname'] ?: 'adminForm';
$classes .= ' akeebaCommonEventsOnChangeSubmit';
$params['data-akeebasubmittarget'] = $formName;
}
// Construct SELECT element's attributes
$attr = [
'class' => trim($classes) ?: null,
'size' => ($params['size'] ?? null) ?: null,
'multiple' => ($params['multiple'] ?? null) ?: null,
'required' => ($params['required'] ?? false) ?: null,
'aria-required' => ($params['required'] ?? false) ? 'true' : null,
'autofocus' => ($params['autofocus'] ?? false) ?: null,
'disabled' => (($params['disabled'] ?? false) || ($params['readonly'] ?? false)) ?: null,
'onchange' => $params['onchange'] ?? null,
];
$attr = array_filter($attr, function ($x) {
return !is_null($x);
});
// We merge the constructed SELECT element's attributes with the 'list.attr' array, if it was provided
$params['list.attr'] = array_merge($attr, (($params['list.attr'] ?? []) ?: []));
// Merge the options with those fetched from a source (e.g. another Helper object)
$options = array_merge($options, self::getOptionsFromSource($params));
if (!empty($params['list.none']))
{
array_unshift($options, HTMLHelper::_('FEFHelp.select.option', '', Text::_($params['list.none'])));
}
$html = [];
// Create a read-only list (no name) with hidden input(s) to store the value(s).
if ($params['readonly'])
{
$html[] = HTMLHelper::_('FEFHelp.select.genericlist', $options, $name, $params);
// E.g. form field type tag sends $this->value as array
if ($params['multiple'] && is_array($currentValue))
{
if (count($currentValue) === 0)
{
$currentValue[] = '';
}
foreach ($currentValue as $value)
{
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
}
}
else
{
$html[] = '<input type="hidden" name="' . $name . '" value="' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '"/>';
}
}
else
// Create a regular list.
{
$html[] = HTMLHelper::_('FEFHelp.select.genericlist', $options, $name, $params);
}
return implode($html);
}
/**
* Replace tags that reference fields with their values
*
* @param string $text Text to process
* @param DataModel $item The DataModel instance to get values from
*
* @return string Text with tags replace
*
* @since 3.3.0
*/
public static function parseFieldTags(string $text, DataModel $item): string
{
$ret = $text;
if (empty($item))
{
return $ret;
}
/**
* Replace [ITEM:ID] in the URL with the item's key value (usually: the auto-incrementing numeric ID)
*/
$replace = $item->getId();
$ret = str_replace('[ITEM:ID]', $replace, $ret);
// Replace the [ITEMID] in the URL with the current Itemid parameter
$ret = str_replace('[ITEMID]', $item->getContainer()->input->getInt('Itemid', 0), $ret);
// Replace the [TOKEN] in the URL with the Joomla! form token
$ret = str_replace('[TOKEN]', $item->getContainer()->platform->getToken(true), $ret);
// Replace other field variables in the URL
$data = $item->getData();
foreach ($data as $field => $value)
{
// Skip non-processable values
if (is_array($value) || is_object($value))
{
continue;
}
$search = '[ITEM:' . strtoupper($field) . ']';
$ret = str_replace($search, $value, $ret);
}
return $ret;
}
/**
* Get the FOF View from the backtrace of the static call. MAGIC!
*
* @return View
*
* @since 3.3.0
*/
public static function getViewFromBacktrace(): View
{
// In case we are on a brain-dead host
if (!function_exists('debug_backtrace'))
{
throw new \RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
}
/**
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
* will expand my search by another 2 entries and so on until I either find a container or I stop
* finding new call stack entries.
*/
$lastNumberOfEntries = 0;
$limit = 4;
$skip = 0;
$container = null;
while (true)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
if (count($backtrace) === $lastNumberOfEntries)
{
throw new \RuntimeException(__METHOD__ . ": Cannot retrieve FOF View from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
$lastNumberOfEntries = count($backtrace);
if ($skip)
{
$backtrace = array_slice($backtrace, $skip);
}
foreach ($backtrace as $bt)
{
if (!isset($bt['object']))
{
continue;
}
if ($bt['object'] instanceof View)
{
return $bt['object'];
}
}
$skip = $limit;
$limit += 2;
}
}
/**
* Get HTMLHelper options from an alternate source, e.g. a helper. This is useful for adding arbitrary options
* which are either dynamic or you do not want to inline to your view, e.g. reusable options across
* different views.
*
* The attribs can be:
* source_file The file to load. You can use FOF's URIs such as 'admin:com_foobar/foo/bar'
* source_class The class to use
* source_method The static method to use on source_class
* source_key Use * if you're returning a key/value array. Otherwise the array key for the key (ID)
* value.
* source_value Use * if you're returning a key/value array. Otherwise the array key for the displayed
* value. source_translate Should I pass the value field through Text? Default: true source_format Set
* to "optionsobject" if you're returning an array of HTMLHelper options. Ignored otherwise.
*
* @param array $attribs
*
* @return array
*
* @since 3.3.0
*/
private static function getOptionsFromSource(array $attribs = []): array
{
$options = [];
$container = self::getContainerFromBacktrace();
$attribs = array_merge([
'source_file' => '',
'source_class' => '',
'source_method' => '',
'source_key' => '*',
'source_value' => '*',
'source_translate' => true,
'source_format' => '',
], $attribs);
$source_file = $attribs['source_file'];
$source_class = $attribs['source_class'];
$source_method = $attribs['source_method'];
$source_key = $attribs['source_key'];
$source_value = $attribs['source_value'];
$source_translate = $attribs['source_translate'];
$source_format = $attribs['source_format'];
if ($source_class && $source_method)
{
// Maybe we have to load a file?
if (!empty($source_file))
{
$source_file = $container->template->parsePath($source_file, true);
if ($container->filesystem->fileExists($source_file))
{
include $source_file;
}
}
// Make sure the class exists
// ...and so does the option
if (class_exists($source_class, true) && in_array($source_method, get_class_methods($source_class)))
{
// Get the data from the class
if ($source_format == 'optionsobject')
{
$options = array_merge($options, $source_class::$source_method());
}
else
{
$source_data = $source_class::$source_method();
// Loop through the data and prime the $options array
foreach ($source_data as $k => $v)
{
$key = (empty($source_key) || ($source_key == '*')) ? $k : @$v[$source_key];
$value = (empty($source_value) || ($source_value == '*')) ? $v : @$v[$source_value];
if ($source_translate)
{
$value = Text::_($value);
}
$options[] = HTMLHelper::_('FEFHelp.select.option', $key, $value, 'value', 'text');
}
}
}
}
reset($options);
return $options;
}
/**
* Get HTMLHelper options from the values returned by a model.
*
* The params can be:
* key_field The model field used for the OPTION's key. Default: the model's ID field.
* value_field The model field used for the OPTION's displayed value. You must provide it.
* apply_access Should I apply Joomla ACLs to the model? Default: FALSE.
* none Placeholder for no selection. Default: NULL (no placeholder).
* none_as_zero When true, the 'none' placeholder applies to values '' **AND** '0' (empty string and zero)
* translate Should I pass the values through Text? Default: TRUE.
* with Array of relation names for eager loading.
* cache Cache the results for faster reuse
*
* @param string $modelName The name of the model, e.g. "items" or "com_foobar.items"
* @param array $params Parameters which define which options to get from the model
* @param array $modelState Optional state variables to pass to the model
* @param array $options Any HTMLHelper select options you want to add in front of the model's returned
* values
*
* @return mixed
*
* @since 3.3.0
*/
private static function getOptionsFromModel(string $modelName, array $params = [], array $modelState = [],
array $options = []): array
{
// Let's find the FOF DI container from the call stack
$container = self::getContainerFromBacktrace();
// Explode model name into component name and prefix
$componentName = $container->componentName;
$mName = $modelName;
if (strpos($modelName, '.') !== false)
{
[$componentName, $mName] = explode('.', $mName, 2);
}
if ($componentName !== $container->componentName)
{
$container = Container::getInstance($componentName);
}
/** @var DataModel $model */
$model = $container->factory->model($mName)->setIgnoreRequest(true)->savestate(false);
$defaultParams = [
'key_field' => $model->getKeyName(),
'value_field' => 'title',
'apply_access' => false,
'none' => null,
'none_as_zero' => false,
'translate' => true,
'with' => [],
];
$params = array_merge($defaultParams, $params);
$cache = isset($params['cache']) && $params['cache'];
$cacheKey = null;
if ($cache)
{
$cacheKey = sha1(print_r([
$model->getContainer()->componentName,
$model->getName(),
$params['key_field'],
$params['value_field'],
$params['apply_access'],
$params['none'],
$params['translate'],
$params['with'],
$modelState,
], true));
}
if ($cache && isset(self::$cacheModelOptions[$cacheKey]))
{
return self::$cacheModelOptions[$cacheKey];
}
if (empty($params['none']) && !is_null($params['none']))
{
$langKey = strtoupper($model->getContainer()->componentName . '_TITLE_' . $model->getName());
$placeholder = Text::_($langKey);
if ($langKey !== $placeholder)
{
$params['none'] = '&mdash; ' . $placeholder . ' &mdash;';
}
}
if (!empty($params['none']))
{
$options[] = HTMLHelper::_('FEFHelp.select.option', null, Text::_($params['none']));
if ($params['none_as_zero'])
{
$options[] = HTMLHelper::_('FEFHelp.select.option', 0, Text::_($params['none']));
}
}
if ($params['apply_access'])
{
$model->applyAccessFiltering();
}
if (!is_null($params['with']))
{
$model->with($params['with']);
}
// Set the model's state, if applicable
foreach ($modelState as $stateKey => $stateValue)
{
$model->setState($stateKey, $stateValue);
}
// Set the query and get the result list.
$items = $model->get(true);
foreach ($items as $item)
{
$value = $item->{$params['value_field']};
if ($params['translate'])
{
$value = Text::_($value);
}
$options[] = HTMLHelper::_('FEFHelp.select.option', $item->{$params['key_field']}, $value);
}
if ($cache)
{
self::$cacheModelOptions[$cacheKey] = $options;
}
return $options;
}
/**
* Get the FOF DI container from the backtrace of the static call. MAGIC!
*
* @return Container
*
* @since 3.3.0
*/
private static function getContainerFromBacktrace(): Container
{
// In case we are on a brain-dead host
if (!function_exists('debug_backtrace'))
{
throw new \RuntimeException("Your host has disabled the <code>debug_backtrace</code> PHP function. Please ask them to re-enable it. It's required for running this software.");
}
/**
* For performance reasons I look into the last 4 call stack entries. If I don't find a container I
* will expand my search by another 2 entries and so on until I either find a container or I stop
* finding new call stack entries.
*/
$lastNumberOfEntries = 0;
$limit = 4;
$skip = 0;
$container = null;
while (true)
{
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit);
if (count($backtrace) === $lastNumberOfEntries)
{
throw new \RuntimeException(__METHOD__ . ": Cannot retrieve FOF container from call stack. You are either calling me from a non-FEF extension or your PHP is broken.");
}
$lastNumberOfEntries = count($backtrace);
if ($skip !== 0)
{
$backtrace = array_slice($backtrace, $skip);
}
foreach ($backtrace as $bt)
{
if (!isset($bt['object']))
{
continue;
}
if (!method_exists($bt['object'], 'getContainer'))
{
continue;
}
return $bt['object']->getContainer();
}
$skip = $limit;
$limit += 2;
}
}
}

View File

@ -0,0 +1,867 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use FOF40\Html\FEFHelper\BrowseView;
use FOF40\Model\DataModel;
use FOF40\Utils\ArrayHelper;
use FOF40\View\DataView\DataViewInterface;
use FOF40\View\DataView\Raw as DataViewRaw;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Pagination\Pagination;
/**
* Custom JHtml (HTMLHelper) class. Offers browse view controls compatible with Akeeba Frontend
* Framework (FEF).
*
* Call these methods as HTMLHelper::_('FEFHelp.browse.methodName', $parameter1, $parameter2, ...)
*
* @noinspection PhpIllegalPsrClassPathInspection
*/
abstract class FEFHelpBrowse
{
/**
* Returns an action button on the browse view's table
*
* @param integer $i The row index
* @param string $task The task to fire when the button is clicked
* @param string|array $prefix An optional task prefix or an array of options
* @param string $active_title An optional active tooltip to display if $enable is true
* @param string $inactive_title An optional inactive tooltip to display if $enable is true
* @param boolean $tip An optional setting for tooltip
* @param string $active_class An optional active HTML class
* @param string $inactive_class An optional inactive HTML class
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function action(int $i, string $task, $prefix = '', string $active_title = '',
string $inactive_title = '', bool $tip = false,
string $active_class = '', string $inactive_class = '',
bool $enabled = true, bool $translate = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$active_title = array_key_exists('active_title', $options) ? $options['active_title'] : $active_title;
$inactive_title = array_key_exists('inactive_title', $options) ? $options['inactive_title'] : $inactive_title;
$tip = array_key_exists('tip', $options) ? $options['tip'] : $tip;
$active_class = array_key_exists('active_class', $options) ? $options['active_class'] : $active_class;
$inactive_class = array_key_exists('inactive_class', $options) ? $options['inactive_class'] : $inactive_class;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
if ($tip)
{
$title = $enabled ? $active_title : $inactive_title;
$title = $translate ? Text::_($title) : $title;
$title = HTMLHelper::_('tooltipText', $title, '', 0);
}
if ($enabled)
{
$btnColor = 'grey';
if (substr($active_class, 0, 2) == '--')
{
[$btnColor, $active_class] = explode(' ', $active_class, 2);
$btnColor = ltrim($btnColor, '-');
}
$html = [];
$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini ' . ($active_class === 'publish' ? ' active' : '') . ($tip ? ' hasTooltip' : '') . '"';
$html[] = ' href="javascript:void(0);" onclick="return Joomla.listItemTask(\'' . $checkbox . $i . '\',\'' . $prefix . $task . '\')"';
$html[] = $tip ? ' title="' . $title . '"' : '';
$html[] = '>';
$html[] = '<span class="akion-' . $active_class . '" aria-hidden="true"></span>&ensp;';
$html[] = '</a>';
return implode($html);
}
$btnColor = 'grey';
if (substr($inactive_class, 0, 2) == '--')
{
[$btnColor, $inactive_class] = explode(' ', $inactive_class, 2);
$btnColor = ltrim($btnColor, '-');
}
$html = [];
$html[] = '<a class="akeeba-btn--' . $btnColor . '--mini disabled akeebagrid' . ($tip ? ' hasTooltip' : '') . '"';
$html[] = $tip ? ' title="' . $title . '"' : '';
$html[] = '>';
if ($active_class === 'protected')
{
$inactive_class = 'locked';
}
$html[] = '<span class="akion-' . $inactive_class . '"></span>&ensp;';
$html[] = '</a>';
return implode($html);
}
/**
* Returns a state change button on the browse view's table
*
* @param array $states array of value/state. Each state is an array of the form
* (task, text, active title, inactive title, tip (boolean), HTML active class,
* HTML inactive class) or ('task'=>task, 'text'=>text, 'active_title'=>active
* title,
* 'inactive_title'=>inactive title, 'tip'=>boolean, 'active_class'=>html active
* class,
* 'inactive_class'=>html inactive class)
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param boolean $translate An optional setting for translation.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function state(array $states, int $value, int $i, $prefix = '', bool $enabled = true,
bool $translate = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$translate = array_key_exists('translate', $options) ? $options['translate'] : $translate;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$state = ArrayHelper::getValue($states, (int) $value, $states[0]);
$task = array_key_exists('task', $state) ? $state['task'] : $state[0];
$text = array_key_exists('text', $state) ? $state['text'] : (array_key_exists(1, $state) ? $state[1] : '');
$active_title = array_key_exists('active_title', $state) ? $state['active_title'] : (array_key_exists(2, $state) ? $state[2] : '');
$inactive_title = array_key_exists('inactive_title', $state) ? $state['inactive_title'] : (array_key_exists(3, $state) ? $state[3] : '');
$tip = array_key_exists('tip', $state) ? $state['tip'] : (array_key_exists(4, $state) ? $state[4] : false);
$active_class = array_key_exists('active_class', $state) ? $state['active_class'] : (array_key_exists(5, $state) ? $state[5] : '');
$inactive_class = array_key_exists('inactive_class', $state) ? $state['inactive_class'] : (array_key_exists(6, $state) ? $state[6] : '');
return static::action(
$i, $task, $prefix, $active_title, $inactive_title, $tip,
$active_class, $inactive_class, $enabled, $translate, $checkbox
);
}
/**
* Returns a published state on the browse view's table
*
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
* @param string $publish_up An optional start publishing date.
* @param string $publish_down An optional finish publishing date.
*
* @return string The HTML markup
*
* @see self::state()
*
* @since 3.3.0
*/
public static function published(int $value, int $i, $prefix = '', bool $enabled = true, string $checkbox = 'cb',
?string $publish_up = null, ?string $publish_down = null): string
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
/**
* Format:
*
* (task, text, active title, inactive title, tip (boolean), active icon class (without akion-), inactive icon class (without akion-))
*/
$states = [
1 => [
'unpublish', 'JPUBLISHED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JPUBLISHED', true, '--green checkmark',
'--green checkmark',
],
0 => [
'publish', 'JUNPUBLISHED', 'JLIB_HTML_PUBLISH_ITEM', 'JUNPUBLISHED', true, '--red close', '--red close',
],
2 => [
'unpublish', 'JARCHIVED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JARCHIVED', true, '--orange ion-ios-box',
'--orange ion-ios-box',
],
-2 => [
'publish', 'JTRASHED', 'JLIB_HTML_PUBLISH_ITEM', 'JTRASHED', true, '--dark trash-a', '--dark trash-a',
],
];
// Special state for dates
if ($publish_up || $publish_down)
{
$nullDate = JoomlaFactory::getDbo()->getNullDate();
$nowDate = JoomlaFactory::getDate()->toUnix();
$tz = JoomlaFactory::getUser()->getTimezone();
$publish_up = (!empty($publish_up) && ($publish_up != $nullDate)) ? JoomlaFactory::getDate($publish_up, 'UTC')->setTimeZone($tz) : false;
$publish_down = (!empty($publish_down) && ($publish_down != $nullDate)) ? JoomlaFactory::getDate($publish_down, 'UTC')->setTimeZone($tz) : false;
// Create tip text, only we have publish up or down settings
$tips = [];
if ($publish_up)
{
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publish_up, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
if ($publish_down)
{
$tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publish_down, Text::_('DATE_FORMAT_LC5'), 'UTC'));
}
$tip = empty($tips) ? false : implode('<br />', $tips);
// Add tips and special titles
foreach (array_keys($states) as $key)
{
// Create special titles for published items
if ($key == 1)
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_ITEM';
if (!empty($publish_up) && ($publish_up != $nullDate) && $nowDate < $publish_up->toUnix())
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_PENDING_ITEM';
$states[$key][5] = $states[$key][6] = 'android-time';
}
if (!empty($publish_down) && ($publish_down != $nullDate) && $nowDate > $publish_down->toUnix())
{
$states[$key][2] = $states[$key][3] = 'JLIB_HTML_PUBLISHED_EXPIRED_ITEM';
$states[$key][5] = $states[$key][6] = 'alert';
}
}
// Add tips to titles
if ($tip)
{
$states[$key][1] = Text::_($states[$key][1]);
$states[$key][2] = Text::_($states[$key][2]) . '<br />' . $tip;
$states[$key][3] = Text::_($states[$key][3]) . '<br />' . $tip;
$states[$key][4] = true;
}
}
return static::state($states, $value, $i, [
'prefix' => $prefix, 'translate' => !$tip,
], $enabled, true, $checkbox);
}
return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
}
/**
* Returns an isDefault state on the browse view's table
*
* @param integer $value The state value.
* @param integer $i The row index
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @see self::state()
* @since 3.3.0
*/
public static function isdefault(int $value, int $i, $prefix = '', bool $enabled = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$states = [
0 => ['setDefault', '', 'JLIB_HTML_SETDEFAULT_ITEM', '', 1, 'android-star-outline', 'android-star-outline'],
1 => [
'unsetDefault', 'JDEFAULT', 'JLIB_HTML_UNSETDEFAULT_ITEM', 'JDEFAULT', 1, 'android-star',
'android-star',
],
];
return static::state($states, $value, $i, $prefix, $enabled, true, $checkbox);
}
/**
* Returns a checked-out icon
*
* @param integer $i The row index.
* @param string $editorName The name of the editor.
* @param string $time The time that the object was checked out.
* @param string|array $prefix An optional task prefix or an array of options
* @param boolean $enabled True to enable the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function checkedout(int $i, string $editorName, string $time, $prefix = '', bool $enabled = false,
string $checkbox = 'cb'): string
{
HTMLHelper::_('bootstrap.tooltip');
if (is_array($prefix))
{
$options = $prefix;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
$text = $editorName . '<br />' . HTMLHelper::_('date', $time, Text::_('DATE_FORMAT_LC')) . '<br />' . HTMLHelper::_('date', $time, 'H:i');
$active_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKIN'), $text, 0);
$inactive_title = HTMLHelper::_('tooltipText', Text::_('JLIB_HTML_CHECKED_OUT'), $text, 0);
return static::action(
$i, 'checkin', $prefix, html_entity_decode($active_title, ENT_QUOTES, 'UTF-8'),
html_entity_decode($inactive_title, ENT_QUOTES, 'UTF-8'), true, 'locked', 'locked', $enabled, false, $checkbox
);
}
/**
* Returns the drag'n'drop reordering field for Browse views
*
* @param string $orderingField The name of the field you're ordering by
* @param string $order The order value of the current row
* @param string $class CSS class for the ordering value INPUT field
* @param string $icon CSS class for the d'n'd handle icon
* @param string $inactiveIcon CSS class for the d'n'd disabled icon
* @param DataViewInterface $view The view you're rendering against. Leave null for auto-detection.
*
* @return string
*/
public static function order(string $orderingField, ?string $order, string $class = 'input-sm',
string $icon = 'akion-android-more-vertical',
string $inactiveIcon = 'akion-android-more-vertical',
DataViewInterface $view = null): string
{
$order = $order ?? 'asc';
/** @var \FOF40\View\DataView\Html $view */
if (is_null($view))
{
$view = BrowseView::getViewFromBacktrace();
}
$dndOrderingActive = $view->getLists()->order == $orderingField;
// Default inactive ordering
$html = '<span class="sortable-handler inactive" >';
$html .= '<span class="' . $icon . '"></span>';
$html .= '</span>';
// The modern drag'n'drop method
if ($view->getPerms()->editstate)
{
$disableClassName = '';
$disabledLabel = '';
// DO NOT REMOVE! It will initialize Joomla libraries and javascript functions
$hasAjaxOrderingSupport = $view->hasAjaxOrderingSupport();
if (!is_array($hasAjaxOrderingSupport) || !$hasAjaxOrderingSupport['saveOrder'])
{
$disabledLabel = Text::_('JORDERINGDISABLED');
$disableClassName = 'inactive tip-top hasTooltip';
}
$orderClass = $dndOrderingActive ? 'order-enabled' : 'order-disabled';
$html = '<div class="' . $orderClass . '">';
$html .= '<span class="sortable-handler ' . $disableClassName . '" title="' . $disabledLabel . '">';
$html .= '<span class="' . ($disableClassName ? $inactiveIcon : $icon) . '"></span>';
$html .= '</span>';
if ($dndOrderingActive)
{
$html .= '<input type="text" name="order[]" style="display: none" size="5" class="' . $class . ' text-area-order" value="' . $order . '" />';
}
$html .= '</div>';
}
return $html;
}
/**
* Returns the drag'n'drop reordering table header for Browse views
*
* @param string $orderingField The name of the field you're ordering by
* @param string $icon CSS class for the d'n'd handle icon
*
* @return string
*/
public static function orderfield(string $orderingField = 'ordering', string $icon = 'akion-stats-bars'): string
{
$title = Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN');
$orderingLabel = Text::_('JFIELD_ORDERING_LABEL');
return <<< HTML
<a href="#"
onclick="Joomla.tableOrdering('{$orderingField}','asc','');return false;"
class="hasPopover"
title="{$orderingLabel}"
data-content="{$title}"
data-placement="top"
>
<span class="{$icon}"></span>
</a>
HTML;
}
/**
* Creates an order-up action icon.
*
* @param integer $i The row index.
* @param string $task An optional task to fire.
* @param string|array $prefix An optional task prefix or an array of options
* @param string $text An optional text to display
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function orderUp(int $i, string $task = 'orderup', $prefix = '', string $text = 'JLIB_HTML_MOVE_UP',
bool $enabled = true, string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$text = array_key_exists('text', $options) ? $options['text'] : $text;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
return static::action($i, $task, $prefix, $text, $text, false, 'arrow-up-b', 'arrow-up-b', $enabled, true, $checkbox);
}
/**
* Creates an order-down action icon.
*
* @param integer $i The row index.
* @param string $task An optional task to fire.
* @param string|array $prefix An optional task prefix or an array of options
* @param string $text An optional text to display
* @param boolean $enabled An optional setting for access control on the action.
* @param string $checkbox An optional prefix for checkboxes.
*
* @return string The HTML markup
*
* @since 3.3.0
*/
public static function orderDown(int $i, string $task = 'orderdown', string $prefix = '',
string $text = 'JLIB_HTML_MOVE_DOWN', bool $enabled = true,
string $checkbox = 'cb'): string
{
if (is_array($prefix))
{
$options = $prefix;
$text = array_key_exists('text', $options) ? $options['text'] : $text;
$enabled = array_key_exists('enabled', $options) ? $options['enabled'] : $enabled;
$checkbox = array_key_exists('checkbox', $options) ? $options['checkbox'] : $checkbox;
$prefix = array_key_exists('prefix', $options) ? $options['prefix'] : '';
}
return static::action($i, $task, $prefix, $text, $text, false, 'arrow-down-b', 'arrow-down-b', $enabled, true, $checkbox);
}
/**
* Table header for a field which changes the sort order when clicked
*
* @param string $title The link title
* @param string $order The order field for the column
* @param string $direction The current direction
* @param string $selected The selected ordering
* @param string $task An optional task override
* @param string $new_direction An optional direction for the new column
* @param string $tip An optional text shown as tooltip title instead of $title
* @param string $form An optional form selector
*
* @return string
*
* @since 3.3.0
*/
public static function sort(string $title, string $order, ?string $direction = 'asc', string $selected = '',
?string $task = null, string $new_direction = 'asc', string $tip = '',
?string $form = null): string
{
HTMLHelper::_('behavior.core');
HTMLHelper::_('bootstrap.popover');
$direction = strtolower($direction ?? 'asc');
$icon = ['akion-android-arrow-dropup', 'akion-android-arrow-dropdown'];
$index = (int) ($direction === 'desc');
if ($order !== $selected)
{
$direction = $new_direction;
}
else
{
$direction = $direction === 'desc' ? 'asc' : 'desc';
}
if ($form)
{
$form = ', document.getElementById(\'' . $form . '\')';
}
$html = '<a href="#" onclick="Joomla.tableOrdering(\'' . $order . '\',\'' . $direction . '\',\'' . $task . '\'' . $form . ');return false;"'
. ' class="hasPopover" title="' . htmlspecialchars(Text::_($tip ?: $title)) . '"'
. ' data-content="' . htmlspecialchars(Text::_('JGLOBAL_CLICK_TO_SORT_THIS_COLUMN')) . '" data-placement="top">';
if (isset($title['0']) && $title['0'] === '<')
{
$html .= $title;
}
else
{
$html .= Text::_($title);
}
if ($order === $selected)
{
$html .= '<span class="' . $icon[$index] . '"></span>';
}
return $html . '</a>';
}
/**
* Method to check all checkboxes on the browse view's table
*
* @param string $name The name of the form element
* @param string $tip The text shown as tooltip title instead of $tip
* @param string $action The action to perform on clicking the checkbox
*
* @return string
*
* @since 3.3.0
*/
public static function checkall(string $name = 'checkall-toggle', string $tip = 'JGLOBAL_CHECK_ALL',
string $action = 'Joomla.checkAll(this)'): string
{
HTMLHelper::_('behavior.core');
HTMLHelper::_('bootstrap.tooltip');
return '<input type="checkbox" name="' . $name . '" value="" class="hasTooltip" title="' . HTMLHelper::_('tooltipText', $tip)
. '" onclick="' . $action . '" />';
}
/**
* Method to create a checkbox for a grid row.
*
* @param integer $rowNum The row index
* @param mixed $recId The record id
* @param boolean $checkedOut True if item is checked out
* @param string $name The name of the form element
* @param string $stub The name of stub identifier
*
* @return mixed String of html with a checkbox if item is not checked out, empty if checked out.
*
* @since 3.3.0
*/
public static function id(int $rowNum, $recId, bool $checkedOut = false, string $name = 'cid',
string $stub = 'cb'): string
{
return $checkedOut ? '' : '<input type="checkbox" id="' . $stub . $rowNum . '" name="' . $name . '[]" value="' . $recId
. '" onclick="Joomla.isChecked(this.checked);" />';
}
/**
* Include the necessary JavaScript for the browse view's table order feature
*
* @param string $orderBy Filed by which we are currently sorting the table.
* @param bool $return Should I return the JS? Default: false (= add to the page's head)
*
* @return string
*/
public static function orderjs(string $orderBy, bool $return = false): ?string
{
$js = <<< JS
Joomla.orderTable = function()
{
var table = document.getElementById("sortTable");
var direction = document.getElementById("directionTable");
var order = table.options[table.selectedIndex].value;
var dirn = 'asc';
if (order !== '$orderBy')
{
dirn = 'asc';
}
else {
dirn = direction.options[direction.selectedIndex].value;
}
Joomla.tableOrdering(order, dirn);
};
JS;
if ($return)
{
return $js;
}
try
{
JoomlaFactory::getApplication()->getDocument()->addScriptDeclaration($js);
}
catch (Exception $e)
{
// If we have no application, well, not having table sorting JS is the least of your worries...
}
return null;
}
/**
* Returns the table ordering / pagination header for a browse view: number of records to display, order direction,
* order by field.
*
* @param DataViewRaw $view The view you're rendering against. If not provided we will guess it using
* MAGIC.
* @param array $sortFields Array of field name => description for the ordering fields in the dropdown.
* If not provided we will use all the fields available in the model.
* @param Pagination $pagination The Joomla pagination object. If not provided we fetch it from the view.
* @param string $sortBy Order by field name. If not provided we fetch it from the view.
* @param string $order_Dir Order direction. If not provided we fetch it from the view.
*
* @return string
*
* @since 3.3.0
*/
public static function orderheader(DataViewRaw $view = null, array $sortFields = [], Pagination $pagination = null,
?string $sortBy = null, ?string $order_Dir = null): string
{
if (is_null($view))
{
$view = BrowseView::getViewFromBacktrace();
}
$showBrowsePagination = $view->showBrowsePagination ?? true;
$showBrowseOrdering = $view->showBrowseOrdering ?? true;
$showBrowseOrderBy = $view->showBrowseOrderBy ?? true;
if (!$showBrowsePagination && !$showBrowseOrdering && !$showBrowseOrderBy)
{
return '';
}
if (empty($sortFields))
{
/** @var DataModel $model */
$model = $view->getModel();
$sortFields = $view->getLists()->sortFields ?? [];
$sortFields = empty($sortFields) ? self::getSortFields($model) : $sortFields;
}
if (empty($pagination))
{
$pagination = $view->getPagination();
}
if (empty($sortBy))
{
$sortBy = $view->getLists()->order;
}
if (empty($order_Dir))
{
$order_Dir = $view->getLists()->order_Dir;
if (empty($order_Dir))
{
$order_Dir = 'asc';
}
}
// Static hidden text labels
$limitLabel = Text::_('JFIELD_PLG_SEARCH_SEARCHLIMIT_DESC');
$orderingDecr = Text::_('JFIELD_ORDERING_DESC');
$sortByLabel = Text::_('JGLOBAL_SORT_BY');
// Order direction dropdown
$directionSelect = HTMLHelper::_('FEFHelp.select.genericlist', [
'' => $orderingDecr,
'asc' => Text::_('JGLOBAL_ORDER_ASCENDING'),
'desc' => Text::_('JGLOBAL_ORDER_DESCENDING'),
], 'directionTable', [
'id' => 'directionTable',
'list.select' => $order_Dir,
'list.attr' => [
'class' => 'input-medium custom-select akeebaCommonEventsOnChangeOrderTable',
],
]);
// Sort by field dropdown
$sortTable = HTMLHelper::_('FEFHelp.select.genericlist', array_merge([
'' => Text::_('JGLOBAL_SORT_BY'),
], $sortFields), 'sortTable', [
'id' => 'sortTable',
'list.select' => $sortBy,
'list.attr' => [
'class' => 'input-medium custom-select akeebaCommonEventsOnChangeOrderTable',
],
]);
$html = '';
if ($showBrowsePagination)
{
$html .= <<< HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="limit" class="element-invisible">
$limitLabel
</label>
{$pagination->getLimitBox()}
</div>
HTML;
}
if ($showBrowseOrdering)
{
$html .= <<< HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="directionTable" class="element-invisible">
$orderingDecr
</label>
$directionSelect
</div>
HTML;
}
if ($showBrowseOrderBy)
{
$html .= <<< HTML
<div class="akeeba-filter-element akeeba-form-group">
<label for="sortTable" class="element-invisible">
{$sortByLabel}
</label>
$sortTable
</div>
HTML;
}
return $html;
}
/**
* Get the default sort fields from a model. It creates a hash array where the keys are the model's field names and
* the values are the translation keys for their names, following FOF's naming conventions.
*
* @param DataModel $model The model for which we get the sort fields
*
* @return array
*
* @since 3.3.0
*/
private static function getSortFields(DataModel $model): array
{
$sortFields = [];
$idField = $model->getIdFieldName() ?: 'id';
$defaultFieldLabels = [
'publish_up' => 'JGLOBAL_FIELD_PUBLISH_UP_LABEL',
'publish_down' => 'JGLOBAL_FIELD_PUBLISH_DOWN_LABEL',
'created_by' => 'JGLOBAL_FIELD_CREATED_BY_LABEL',
'created_on' => 'JGLOBAL_FIELD_CREATED_LABEL',
'modified_by' => 'JGLOBAL_FIELD_MODIFIED_BY_LABEL',
'modified_on' => 'JGLOBAL_FIELD_MODIFIED_LABEL',
'ordering' => 'JGLOBAL_FIELD_FIELD_ORDERING_LABEL',
'id' => 'JGLOBAL_FIELD_ID_LABEL',
'hits' => 'JGLOBAL_HITS',
'title' => 'JGLOBAL_TITLE',
'user_id' => 'JGLOBAL_USERNAME',
'username' => 'JGLOBAL_USERNAME',
];
$componentName = $model->getContainer()->componentName;
$viewNameSingular = $model->getContainer()->inflector->singularize($model->getName());
$viewNamePlural = $model->getContainer()->inflector->pluralize($model->getName());
foreach (array_keys($model->getFields()) as $field)
{
$possibleKeys = [
$componentName . '_' . $viewNamePlural . '_FIELD_' . $field,
$componentName . '_' . $viewNamePlural . '_' . $field,
$componentName . '_' . $viewNameSingular . '_FIELD_' . $field,
$componentName . '_' . $viewNameSingular . '_' . $field,
];
if (array_key_exists($field, $defaultFieldLabels))
{
$possibleKeys[] = $defaultFieldLabels[$field];
}
if ($field === $idField)
{
$possibleKeys[] = $defaultFieldLabels['id'];
}
$fieldLabel = '';
foreach ($possibleKeys as $langKey)
{
$langKey = strtoupper($langKey);
$fieldLabel = Text::_($langKey);
if ($fieldLabel !== $langKey)
{
break;
}
$fieldLabel = '';
}
if (!empty($fieldLabel))
{
$sortFields[$field] = (new Joomla\Filter\InputFilter())->clean($fieldLabel);
}
}
return $sortFields;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory as JoomlaFactory;
/**
* Custom JHtml (HTMLHelper) class. Offers edit (form) view controls compatible with Akeeba Frontend
* Framework (FEF).
*
* Call these methods as HTMLHelper::_('FEFHelp.edit.methodName', $parameter1, $parameter2, ...)
*/
abstract class FEFHelpEdit
{
public static function editor(string $fieldName, ?string $value, array $params = []): string
{
$params = array_merge([
'id' => null,
'editor' => null,
'width' => '100%',
'height' => 500,
'columns' => 50,
'rows' => 20,
'created_by' => null,
'asset_id' => null,
'buttons' => true,
'hide' => false,
], $params);
$editorType = $params['editor'];
if (is_null($editorType))
{
$editorType = JoomlaFactory::getConfig()->get('editor');
$user = JoomlaFactory::getUser();
if (!$user->guest)
{
$editorType = $user->getParam('editor', $editorType);
}
}
if (is_null($params['id']))
{
$params['id'] = $fieldName;
}
$editor = Editor::getInstance($editorType);
return $editor->display($fieldName, $value, $params['width'], $params['height'],
$params['columns'], $params['rows'], $params['buttons'], $params['id'],
$params['asset_id'], $params['created_by'], $params);
}
}

View File

@ -0,0 +1,882 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die;
use FOF40\Utils\ArrayHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
/**
* Custom JHtml (HTMLHelper) class. Offers selects compatible with Akeeba Frontend Framework (FEF)
*
* Call these methods as HTMLHelper::_('FEFHelp.select.methodName', $parameter1, $parameter2, ...)
*
* @noinspection PhpIllegalPsrClassPathInspection
*/
abstract class FEFHelpSelect
{
/**
* Default values for options. Organized by option group.
*
* @var array
*/
protected static $optionDefaults = [
'option' => [
'option.attr' => null,
'option.disable' => 'disable',
'option.id' => null,
'option.key' => 'value',
'option.key.toHtml' => true,
'option.label' => null,
'option.label.toHtml' => true,
'option.text' => 'text',
'option.text.toHtml' => true,
'option.class' => 'class',
'option.onclick' => 'onclick',
],
];
/**
* Generates a yes/no radio list.
*
* @param string $name The value of the HTML name attribute
* @param array $attribs Additional HTML attributes for the `<select>` tag
* @param mixed $selected The key that is selected
* @param string $yes Language key for Yes
* @param string $no Language key for no
* @param mixed $id The id for the field or false for no id
*
* @return string HTML for the radio list
*
* @see JFormFieldRadio
*/
public static function booleanlist($name, $attribs = [], $selected = null, $yes = 'JYES', $no = 'JNO', $id = false)
{
$options = [
\Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.option', '0', \Joomla\CMS\Language\Text::_($no)),
\Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.option', '1', \Joomla\CMS\Language\Text::_($yes)),
];
$attribs = array_merge(['forSelect' => 1], $attribs);
return \Joomla\CMS\HTML\HTMLHelper::_('FEFHelp.select.radiolist', $options, $name, $attribs, 'value', 'text', (int) $selected, $id);
}
/**
* Generates a searchable HTML selection list (Chosen on J3, Choices.js on J4).
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see JHtml::$formatOptions}.
* Selection options, see {@see JHtmlSelect::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
* @since 3.7.2
*/
public static function smartlist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false, $translate = false)
{
$innerList = self::genericlist($data, $name, $attribs, $optKey, $optText, $selected, $idtag, $translate);
// Joomla 3: Use Chosen
if (version_compare(JVERSION, '3.999.999', 'le'))
{
HTMLHelper::_('formbehavior.chosen');
return $innerList;
}
// Joomla 4: Use the joomla-field-fancy-select using choices.js
try
{
\Joomla\CMS\Factory::getApplication()->getDocument()->getWebAssetManager()
->usePreset('choicesjs')
->useScript('webcomponent.field-fancy-select');
}
catch (Exception $e)
{
return $innerList;
}
$j4Attr = array_filter([
'class' => $attribs['class'] ?? null,
'placeholder' => $attribs['placeholder'] ?? null,
], function ($x) {
return !empty($x);
});
$dataAttribute = '';
if (isset($attribs['dataAttribute']))
{
$dataAttribute = is_string($attribs['dataAttribute']) ? $attribs['dataAttribute'] : '';
}
if ((bool) ($attribs['allowCustom'] ?? false))
{
$dataAttribute .= ' allow-custom new-item-prefix="#new#"';
}
$remoteSearchUrl = $attribs['remoteSearchURL'] ?? null;
$remoteSearch = ((bool) ($attribs['remoteSearch'] ?? false)) && !empty($remoteSearchUrl);
$termKey = $attribs['termKey'] ?? 'like';
$minTermLength = $attribs['minTermLength'] ?? 3;
if ($remoteSearch)
{
$dataAttribute .= ' remote-search';
$j4Attr['url'] = $remoteSearchUrl;
$j4Attr['term-key'] = $termKey;
$j4Attr['min-term-length'] = $minTermLength;
}
if (isset($attribs['required']))
{
$j4Attr['class'] = ($j4Attr['class'] ?? '') . ' required';
$dataAttribute .= ' required';
}
if (isset($attribs['readonly']))
{
return $innerList;
}
return sprintf("<joomla-field-fancy-select %s %s>%s</joomla-field-fancy-select>", ArrayHelper::toString($j4Attr), $dataAttribute, $innerList);
}
/**
* Generates an HTML selection list.
*
* @param array $data An array of objects, arrays, or scalars.
* @param string $name The value of the HTML name attribute.
* @param mixed $attribs Additional HTML attributes for the `<select>` tag. This
* can be an array of attributes, or an array of options. Treated as options
* if it is the last argument passed. Valid options are:
* Format options, see {@see HTMLHelper::$formatOptions}.
* Selection options, see {@see HTMLHelper::options()}.
* list.attr, string|array: Additional attributes for the select
* element.
* id, string: Value to use as the select element id attribute.
* Defaults to the same as the name.
* list.select, string|array: Identifies one or more option elements
* to be selected, based on the option key values.
* @param string $optKey The name of the object variable for the option value. If
* set to null, the index of the value array is used.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string).
* @param mixed $idtag Value of the field id or null by default
* @param boolean $translate True to translate
*
* @return string HTML for the select list.
*
*/
public static function genericlist(array $data, string $name, ?array $attribs = null, string $optKey = 'value',
string $optText = 'text', $selected = null, $idtag = false,
bool $translate = false): string
{
// Set default options
$options = array_merge(HTMLHelper::$formatOptions, ['format.depth' => 0, 'id' => false]);
if (is_array($attribs) && func_num_args() === 3)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
}
else
{
// Get options from the parameters
$options['id'] = $idtag;
$options['list.attr'] = $attribs;
$options['list.translate'] = $translate;
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(['[', ']', ' '], '', $id);
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
return $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol']
. static::options($data, $options) . $baseIndent . '</select>' . $options['format.eol'];
}
/**
* Generates a grouped HTML selection list from nested arrays.
*
* @param array $data An array of groups, each of which is an array of options.
* @param string $name The value of the HTML name attribute
* @param array $options Options, an array of key/value pairs. Valid options are:
* Format options, {@see HTMLHelper::$formatOptions}.
* Selection options. See {@see HTMLHelper::options()}.
* group.id: The property in each group to use as the group id
* attribute. Defaults to none.
* group.label: The property in each group to use as the group
* label. Defaults to "text". If set to null, the data array index key is
* used.
* group.items: The property in each group to use as the array of
* items in the group. Defaults to "items". If set to null, group.id and
* group. label are forced to null and the data element is assumed to be a
* list of selections.
* id: Value to use as the select element id attribute. Defaults to
* the same as the name.
* list.attr: Attributes for the select element. Can be a string or
* an array of key/value pairs. Defaults to none.
* list.select: either the value of one selected option or an array
* of selected options. Default: none.
* list.translate: Boolean. If set, text and labels are translated via
* Text::_().
*
* @return string HTML for the select list
*
* @throws RuntimeException If a group has contents that cannot be processed.
*/
public static function groupedlist(array $data, string $name, array $options = []): string
{
// Set default options and overwrite with anything passed in
$options = array_merge(
HTMLHelper::$formatOptions,
[
'format.depth' => 0, 'group.items' => 'items', 'group.label' => 'text', 'group.label.toHtml' => true,
'id' => false,
],
$options
);
// Apply option rules
if ($options['group.items'] === null)
{
$options['group.label'] = null;
}
$attribs = '';
if (isset($options['list.attr']))
{
if (is_array($options['list.attr']))
{
$attribs = ArrayHelper::toString($options['list.attr']);
}
else
{
$attribs = $options['list.attr'];
}
if ($attribs !== '')
{
$attribs = ' ' . $attribs;
}
}
$id = $options['id'] !== false ? $options['id'] : $name;
$id = str_replace(['[', ']', ' '], '', $id);
// Disable groups in the options.
$options['groups'] = false;
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']++);
$html = $baseIndent . '<select' . ($id !== '' ? ' id="' . $id . '"' : '') . ' name="' . $name . '"' . $attribs . '>' . $options['format.eol'];
$groupIndent = str_repeat($options['format.indent'], $options['format.depth']++);
foreach ($data as $dataKey => $group)
{
$label = $dataKey;
$id = '';
$noGroup = is_int($dataKey);
if ($options['group.items'] == null)
{
// Sub-list is an associative array
$subList = $group;
}
elseif (is_array($group))
{
// Sub-list is in an element of an array.
$subList = $group[$options['group.items']];
if (isset($group[$options['group.label']]))
{
$label = $group[$options['group.label']];
$noGroup = false;
}
if (isset($options['group.id']) && isset($group[$options['group.id']]))
{
$id = $group[$options['group.id']];
$noGroup = false;
}
}
elseif (is_object($group))
{
// Sub-list is in a property of an object
$subList = $group->{$options['group.items']};
if (isset($group->{$options['group.label']}))
{
$label = $group->{$options['group.label']};
$noGroup = false;
}
if (isset($options['group.id']) && isset($group->{$options['group.id']}))
{
$id = $group->{$options['group.id']};
$noGroup = false;
}
}
else
{
throw new RuntimeException('Invalid group contents.', 1);
}
if ($noGroup)
{
$html .= static::options($subList, $options);
}
else
{
$html .= $groupIndent . '<optgroup' . (empty($id) ? '' : ' id="' . $id . '"') . ' label="'
. ($options['group.label.toHtml'] ? htmlspecialchars($label, ENT_COMPAT, 'UTF-8') : $label) . '">' . $options['format.eol']
. static::options($subList, $options) . $groupIndent . '</optgroup>' . $options['format.eol'];
}
}
return $html . ($baseIndent . '</select>' . $options['format.eol']);
}
/**
* Generates a selection list of integers.
*
* @param integer $start The start integer
* @param integer $end The end integer
* @param integer $inc The increment
* @param string $name The value of the HTML name attribute
* @param mixed $attribs Additional HTML attributes for the `<select>` tag, an array of
* attributes, or an array of options. Treated as options if it is the last
* argument passed.
* @param mixed $selected The key that is selected
* @param string $format The printf format to be applied to the number
*
* @return string HTML for the select list
*/
public static function integerlist(int $start, int $end, int $inc, string $name, ?array $attribs = null,
$selected = null, string $format = ''): string
{
// Set default options
$options = array_merge(HTMLHelper::$formatOptions, ['format.depth' => 0, 'option.format' => '', 'id' => null]);
if (is_array($attribs) && func_num_args() === 5)
{
// Assume we have an options array
$options = array_merge($options, $attribs);
// Extract the format and remove it from downstream options
$format = $options['option.format'];
unset($options['option.format']);
}
else
{
// Get options from the parameters
$options['list.attr'] = $attribs;
$options['list.select'] = $selected;
}
$start = (int) $start;
$end = (int) $end;
$inc = (int) $inc;
$data = [];
for ($i = $start; $i <= $end; $i += $inc)
{
$data[$i] = $format ? sprintf($format, $i) : $i;
}
// Tell genericlist() to use array keys
$options['option.key'] = null;
return HTMLHelper::_('FEFHelp.select.genericlist', $data, $name, $options);
}
/**
* Create an object that represents an option in an option list.
*
* @param string $value The value of the option
* @param string $text The text for the option
* @param mixed $optKey If a string, the returned object property name for
* the value. If an array, options. Valid options are:
* attr: String|array. Additional attributes for this option.
* Defaults to none.
* disable: Boolean. If set, this option is disabled.
* label: String. The value for the option label.
* option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* option.key: The property that will hold the selection value.
* Defaults to "value".
* option.label: The property in each option array to use as the
* selection label attribute. If a "label" option is provided, defaults to
* "label", if no label is given, defaults to null (none).
* option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The property that will hold the the displayed text. This
* parameter is ignored if an options array is passed.
* @param boolean $disable Not used.
*
* @return stdClass
*/
public static function option(?string $value, string $text = '', $optKey = 'value', string $optText = 'text',
bool $disable = false)
{
$options = [
'attr' => null,
'disable' => false,
'option.attr' => null,
'option.disable' => 'disable',
'option.key' => 'value',
'option.label' => null,
'option.text' => 'text',
];
if (is_array($optKey))
{
// Merge in caller's options
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['disable'] = $disable;
}
$obj = new stdClass;
$obj->{$options['option.key']} = $value;
$obj->{$options['option.text']} = trim($text) ? $text : $value;
/*
* If a label is provided, save it. If no label is provided and there is
* a label name, initialise to an empty string.
*/
$hasProperty = $options['option.label'] !== null;
if (isset($options['label']))
{
$labelProperty = $hasProperty ? $options['option.label'] : 'label';
$obj->$labelProperty = $options['label'];
}
elseif ($hasProperty)
{
$obj->{$options['option.label']} = '';
}
// Set attributes only if there is a property and a value
if ($options['attr'] !== null)
{
$obj->{$options['option.attr']} = $options['attr'];
}
// Set disable only if it has a property and a value
if ($options['disable'] !== null)
{
$obj->{$options['option.disable']} = $options['disable'];
}
return $obj;
}
/**
* Generates the option tags for an HTML select list (with no select tag
* surrounding the options).
*
* @param array $arr An array of objects, arrays, or values.
* @param mixed $optKey If a string, this is the name of the object variable for
* the option value. If null, the index of the array of objects is used. If
* an array, this is a set of options, as key/value pairs. Valid options are:
* -Format options, {@see HTMLHelper::$formatOptions}.
* -groups: Boolean. If set, looks for keys with the value
* "&lt;optgroup>" and synthesizes groups from them. Deprecated. Defaults
* true for backwards compatibility.
* -list.select: either the value of one selected option or an array
* of selected options. Default: none.
* -list.translate: Boolean. If set, text and labels are translated via
* Text::_(). Default is false.
* -option.id: The property in each option array to use as the
* selection id attribute. Defaults to none.
* -option.key: The property in each option array to use as the
* selection value. Defaults to "value". If set to null, the index of the
* option array is used.
* -option.label: The property in each option array to use as the
* selection label attribute. Defaults to null (none).
* -option.text: The property in each option array to use as the
* displayed text. Defaults to "text". If set to null, the option array is
* assumed to be a list of displayable scalars.
* -option.attr: The property in each option array to use for
* additional selection attributes. Defaults to none.
* -option.disable: The property that will hold the disabled state.
* Defaults to "disable".
* -option.key: The property that will hold the selection value.
* Defaults to "value".
* -option.text: The property that will hold the the displayed text.
* Defaults to "text". If set to null, the option array is assumed to be a
* list of displayable scalars.
* @param string $optText The name of the object variable for the option text.
* @param mixed $selected The key that is selected (accepts an array or a string)
* @param boolean $translate Translate the option values.
*
* @return string HTML for the select list
*/
public static function options(array $arr, $optKey = 'value', string $optText = 'text',
?string $selected = null, bool $translate = false): string
{
$options = array_merge(
HTMLHelper::$formatOptions,
static::$optionDefaults['option'],
['format.depth' => 0, 'groups' => true, 'list.select' => null, 'list.translate' => false]
);
if (is_array($optKey))
{
// Set default options and overwrite with anything passed in
$options = array_merge($options, $optKey);
}
else
{
// Get options from the parameters
$options['option.key'] = $optKey;
$options['option.text'] = $optText;
$options['list.select'] = $selected;
$options['list.translate'] = $translate;
}
$html = '';
$baseIndent = str_repeat($options['format.indent'], $options['format.depth']);
foreach ($arr as $elementKey => &$element)
{
$attr = '';
$extra = '';
$label = '';
$id = '';
if (is_array($element))
{
$key = $options['option.key'] === null ? $elementKey : $element[$options['option.key']];
$text = $element[$options['option.text']];
if (isset($element[$options['option.attr']]))
{
$attr = $element[$options['option.attr']];
}
if (isset($element[$options['option.id']]))
{
$id = $element[$options['option.id']];
}
if (isset($element[$options['option.label']]))
{
$label = $element[$options['option.label']];
}
if (isset($element[$options['option.disable']]) && $element[$options['option.disable']])
{
$extra .= ' disabled="disabled"';
}
}
elseif (is_object($element))
{
$key = $options['option.key'] === null ? $elementKey : $element->{$options['option.key']};
$text = $element->{$options['option.text']};
if (isset($element->{$options['option.attr']}))
{
$attr = $element->{$options['option.attr']};
}
if (isset($element->{$options['option.id']}))
{
$id = $element->{$options['option.id']};
}
if (isset($element->{$options['option.label']}))
{
$label = $element->{$options['option.label']};
}
if (isset($element->{$options['option.disable']}) && $element->{$options['option.disable']})
{
$extra .= ' disabled="disabled"';
}
if (isset($element->{$options['option.class']}) && $element->{$options['option.class']})
{
$extra .= ' class="' . $element->{$options['option.class']} . '"';
}
if (isset($element->{$options['option.onclick']}) && $element->{$options['option.onclick']})
{
$extra .= ' onclick="' . $element->{$options['option.onclick']} . '"';
}
}
else
{
// This is a simple associative array
$key = $elementKey;
$text = $element;
}
/*
* The use of options that contain optgroup HTML elements was
* somewhat hacked for J1.5. J1.6 introduces the grouplist() method
* to handle this better. The old solution is retained through the
* "groups" option, which defaults true in J1.6, but should be
* deprecated at some point in the future.
*/
$key = (string) $key;
if ($key === '<OPTGROUP>' && $options['groups'])
{
$html .= $baseIndent . '<optgroup label="' . ($options['list.translate'] ? Text::_($text) : $text) . '">' . $options['format.eol'];
$baseIndent = str_repeat($options['format.indent'], ++$options['format.depth']);
}
elseif ($key === '</OPTGROUP>' && $options['groups'])
{
$baseIndent = str_repeat($options['format.indent'], --$options['format.depth']);
$html .= $baseIndent . '</optgroup>' . $options['format.eol'];
}
else
{
// If no string after hyphen - take hyphen out
$splitText = explode(' - ', $text, 2);
$text = $splitText[0];
if (isset($splitText[1]) && $splitText[1] !== '' && !preg_match('/^[\s]+$/', $splitText[1]))
{
$text .= ' - ' . $splitText[1];
}
if (!empty($label) && $options['list.translate'])
{
$label = Text::_($label);
}
if ($options['option.label.toHtml'])
{
$label = htmlentities($label);
}
if (is_array($attr))
{
$attr = ArrayHelper::toString($attr);
}
else
{
$attr = trim($attr);
}
$extra = ($id ? ' id="' . $id . '"' : '') . ($label ? ' label="' . $label . '"' : '') . ($attr ? ' ' . $attr : '') . $extra;
if (is_array($options['list.select']))
{
foreach ($options['list.select'] as $val)
{
$key2 = is_object($val) ? $val->{$options['option.key']} : $val;
if ($key == $key2)
{
$extra .= ' selected="selected"';
break;
}
}
}
elseif ((string) $key === (string) $options['list.select'])
{
$extra .= ' selected="selected"';
}
if ($options['list.translate'])
{
$text = Text::_($text);
}
// Generate the option, encoding as required
$html .= $baseIndent . '<option value="' . ($options['option.key.toHtml'] ? htmlspecialchars($key, ENT_COMPAT, 'UTF-8') : $key) . '"'
. $extra . '>';
$html .= $options['option.text.toHtml'] ? htmlentities(html_entity_decode($text, ENT_COMPAT, 'UTF-8'), ENT_COMPAT, 'UTF-8') : $text;
$html .= '</option>' . $options['format.eol'];
}
}
return $html;
}
/**
* Generates an HTML radio list.
*
* @param array $data An array of objects
* @param string $name The value of the HTML name attribute
* @param string $attribs Additional HTML attributes for the `<select>` tag
* @param mixed $optKey The key that is selected
* @param string $optText The name of the object variable for the option value
* @param mixed $selected The name of the object variable for the option text
* @param boolean $idtag Value of the field id or null by default
* @param boolean $translate True if options will be translated
*
* @return string HTML for the select list
*/
public static function radiolist($data, $name, $attribs = null, $optKey = 'value', $optText = 'text', $selected = null, $idtag = false,
$translate = false)
{
$forSelect = false;
if (isset($attribs['forSelect']))
{
$forSelect = (bool) ($attribs['forSelect']);
unset($attribs['forSelect']);
}
if (is_array($attribs))
{
$attribs = ArrayHelper::toString($attribs);
}
$id_text = empty($idtag) ? $name : $idtag;
$html = '';
foreach ($data as $optionObject)
{
$optionValue = $optionObject->$optKey;
$labelText = $translate ? \Joomla\CMS\Language\Text::_($optionObject->$optText) : $optionObject->$optText;
$id = ($optionObject->id ?? null);
$extra = '';
$id = $id ? $optionObject->id : $id_text . $optionValue;
if (is_array($selected))
{
foreach ($selected as $val)
{
$k2 = is_object($val) ? $val->$optKey : $val;
if ($optionValue == $k2)
{
$extra .= ' selected="selected" ';
break;
}
}
}
else
{
$extra .= ((string) $optionValue === (string) $selected ? ' checked="checked" ' : '');
}
if ($forSelect)
{
$html .= "\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />';
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">' . $labelText . '</label>';
}
else
{
$html .= "\n\t" . '<label for="' . $id . '" id="' . $id . '-lbl">';
$html .= "\n\t\n\t" . '<input type="radio" name="' . $name . '" id="' . $id . '" value="' . $optionValue . '" ' . $extra
. $attribs . ' />' . $labelText;
$html .= "\n\t" . '</label>';
}
}
return $html . "\n";
}
/**
* Creates two radio buttons styled with FEF to appear as a YES/NO switch
*
* @param string $name Name of the field
* @param mixed $selected Selected field
* @param array $attribs Additional attributes to add to the switch
*
* @return string The HTML for the switch
*/
public static function booleanswitch(string $name, $selected, array $attribs = []): string
{
if (empty($attribs))
{
$attribs = ['class' => 'akeeba-toggle'];
}
elseif (isset($attribs['class']))
{
$attribs['class'] .= ' akeeba-toggle';
}
else
{
$attribs['class'] = 'akeeba-toggle';
}
$temp = '';
foreach ($attribs as $key => $value)
{
$temp .= $key . ' = "' . $value . '"';
}
$attribs = $temp;
$checked_1 = $selected ? '' : 'checked ';
$checked_2 = $selected ? 'checked ' : '';
$html = '<div ' . $attribs . '>';
$html .= '<input type="radio" class="radio-yes" name="' . $name . '" ' . $checked_2 . 'id="' . $name . '-2" value="1">';
$html .= '<label for="' . $name . '-2" class="green">' . Text::_('JYES') . '</label>';
$html .= '<input type="radio" class="radio-no" name="' . $name . '" ' . $checked_1 . 'id="' . $name . '-1" value="0">';
$html .= '<label for="' . $name . '-1" class="red">' . Text::_('JNO') . '</label>';
$html .= '</div>';
return $html;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') || die();
use Joomla\CMS\Form\FormHelper;
// Prevent PHP fatal errors if this somehow gets accidentally loaded multiple times
if (class_exists('JFormFieldFancyradio'))
{
return;
}
// Load the base form field class
FormHelper::loadFieldClass('radio');
/**
* Yes/No switcher, compatible with Joomla 3 and 4
*
* ## How to use
*
* 1. Create a folder in your project for custom Joomla form fields, e.g. components/com_example/fields
* 2. Create a new file called `fancyradio.php` with the content
* ```php
* defined('_JEXEC') || die();
* require_once JPATH_LIBRARIES . '/fof40/Html/Fields/fancyradio.php';
* ```
*
* @package Joomla\CMS\Form\Field
*
* @since 1.0.0
* @noinspection PhpUnused
* @noinspection PhpIllegalPsrClassPathInspection
*/
class JFormFieldFancyradio extends JFormFieldRadio
{
public function __construct($form = null)
{
if (version_compare(JVERSION, '3.999.999', 'gt'))
{
// Joomla 4.0 and later.
$this->layout = 'joomla.form.field.radio.switcher';
}
else
{
// Joomla 3.x. Yes, 3.10 does have the layout but I am playing it safe.
$this->layout = 'joomla.form.field.radio';
}
parent::__construct($form);
}
}

View File

@ -0,0 +1,466 @@
<?php
/**
* @package FOF
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace FOF40\Html;
defined('_JEXEC') || die;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Factory as JoomlaFactory;
use Joomla\CMS\Helper\UserGroupsHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use stdClass;
/**
* Returns arrays of HTMLHelper select options for Joomla-specific information such as access levels.
*
* @method static array access() access(array $params)
* @method static array usergroups() usergroups(array $params)
* @method static array cachehandlers() cachehandlers(array $params)
* @method static array components() components(array $params)
* @method static array languages() languages(array $params)
* @method static array published() published(array $params)
*/
class SelectOptions
{
private static $cache = [];
/**
* Magic method to handle static calls
*
* @param string $name The name of the static method being called
* @param array $arguments Optional arguments, if they are supported by the options type.
*
* @return mixed
* @since 3.3.0
*/
public static function __callStatic(string $name, array $arguments = [])
{
return self::getOptions($name, $arguments);
}
/**
* Get a list of Joomla options of the type you specify. Supported types
* - access View access levels
* - usergroups User groups
* - cachehandlers Cache handlers
* - components Installed components accessible by the current user
* - languages Site or administrator languages
* - published Published status
*
* Global params:
* - cache Should I returned cached data? Default: true.
*
* See the private static methods of this class for more information on params.
*
* @param string $type The options type to get
* @param array $params Optional arguments, if they are supported by the options type.
*
* @return stdClass[]
* @since 3.3.0
* @api
*/
public static function getOptions(string $type, array $params = []): array
{
if ((substr($type, 0, 1) == '_') || !method_exists(__CLASS__, '_api_' . $type))
{
throw new \InvalidArgumentException(__CLASS__ . "does not support option type '$type'.");
}
$useCache = true;
if (isset($params['cache']))
{
$useCache = isset($params['cache']);
unset($params['cache']);
}
$cacheKey = sha1($type . '--' . print_r($params, true));
$fetchNew = !$useCache || ($useCache && !isset(self::$cache[$cacheKey]));
$ret = [];
if ($fetchNew)
{
$ret = forward_static_call_array([__CLASS__, '_api_' . $type], [$params]);
}
if (!$useCache)
{
return $ret;
}
if ($fetchNew)
{
self::$cache[$cacheKey] = $ret;
}
return self::$cache[$cacheKey];
}
/**
* Joomla! Access Levels (previously: view access levels)
*
* Available params:
* - allLevels: Show an option for all levels (default: false)
*
* @param array $params Parameters
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @see \Joomla\CMS\HTML\Helpers\Access::level()
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_access(array $params = []): array
{
$db = JoomlaFactory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('a.id', 'value') . ', ' . $db->quoteName('a.title', 'text'))
->from($db->quoteName('#__viewlevels', 'a'))
->group($db->quoteName(['a.id', 'a.title', 'a.ordering']))
->order($db->quoteName('a.ordering') . ' ASC')
->order($db->quoteName('title') . ' ASC');
// Get the options.
$db->setQuery($query);
$options = $db->loadObjectList() ?? [];
if (isset($params['allLevels']) && $params['allLevels'])
{
array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_LEVELS')));
}
return $options;
}
/**
* Joomla! User Groups
*
* Available params:
* - allGroups: Show an option for all groups (default: false)
*
* @param array $params Parameters
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @see \Joomla\CMS\HTML\Helpers\Access::usergroup()
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_usergroups(array $params = []): array
{
$options = array_values(UserGroupsHelper::getInstance()->getAll());
for ($i = 0, $n = count($options); $i < $n; $i++)
{
$options[$i]->value = $options[$i]->id;
$options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title;
}
// If all usergroups is allowed, push it into the array.
if (isset($params['allGroups']) && $params['allGroups'])
{
array_unshift($options, HTMLHelper::_('select.option', '', Text::_('JOPTION_ACCESS_SHOW_ALL_GROUPS')));
}
return $options;
}
/**
* Joomla cache handlers
*
* @param array $params Ignored
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_cachehandlers(array $params = []): array
{
$options = [];
// Convert to name => name array.
foreach (Cache::getStores() as $store)
{
$options[] = HTMLHelper::_('select.option', $store, Text::_('JLIB_FORM_VALUE_CACHE_' . $store), 'value', 'text');
}
return $options;
}
/**
* Get a list of all installed components and also translates them.
*
* Available params:
* - client_ids Array of Joomla application client IDs
*
* @param array $params
*
* @return stdClass[]
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_components(array $params = []): array
{
$db = JoomlaFactory::getDbo();
// Check for client_ids override
$client_ids = $params['client_ids'] ?? [0, 1];
if (is_string($client_ids))
{
$client_ids = explode(',', $client_ids);
}
// Calculate client_ids where clause
$client_ids = array_map(function ($client_id) use ($db) {
return $db->q((int) trim($client_id));
}, $client_ids);
$query = $db->getQuery(true)
->select(
[
$db->qn('name'),
$db->qn('element'),
$db->qn('client_id'),
$db->qn('manifest_cache'),
]
)
->from($db->qn('#__extensions'))
->where($db->qn('type') . ' = ' . $db->q('component'))
->where($db->qn('client_id') . ' IN (' . implode(',', $client_ids) . ')');
$components = $db->setQuery($query)->loadObjectList('element');
// Convert to array of objects, so we can use sortObjects()
// Also translate component names with Text::_()
$aComponents = [];
$user = JoomlaFactory::getUser();
foreach ($components as $component)
{
// Don't show components in the list where the user doesn't have access for
// TODO: perhaps add an option for this
if (!$user->authorise('core.manage', $component->element))
{
continue;
}
$aComponents[$component->element] = (object) [
'value' => $component->element,
'text' => self::_translateComponentName($component),
];
}
// Reorder the components array, because the alphabetical
// ordering changed due to the Text::_() translation
uasort(
$aComponents,
function ($a, $b) {
return strcasecmp($a->text, $b->text);
}
);
return $aComponents;
}
/**
* Method to get the field options.
*
* Available params:
* - client 'site' (default) or 'administrator'
* - none Text to show for "all languages" option, use empty string to remove it
*
* @param array $params
*
* @return object[] Languages for the specified client
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_languages(array $params): array
{
$client = $params['client'] ?? 'site';
if (!in_array($client, ['site', 'administrator']))
{
$client = 'site';
}
// Make sure the languages are sorted base on locale instead of random sorting
$options = LanguageHelper::createLanguageList(null, constant('JPATH_' . strtoupper($client)), true, true);
if (count($options) > 1)
{
usort(
$options,
function ($a, $b) {
return strcmp($a['value'], $b['value']);
}
);
}
$none = $params['none'] ?? '*';
if (!empty($none))
{
array_unshift($options, HTMLHelper::_('select.option', '*', Text::_($none)));
}
return $options;
}
/**
* Options for a Published field
*
* Params:
* - none Placeholder for no selection (empty key). Default: null.
* - published Show "Published"? Default: true
* - unpublished Show "Unpublished"? Default: true
* - archived Show "Archived"? Default: false
* - trash Show "Trashed"? Default: false
* - all Show "All" option? This is different than none, the key is '*'. Default: false
*
* @param array $params
*
* @return array
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_published(array $params = []): array
{
$config = array_merge([
'none' => '',
'published' => true,
'unpublished' => true,
'archived' => false,
'trash' => false,
'all' => false,
], $params);
$options = [];
if (!empty($config['none']))
{
$options[] = HTMLHelper::_('select.option', '', Text::_($config['none']));
}
if ($config['published'])
{
$options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED'));
}
if ($config['unpublished'])
{
$options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED'));
}
if ($config['archived'])
{
$options[] = HTMLHelper::_('select.option', '2', Text::_('JARCHIVED'));
}
if ($config['trash'])
{
$options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED'));
}
if ($config['all'])
{
$options[] = HTMLHelper::_('select.option', '*', Text::_('JALL'));
}
return $options;
}
/**
* Options for a Published field
*
* Params:
* - none Placeholder for no selection (empty key). Default: null.
*
* @param array $params
*
* @return array
* @since 3.3.0
* @internal
*
* @noinspection PhpUnusedPrivateMethodInspection
*/
private static function _api_boolean(array $params = []): array
{
$config = array_merge([
'none' => '',
], $params);
$options = [];
if (!empty($config['none']))
{
$options[] = HTMLHelper::_('select.option', '', Text::_($config['none']));
}
$options[] = HTMLHelper::_('select.option', '1', Text::_('JYES'));
$options[] = HTMLHelper::_('select.option', '0', Text::_('JNO'));
return $options;
}
/**
* Translate a component name
*
* @param object $item The component object
*
* @return string $text The translated name of the extension
* @since 3.3.0
* @internal
*
* @see /administrator/com_installer/models/extension.php
*/
private static function _translateComponentName(object $item): string
{
// Map the manifest cache to $item. This is needed to get the name from the
// manifest_cache and NOT from the name column, else some Text::_() translations fails.
$mData = json_decode($item->manifest_cache);
if ($mData)
{
foreach ($mData as $key => $value)
{
if ($key == 'type')
{
// Ignore the type field
continue;
}
$item->$key = $value;
}
}
$lang = JoomlaFactory::getLanguage();
$source = JPATH_ADMINISTRATOR . '/components/' . $item->element;
$lang->load("$item->element.sys", JPATH_ADMINISTRATOR, null, false, false)
|| $lang->load("$item->element.sys", $source, null, false, false)
|| $lang->load("$item->element.sys", JPATH_ADMINISTRATOR, $lang->getDefault(), false, false)
|| $lang->load("$item->element.sys", $source, $lang->getDefault(), false, false);
return Text::_($item->name);
}
}