diff --git a/src/packages/mod_mokowaas_categories/language/en-GB/mod_mokowaas_categories.ini b/src/packages/mod_mokowaas_categories/language/en-GB/mod_mokowaas_categories.ini new file mode 100644 index 00000000..e01d1dc4 --- /dev/null +++ b/src/packages/mod_mokowaas_categories/language/en-GB/mod_mokowaas_categories.ini @@ -0,0 +1,24 @@ +MOD_MOKOWAAS_CATEGORIES="MokoWaaS Categories" +MOD_MOKOWAAS_CATEGORIES_DESC="Auto-discovers article categories and renders them as a collapsible tree menu. Ideal for knowledge base and help sections." + +MOD_MOKOWAAS_CATEGORIES_ROOT_LABEL="Root Category" +MOD_MOKOWAAS_CATEGORIES_ROOT_DESC="Select a parent category. Only its children (and their subcategories) will be displayed. Leave as All to show the entire category tree." +MOD_MOKOWAAS_CATEGORIES_ALL_CATEGORIES="- All Categories -" + +MOD_MOKOWAAS_CATEGORIES_DEPTH_LABEL="Maximum Depth" +MOD_MOKOWAAS_CATEGORIES_DEPTH_DESC="How many levels deep to display. 1 shows only top-level categories, 2 adds one level of subcategories, etc." + +MOD_MOKOWAAS_CATEGORIES_COUNT_LABEL="Show Article Count" +MOD_MOKOWAAS_CATEGORIES_COUNT_DESC="Display the number of published articles next to each category name." + +MOD_MOKOWAAS_CATEGORIES_EMPTY_LABEL="Show Empty Categories" +MOD_MOKOWAAS_CATEGORIES_EMPTY_DESC="Display categories that have no published articles. Only applies when Show Article Count is enabled." + +MOD_MOKOWAAS_CATEGORIES_MENUITEM_LABEL="Target Menu Item" +MOD_MOKOWAAS_CATEGORIES_MENUITEM_DESC="The menu item to use as the base for category links. This sets the Itemid parameter for proper template and menu highlighting." + +MOD_MOKOWAAS_CATEGORIES_ORDER_LABEL="Category Ordering" +MOD_MOKOWAAS_CATEGORIES_ORDER_DESC="How to sort categories within each level." +MOD_MOKOWAAS_CATEGORIES_ORDER_TREE="Tree Order (default)" +MOD_MOKOWAAS_CATEGORIES_ORDER_TITLE="Alphabetical" +MOD_MOKOWAAS_CATEGORIES_ORDER_CREATED="Date Created" diff --git a/src/packages/mod_mokowaas_categories/language/en-GB/mod_mokowaas_categories.sys.ini b/src/packages/mod_mokowaas_categories/language/en-GB/mod_mokowaas_categories.sys.ini new file mode 100644 index 00000000..4338989f --- /dev/null +++ b/src/packages/mod_mokowaas_categories/language/en-GB/mod_mokowaas_categories.sys.ini @@ -0,0 +1,2 @@ +MOD_MOKOWAAS_CATEGORIES="MokoWaaS Categories" +MOD_MOKOWAAS_CATEGORIES_DESC="Auto-discovers article categories and renders them as a collapsible tree menu. Ideal for knowledge base and help sections." diff --git a/src/packages/mod_mokowaas_categories/mod_mokowaas_categories.xml b/src/packages/mod_mokowaas_categories/mod_mokowaas_categories.xml new file mode 100644 index 00000000..4fefebe0 --- /dev/null +++ b/src/packages/mod_mokowaas_categories/mod_mokowaas_categories.xml @@ -0,0 +1,76 @@ + + + mod_mokowaas_categories + Moko Consulting + 2026-06-06 + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + hello@mokoconsulting.tech + https://mokoconsulting.tech + 02.34.10-dev + MOD_MOKOWAAS_CATEGORIES_DESC + Moko\Module\MokoWaaSCategories + + + services + src + tmpl + + + + en-GB/mod_mokowaas_categories.ini + en-GB/mod_mokowaas_categories.sys.ini + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/src/packages/mod_mokowaas_categories/services/provider.php b/src/packages/mod_mokowaas_categories/services/provider.php new file mode 100644 index 00000000..0ef768ba --- /dev/null +++ b/src/packages/mod_mokowaas_categories/services/provider.php @@ -0,0 +1,25 @@ +registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSCategories')); + $container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoWaaSCategories\\Administrator\\Helper')); + $container->registerServiceProvider(new Module()); + } +}; diff --git a/src/packages/mod_mokowaas_categories/src/Dispatcher/Dispatcher.php b/src/packages/mod_mokowaas_categories/src/Dispatcher/Dispatcher.php new file mode 100644 index 00000000..07857915 --- /dev/null +++ b/src/packages/mod_mokowaas_categories/src/Dispatcher/Dispatcher.php @@ -0,0 +1,32 @@ +getHelperFactory()->getHelper('CategoriesHelper'); + + $data['categories'] = $helper->getCategories($params); + + return $data; + } +} diff --git a/src/packages/mod_mokowaas_categories/src/Helper/CategoriesHelper.php b/src/packages/mod_mokowaas_categories/src/Helper/CategoriesHelper.php new file mode 100644 index 00000000..c33da3ec --- /dev/null +++ b/src/packages/mod_mokowaas_categories/src/Helper/CategoriesHelper.php @@ -0,0 +1,148 @@ +get(DatabaseInterface::class); + + $rootId = (int) $params->get('root_category', 0); + $maxDepth = (int) $params->get('max_depth', 3); + $showEmpty = (int) $params->get('show_empty', 0); + $showCount = (int) $params->get('show_article_count', 1); + $ordering = $params->get('ordering', 'lft'); + $user = Factory::getApplication()->getIdentity(); + $accessLevels = $user->getAuthorisedViewLevels(); + + // Build base query + $query = $db->getQuery(true) + ->select([ + $db->quoteName('c.id'), + $db->quoteName('c.title'), + $db->quoteName('c.alias'), + $db->quoteName('c.parent_id'), + $db->quoteName('c.level'), + $db->quoteName('c.lft'), + $db->quoteName('c.rgt'), + $db->quoteName('c.description'), + ]) + ->from($db->quoteName('#__categories', 'c')) + ->where($db->quoteName('c.extension') . ' = ' . $db->quote('com_content')) + ->where($db->quoteName('c.published') . ' = 1') + ->whereIn($db->quoteName('c.access'), $accessLevels); + + // If a root category is set, constrain to its subtree + if ($rootId > 0) + { + $rootQuery = $db->getQuery(true) + ->select([$db->quoteName('lft'), $db->quoteName('rgt'), $db->quoteName('level')]) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = ' . $rootId); + $db->setQuery($rootQuery); + $root = $db->loadObject(); + + if (!$root) + { + return []; + } + + $query->where($db->quoteName('c.lft') . ' > ' . (int) $root->lft) + ->where($db->quoteName('c.rgt') . ' < ' . (int) $root->rgt) + ->where($db->quoteName('c.level') . ' <= ' . ((int) $root->level + $maxDepth)); + } + else + { + // No root — show from level 1 (skip the virtual root) + $query->where($db->quoteName('c.level') . ' >= 1') + ->where($db->quoteName('c.level') . ' <= ' . $maxDepth); + } + + // Article count subquery + if ($showCount) + { + $countSub = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__content', 'a')) + ->where($db->quoteName('a.catid') . ' = ' . $db->quoteName('c.id')) + ->where($db->quoteName('a.state') . ' = 1'); + $query->select('(' . $countSub . ') AS ' . $db->quoteName('article_count')); + } + + // Ordering + $validOrders = ['lft', 'title', 'created_time']; + $orderCol = \in_array($ordering, $validOrders, true) ? $ordering : 'lft'; + $query->order($db->quoteName('c.' . $orderCol) . ' ASC'); + + $db->setQuery($query); + $categories = $db->loadObjectList() ?: []; + + // Filter empty categories if configured + if (!$showEmpty && $showCount) + { + $categories = array_filter($categories, function ($cat) { + return (int) $cat->article_count > 0; + }); + $categories = array_values($categories); + } + + // Build nested tree + return $this->buildTree($categories, $rootId); + } + + /** + * Build a nested tree from a flat list of categories. + * + * @param array $categories Flat list of category objects + * @param int $rootId Root category ID (0 for all) + * + * @return array Nested array with 'children' key on each node + */ + private function buildTree(array $categories, int $rootId): array + { + $map = []; + $tree = []; + + foreach ($categories as $cat) + { + $cat->children = []; + $map[$cat->id] = $cat; + } + + foreach ($categories as $cat) + { + $parentId = (int) $cat->parent_id; + + if (isset($map[$parentId])) + { + $map[$parentId]->children[] = $cat; + } + else + { + $tree[] = $cat; + } + } + + return $tree; + } +} diff --git a/src/packages/mod_mokowaas_categories/tmpl/default.php b/src/packages/mod_mokowaas_categories/tmpl/default.php new file mode 100644 index 00000000..81745845 --- /dev/null +++ b/src/packages/mod_mokowaas_categories/tmpl/default.php @@ -0,0 +1,138 @@ +get('show_article_count', 1)); +$menuItemId = (int) $params->get('menu_item_id', 0); + +if (empty($categories)) +{ + return; +} + +// Detect active category from current URL +$app = \Joomla\CMS\Factory::getApplication(); +$activeCatId = (int) $app->input->getInt('id', 0); +$currentView = $app->input->getCmd('view', ''); +$isCatView = \in_array($currentView, ['category', 'categories'], true); + +/** + * Build the link for a category. + */ +$buildLink = function (object $cat) use ($menuItemId): string { + $link = 'index.php?option=com_content&view=category&id=' . (int) $cat->id; + + if ($menuItemId) + { + $link .= '&Itemid=' . $menuItemId; + } + + return Route::_($link); +}; + +/** + * Check if a category or any of its descendants is the active category. + */ +$isActiveOrAncestor = function (object $cat) use ($activeCatId, $isCatView, &$isActiveOrAncestor): bool { + if (!$isCatView || !$activeCatId) + { + return false; + } + + if ((int) $cat->id === $activeCatId) + { + return true; + } + + foreach ($cat->children as $child) + { + if ($isActiveOrAncestor($child)) + { + return true; + } + } + + return false; +}; + +/** + * Render a category list recursively. + */ +$renderTree = function (array $categories, int $depth = 1) use ( + &$renderTree, $buildLink, $isActiveOrAncestor, $showCount, $activeCatId, $isCatView +): void { + foreach ($categories as $cat): + $hasChildren = !empty($cat->children); + $isActive = $isCatView && (int) $cat->id === $activeCatId; + $isAncestor = $hasChildren && $isActiveOrAncestor($cat); + $liClass = 'item mokowaas-cat-item mokowaas-cat-level-' . $depth; + + if ($isActive) + { + $liClass .= ' mm-active'; + } + + if ($hasChildren) + { + $liClass .= ' parent'; + } + + $aClass = ($hasChildren ? 'has-arrow' : 'no-dropdown'); + + if ($isActive) + { + $aClass .= ' mm-active'; + } + + $collapseClass = 'collapse-cat-level-' . ($depth + 1) . ' mm-collapse'; + + if ($isAncestor || $isActive) + { + $collapseClass .= ' mm-show'; + } + + $count = isset($cat->article_count) ? (int) $cat->article_count : 0; + ?> +
  • + > + + title); ?> + + + + + + + +
  • + + + + + diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml index cab7478c..0608b897 100644 --- a/src/pkg_mokowaas.xml +++ b/src/pkg_mokowaas.xml @@ -24,6 +24,7 @@ mod_mokowaas_cpanel.zip mod_mokowaas_menu.zip mod_mokowaas_cache.zip + mod_mokowaas_categories.zip plg_webservices_mokowaas.zip plg_webservices_perfectpublisher.zip plg_task_mokowaasdemo.zip