diff --git a/administrator/components/com_categories/forms/category.xml b/administrator/components/com_categories/forms/category.xml index 61a5053e5c3b6..1f1dc926ec352 100644 --- a/administrator/components/com_categories/forms/category.xml +++ b/administrator/components/com_categories/forms/category.xml @@ -204,7 +204,6 @@ name="tags" type="tag" label="JTAG" - class="col-sm-12" multiple="true" /> diff --git a/administrator/components/com_fields/forms/field.xml b/administrator/components/com_fields/forms/field.xml index 17906b556b1d9..571bd83d59b83 100644 --- a/administrator/components/com_fields/forms/field.xml +++ b/administrator/components/com_fields/forms/field.xml @@ -266,10 +266,9 @@ diff --git a/administrator/components/com_menus/Controller/MenuController.php b/administrator/components/com_menus/Controller/MenuController.php index 2c45deaba555f..017d690adfed5 100644 --- a/administrator/components/com_menus/Controller/MenuController.php +++ b/administrator/components/com_menus/Controller/MenuController.php @@ -76,7 +76,7 @@ public function save($key = null, $urlVar = null) // Get the model and attempt to validate the posted data. /* @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel('Menu'); + $model = $this->getModel('Menu', '', ['ignore_request' => false]); $form = $model->getForm(); if (!$form) diff --git a/administrator/components/com_menus/Field/MenuPresetField.php b/administrator/components/com_menus/Field/MenuPresetField.php index 22310a786d6a0..66eeaa548589d 100644 --- a/administrator/components/com_menus/Field/MenuPresetField.php +++ b/administrator/components/com_menus/Field/MenuPresetField.php @@ -14,7 +14,7 @@ use Joomla\CMS\Form\Field\ListField; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; -use Joomla\CMS\Menu\MenuHelper; +use Joomla\Component\Menus\Administrator\Helper\MenusHelper; /** * Administrator Menu Presets list field. @@ -42,7 +42,7 @@ class MenuPresetField extends ListField protected function getOptions() { $options = array(); - $presets = MenuHelper::getPresets(); + $presets = MenusHelper::getPresets(); foreach ($presets as $preset) { diff --git a/administrator/components/com_menus/Helper/MenusHelper.php b/administrator/components/com_menus/Helper/MenusHelper.php index 07427b3b8bd5b..daf3457ca0c22 100644 --- a/administrator/components/com_menus/Helper/MenusHelper.php +++ b/administrator/components/com_menus/Helper/MenusHelper.php @@ -12,11 +12,12 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Helper\ContentHelper; use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\Multilanguage; use Joomla\CMS\Language\Text; -use Joomla\CMS\Menu\MenuHelper; +use Joomla\CMS\Menu\MenuItem; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseInterface; use Joomla\Registry\Registry; @@ -34,6 +35,15 @@ class MenusHelper extends ContentHelper */ protected static $_filter = array('option', 'view', 'layout'); + /** + * List of preset include paths + * + * @var array + * + * @since __DEPLOY_VERSION__ + */ + protected static $presets = null; + /** * Gets a standard form of a link for lookups. * @@ -290,12 +300,13 @@ public static function getAssociations($pk) * @param boolean $enabledOnly Whether to load only enabled/published menu items. * @param int[] $exclude The menu items to exclude from the list * - * @return array + * @return MenuItem A root node with the menu items as children * - * @since 3.8.0 + * @since __DEPLOY_VERSION__ */ public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) { + $root = new MenuItem; $db = Factory::getContainer()->get(DatabaseInterface::class); $query = $db->getQuery(true); @@ -340,21 +351,45 @@ public static function getMenuItems($menutype, $enabledOnly = false, $exclude = try { - $menuItems = $db->loadObjectList(); + $menuItems = $db->loadObjectList('id', '\Joomla\CMS\Menu\MenuItem'); - foreach ($menuItems as &$menuitem) + foreach ($menuItems as $menuitem) { $menuitem->params = new Registry($menuitem->params); + + // Resolve the alias item to get the original item + if ($menuitem->type == 'alias') + { + static::resolveAlias($menuitem); + } + + if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) + { + $menuitem->submenu = array(); + $menuitem->class = $menuitem->img ?? ''; + $menuitem->scope = $menuitem->scope ?? null; + $menuitem->browserNav = $menuitem->browserNav ? '_blank' : ''; + } + + if ($menuitem->parent_id > 1) + { + if (isset($menuItems[$menuitem->parent_id])) + { + $menuItems[$menuitem->parent_id]->addChild($menuitem); + } + } + else + { + $root->addChild($menuitem); + } } } catch (\RuntimeException $e) { - $menuItems = array(); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); } - return $menuItems; + return $root; } /** @@ -367,37 +402,37 @@ public static function getMenuItems($menutype, $enabledOnly = false, $exclude = * * @throws \Exception * - * @since 3.8.0 + * @since __DEPLOY_VERSION__ */ public static function installPreset($preset, $menutype) { - $items = MenuHelper::loadPreset($preset, false); + $root = static::loadPreset($preset, false); - if (count($items) == 0) + if (count($root->getChildren()) == 0) { throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); } - static::installPresetItems($items, $menutype, 1); + static::installPresetItems($root, $menutype, 1); } /** * Method to install a preset menu item into database and link it to the given menutype * - * @param \stdClass[] $items The single menuitem instance with a list of its descendants - * @param string $menutype The target menutype - * @param int $parent The parent id or object + * @param MenuItem $node The parent node of the items to process + * @param string $menutype The target menutype * * @return void * * @throws \Exception * - * @since 3.8.0 + * @since __DEPLOY_VERSION__ */ - protected static function installPresetItems(&$items, $menutype, $parent = 1) + protected static function installPresetItems($node, $menutype) { $db = Factory::getDbo(); $query = $db->getQuery(true); + $items = $node->getChildren(); static $components = array(); @@ -410,7 +445,7 @@ protected static function installPresetItems(&$items, $menutype, $parent = 1) Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); - foreach ($items as &$item) + foreach ($items as $item) { /** @var \JTableMenu $table */ $table = Table::getInstance('Menu'); @@ -430,7 +465,7 @@ protected static function installPresetItems(&$items, $menutype, $parent = 1) 'menutype' => $menutype, 'type' => $item->type, 'title' => $item->title, - 'parent_id' => $parent, + 'parent_id' => $item->getParent()->id, 'client_id' => 1, ); $table->load($keys); @@ -456,7 +491,7 @@ protected static function installPresetItems(&$items, $menutype, $parent = 1) 'menutype' => $menutype, 'type' => $item->type, 'link' => $item->link, - 'parent_id' => $parent, + 'parent_id' => $item->getParent()->id, 'client_id' => 1, ); $table->load($keys); @@ -489,7 +524,7 @@ protected static function installPresetItems(&$items, $menutype, $parent = 1) 'img' => $item->class, 'access' => $item->access, 'component_id' => array_search($item->element, $components) ?: 0, - 'parent_id' => $parent, + 'parent_id' => $item->getParent()->id, 'client_id' => 1, 'published' => 1, 'language' => '*', @@ -502,7 +537,7 @@ protected static function installPresetItems(&$items, $menutype, $parent = 1) throw new \Exception('Bind failed: ' . $table->getError()); } - $table->setLocation($parent, 'last-child'); + $table->setLocation($item->getParent()->id, 'last-child'); if (!$table->check()) { @@ -516,10 +551,351 @@ protected static function installPresetItems(&$items, $menutype, $parent = 1) $item->id = $table->get('id'); - if (!empty($item->submenu)) + if ($item->hasChildren()) { - static::installPresetItems($item->submenu, $menutype, $item->id); + static::installPresetItems($item, $menutype); } } } + + /** + * Add a custom preset externally via plugin or any other means. + * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) + * + * @param string $name The unique identifier for the preset. + * @param string $title The display label for the preset. + * @param string $path The path to the preset file. + * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public static function addPreset($name, $title, $path, $replace = true) + { + if (static::$presets === null) + { + static::getPresets(); + } + + if ($name == 'joomla') + { + $replace = false; + } + + if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) + { + $preset = new \stdClass; + + $preset->name = $name; + $preset->title = $title; + $preset->path = $path; + + static::$presets[$name] = $preset; + } + } + + /** + * Get a list of available presets. + * + * @return \stdClass[] + * + * @since __DEPLOY_VERSION__ + */ + public static function getPresets() + { + if (static::$presets === null) + { + // Important: 'null' will cause infinite recursion. + static::$presets = array(); + + static::addPreset('joomla', 'JLIB_MENUS_PRESET_JOOMLA', JPATH_ADMINISTRATOR . '/components/com_menus/presets/joomla.xml'); + static::addPreset('modern', 'JLIB_MENUS_PRESET_MODERN', JPATH_ADMINISTRATOR . '/components/com_menus/presets/modern.xml'); + + // Load from template folder automatically + $app = Factory::getApplication(); + $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; + + if (is_dir($tpl)) + { + $files = Folder::files($tpl, '\.xml$'); + + foreach ($files as $file) + { + $name = substr($file, 0, -4); + $title = str_replace('-', ' ', $name); + + static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); + } + } + } + + return static::$presets; + } + + /** + * Load the menu items from a preset file into a hierarchical list of objects + * + * @param string $name The preset name + * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? + * @param MenuItem $parent Root node of the menu + * + * @return MenuItem + * + * @since __DEPLOY_VERSION__ + */ + public static function loadPreset($name, $fallback = true, $parent = null) + { + $presets = static::getPresets(); + + if (!$parent) + { + $parent = new MenuItem; + } + + if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) + { + static::loadXml($xml, $parent); + } + elseif ($fallback && isset($presets['joomla'])) + { + if (($xml = simplexml_load_file($presets['joomla']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) + { + static::loadXml($xml, $parent); + } + } + + return $parent; + } + + /** + * Method to resolve the menu item alias type menu item + * + * @param \stdClass &$item The alias object + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public static function resolveAlias(&$item) + { + $obj = $item; + + while ($obj->type == 'alias') + { + $params = new Registry($obj->params); + $aliasTo = $params->get('aliasoptions'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select('a.id, a.link, a.type, e.element') + ->from('#__menu a') + ->where('a.id = ' . (int) $aliasTo) + ->join('left', '#__extensions e ON e.id = a.component_id = e.id'); + + try + { + $obj = $db->setQuery($query)->loadObject(); + + if (!$obj) + { + $item->link = ''; + + return; + } + } + catch (\Exception $e) + { + $item->link = ''; + + return; + } + } + + $item->id = $obj->id; + $item->link = $obj->link; + $item->type = $obj->type; + $item->element = $obj->element; + } + + /** + * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. + * + * @param MenuItem $item Menu item to preprocess + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public static function preprocess($item) + { + // Resolve the alias item to get the original item + if ($item->type == 'alias') + { + static::resolveAlias($item); + } + + if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) + { + $item->submenu = array(); + $item->class = $item->img ?? ''; + $item->scope = $item->scope ?? null; + $item->browserNav = $item->browserNav ? '_blank' : ''; + } + } + + /** + * Load a menu tree from an XML file + * + * @param \SimpleXMLElement[] $elements The xml menuitem nodes + * @param MenuItem $parent The menu hierarchy list to be populated + * @param string[] $replace The substring replacements for iterator type items + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + protected static function loadXml($elements, $parent, $replace = array()) + { + foreach ($elements as $element) + { + if ($element->getName() != 'menuitem') + { + continue; + } + + $select = (string) $element['sql_select']; + $from = (string) $element['sql_from']; + + /** + * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) + * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. + * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu + */ + if ($select && $from) + { + $hidden = $element['hidden'] == 'true'; + $where = (string) $element['sql_where']; + $order = (string) $element['sql_order']; + $group = (string) $element['sql_group']; + $lJoin = (string) $element['sql_leftjoin']; + $iJoin = (string) $element['sql_innerjoin']; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($select)->from($from); + + if ($where) + { + $query->where($where); + } + + if ($order) + { + $query->order($order); + } + + if ($group) + { + $query->group($group); + } + + if ($lJoin) + { + $query->leftJoin($lJoin); + } + + if ($iJoin) + { + $query->innerJoin($iJoin); + } + + $results = $db->setQuery($query)->loadObjectList(); + + // Skip the entire group if no items to iterate over. + if ($results) + { + // Show the repeatable group heading node only if not set as hidden. + if (!$hidden) + { + $child = static::parseXmlNode($element, $replace); + $parent->addChild($child); + } + + // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. + foreach ($results as $result) + { + static::loadXml($element->menuitem, $parent, $result); + } + } + } + else + { + $item = static::parseXmlNode($element, $replace); + + // Process the child nodes + static::loadXml($element->menuitem, $item, $replace); + + $parent->addChild($item); + } + } + } + + /** + * Create a menu item node from an xml element + * + * @param \SimpleXMLElement $node A menuitem element from preset xml + * @param string[] $replace The values to substitute in the title, link and element texts + * + * @return \stdClass + * + * @since __DEPLOY_VERSION__ + */ + protected static function parseXmlNode($node, $replace = array()) + { + $item = new MenuItem; + + $item->id = null; + $item->type = (string) $node['type']; + $item->title = (string) $node['title']; + $item->link = (string) $node['link']; + $item->element = (string) $node['element']; + $item->class = (string) $node['class']; + $item->icon = (string) $node['icon']; + $item->browserNav = (string) $node['target']; + $item->access = (int) $node['access']; + $item->scope = (string) $node['scope'] ?: 'default'; + $item->setParams(new Registry(trim($node->params))); + $item->getParams()->set('menu-permission', (string) $node['permission']); + + if ($item->type == 'separator' && trim($item->title, '- ')) + { + $item->getParams()->set('text_separator', 1); + } + + if ($item->type == 'heading' || $item->type == 'container') + { + $item->link = '#'; + } + + if ((string) $node['quicktask']) + { + $item->getParams()->set('menu-quicktask', (string) $node['quicktask']); + $item->getParams()->set('menu-quicktask-title', (string) $node['quicktask-title']); + $item->getParams()->set('menu-quicktask-icon', (string) $node['quicktask-icon']); + $item->getParams()->set('menu-quicktask-permission', (string) $node['quicktask-permission']); + } + + // Translate attributes for iterator values + foreach ($replace as $var => $val) + { + $item->title = str_replace("{sql:$var}", $val, $item->title); + $item->element = str_replace("{sql:$var}", $val, $item->element); + $item->link = str_replace("{sql:$var}", $val, $item->link); + $item->class = str_replace("{sql:$var}", $val, $item->class); + $item->icon = str_replace("{sql:$var}", $val, $item->icon); + } + + return $item; + } } diff --git a/administrator/components/com_menus/View/Menu/XmlView.php b/administrator/components/com_menus/View/Menu/XmlView.php index 71bb461b2010d..497449e246367 100644 --- a/administrator/components/com_menus/View/Menu/XmlView.php +++ b/administrator/components/com_menus/View/Menu/XmlView.php @@ -56,10 +56,10 @@ public function display($tpl = null) if ($menutype) { - $items = MenusHelper::getMenuItems($menutype, true); + $root = MenusHelper::getMenuItems($menutype, true); } - if (empty($items)) + if ($root->hasChildren()) { Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); @@ -68,7 +68,7 @@ public function display($tpl = null) return; } - $this->items = MenuHelper::createLevels($items); + $this->items = $root->getChildren(); $xml = new \SimpleXMLElement(' + link="index.php?option=com_content" + quicktask="index.php?option=com_content&task=article.add" + quicktask-title="MOD_MENU_COM_CONTENT_ARTICLE_ADD" + > + link="index.php?option=com_categories&extension=com_content" + quicktask="index.php?option=com_categories&extension=com_content&task=category.add" + > - + link="index.php?option=com_menus&view=items&menutype={sql:menutype}" + icon="{sql:icon}" + class="class:menu"> diff --git a/administrator/components/com_modules/tmpl/module/edit_assignment.php b/administrator/components/com_modules/tmpl/module/edit_assignment.php index bf9c2470c298e..9ddd1fdf68109 100644 --- a/administrator/components/com_modules/tmpl/module/edit_assignment.php +++ b/administrator/components/com_modules/tmpl/module/edit_assignment.php @@ -22,7 +22,7 @@ HTMLHelper::_('script', 'com_modules/admin-module-edit_assignment.min.js', array('version' => 'auto', 'relative' => true)); ?>
- +
element - this.select = this.querySelector('select'); + // Keycodes + this.keyCode = { + ENTER: 13, + }; - if (!this.select) { - throw new Error('JoomlaFieldFancySelect require element + this.select = this.querySelector('select'); - // Make sure nothing is highlighted - const highlighted = this.choicesInstance.dropdown.querySelector(`.${this.choicesInstance.config.classNames.highlightedState}`); - if (highlighted) return; + if (!this.select) { + throw new Error('JoomlaFieldFancySelect requires