Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .php-cs-fixer.cache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"php":"8.3.25","version":"3.64.0:v3.64.0#58dd9c931c785a79739310aef5178928305ffa67","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":true,"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"single_quote":true,"trailing_comma_in_multiline":true},"hashes":{"fragments\/QuickNavigation\/Dropdown.php":"7cc52d767a1b8fa704e4d89c9a3e48a7","fragments\/QuickNavigation\/List.php":"e90ddecd29ce153eec1083841a3088e2","fragments\/QuickNavigation\/NoResult.php":"9b879a1abf902a8aa81cb9e6ce5d921a","fragments\/QuickNavigation\/MinibarList.php":"6c40eb3f991f48d730f45b8d222914b1","pages\/config.php":"cae53f73bf533cc8bb59dadf1cbdebe4","pages\/index.php":"daf2b1a819e7a27391553c599d053802","lib\/LinkMap\/QuickNavigationLinkMap.php":"3060d0975067e724f1dd427eadb02d47","lib\/Utility\/BuildNavigationArray.php":"d2a23cc394de4fd4e83bb34da8b3c0bc","lib\/Minibar\/ArticleHistoryElement.php":"fa606bdfb8e44afdcd46695f36b5d327","lib\/QuickNavigationApi.php":"4eabc25eae94790b9095085102dc278a","lib\/ApiFunction\/MenuRender.php":"fa555351a229780f110eba970ed42a59","lib\/ApiFunction\/MediaSearch.php":"04d4e816aa6766f1f2bf889482187287","lib\/Button\/FavoriteButton.php":"c8163f9adb12699453fe458de325515e","lib\/Button\/ButtonRegistry.php":"0ecf7951feeb0b3524fa1b8c92ac6288","lib\/Button\/YformButton.php":"cb3c1d7608735d18b5732b7425f3c14a","lib\/Button\/ArticleHistoryButton.php":"3851f59535b0647f184a5d706da08aba","lib\/Button\/ButtonInterface.php":"943bf3db82d39b6854401e16fe7e36b6","lib\/Button\/ArticleNavigationButton.php":"a81f2e97e73f235cba29ffcad4db1854","lib\/Button\/WatsonButton.php":"ef0d3641591e83b6e7dfec54008e36a8","lib\/Button\/CategoryButton.php":"3854696ad9b32dffad7d25d881fd2b18","lib\/Media\/MediaSorter.php":"4af98d56992a2e58b4d440ff1545af50","lib\/Media\/QuickNavigationMedia.php":"aa21d3042a12fc77d87e15ccb9c66319","lib\/QuickNavigation.php":"9a208b43632faa9d39923ff64c22f056","update.php":"d6e8377314743b0306a045ec165b99ea","boot.php":"85e4410621c51370487316f2c7de01b4"}}
{"php":"8.4.12","version":"3.64.0:v3.64.0#58dd9c931c785a79739310aef5178928305ffa67","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"new_with_parentheses":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":true,"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"visibility_required":true,"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"no_unused_imports":true,"single_quote":true,"trailing_comma_in_multiline":true},"hashes":{"update.php":"599fd370563dc5811b440c0ae86309d7","fragments\/QuickNavigation\/NoResult.php":"9b879a1abf902a8aa81cb9e6ce5d921a","fragments\/QuickNavigation\/MinibarList.php":"6c40eb3f991f48d730f45b8d222914b1","fragments\/QuickNavigation\/List.php":"e90ddecd29ce153eec1083841a3088e2","fragments\/QuickNavigation\/Dropdown.php":"7cc52d767a1b8fa704e4d89c9a3e48a7","lib\/QuickNavigationApi.php":"4eabc25eae94790b9095085102dc278a","lib\/ApiFunction\/MenuRender.php":"fa555351a229780f110eba970ed42a59","lib\/ApiFunction\/MediaSearch.php":"04d4e816aa6766f1f2bf889482187287","lib\/QuickNavigation.php":"9a208b43632faa9d39923ff64c22f056","lib\/Minibar\/ArticleHistoryElement.php":"fa606bdfb8e44afdcd46695f36b5d327","lib\/Button\/ArticleNavigationButton.php":"a81f2e97e73f235cba29ffcad4db1854","lib\/Button\/YformButton.php":"cb3c1d7608735d18b5732b7425f3c14a","lib\/Button\/ButtonInterface.php":"943bf3db82d39b6854401e16fe7e36b6","lib\/Button\/ButtonRegistry.php":"0ecf7951feeb0b3524fa1b8c92ac6288","lib\/Button\/WatsonButton.php":"ef0d3641591e83b6e7dfec54008e36a8","lib\/Button\/ArticleHistoryButton.php":"3851f59535b0647f184a5d706da08aba","lib\/Button\/FavoriteButton.php":"c8163f9adb12699453fe458de325515e","lib\/Button\/CategoryButton.php":"3854696ad9b32dffad7d25d881fd2b18","lib\/LinkMap\/QuickNavigationLinkMap.php":"3060d0975067e724f1dd427eadb02d47","lib\/Utility\/BuildNavigationArray.php":"d2a23cc394de4fd4e83bb34da8b3c0bc","lib\/Media\/QuickNavigationMedia.php":"aa21d3042a12fc77d87e15ccb9c66319","lib\/Media\/MediaSorter.php":"4af98d56992a2e58b4d440ff1545af50","boot.php":"85e4410621c51370487316f2c7de01b4","pages\/index.php":"daf2b1a819e7a27391553c599d053802","pages\/config.php":"cae53f73bf533cc8bb59dadf1cbdebe4"}}
62 changes: 62 additions & 0 deletions assets/quick-navigation.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,65 @@
{
color: var(--quick-navigation-color-offline);
}

/* Favoriten Sections */
.quick-navigation-section-header {
padding: 10px 12px 6px;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
color: #666;
background-color: #f5f5f5;
border-top: 1px solid #e0e0e0;
margin: 0;
}

.quick-navigation-section-header:first-child {
border-top: none;
}

.quick-navigation-section-divider {
display: none;
}
Comment on lines +172 to +174
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The .quick-navigation-section-divider class is defined but set to display: none;. This suggests it's not currently being used. Consider removing this unused CSS rule or documenting why it's kept for future use.

Suggested change
.quick-navigation-section-divider {
display: none;
}

Copilot uses AI. Check for mistakes.

.quick-navigation-addon-fav {
padding: 0;
}

.quick-navigation-addon-fav a {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
color: inherit;
text-decoration: none;
}

.quick-navigation-addon-fav a:hover {
background-color: #f0f0f0;
}

.quick-navigation-addon-fav a i {
width: 16px;
text-align: center;
flex-shrink: 0;
}

/* Dark Theme Support */
.rex-theme-dark .quick-navigation-section-header {
color: #aaa;
background-color: #2a2d33;
border-top-color: #404040;
}

.rex-theme-dark .quick-navigation-addon-fav a:hover {
background-color: #35383e;
}

.rex-theme-dark .quick-navigation-menu-header {
border-bottom-color: #404040;
}

.rex-theme-dark .quick-navigation-no-results {
background-color: rgba(42, 45, 51, 0.59);
}
42 changes: 36 additions & 6 deletions boot.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,44 @@

$userId = rex::getUser()->getId();
if (rex_addon::get('quick_navigation')->getConfig('quick_navigation_artdirections' . $userId) != '1') {
ButtonRegistry::registerButton(new ArticleNavigationButton(), 10);
ButtonRegistry::registerButton(
new ArticleNavigationButton(),
10,
'article_navigation',
rex_addon::get('quick_navigation')->i18n('quick_navigation_button_article_navigation')
);
}

ButtonRegistry::registerButton(new WatsonButton(), 20);
ButtonRegistry::registerButton(new CategoryButton(), 30);
ButtonRegistry::registerButton(new ArticleHistoryButton('structure', 20), 40);
ButtonRegistry::registerButton(new YformButton(), 50);
ButtonRegistry::registerButton(new FavoriteButton(), 60);
ButtonRegistry::registerButton(
new WatsonButton(),
20,
'watson',
rex_addon::get('quick_navigation')->i18n('quick_navigation_button_watson')
);
ButtonRegistry::registerButton(
new CategoryButton(),
30,
'category',
rex_addon::get('quick_navigation')->i18n('quick_navigation_button_category')
);
ButtonRegistry::registerButton(
new ArticleHistoryButton('structure', 20),
40,
'article_history',
rex_addon::get('quick_navigation')->i18n('quick_navigation_button_article_history')
);
ButtonRegistry::registerButton(
new YformButton(),
50,
'yform',
rex_addon::get('quick_navigation')->i18n('quick_navigation_button_yform')
);
ButtonRegistry::registerButton(
new FavoriteButton(),
60,
'favorite',
rex_addon::get('quick_navigation')->i18n('quick_navigation_button_favorite')
);

// Addonrechte (permissions) registieren
rex_perm::register('quick_navigation[]');
Expand Down
4 changes: 4 additions & 0 deletions fragments/QuickNavigation/List.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@

<ul>
<?php foreach($this->getVar('listItems', []) as $listItem): ?>
<?php if (str_contains($listItem, 'quick-navigation-section-header') || str_contains($listItem, 'quick-navigation-section-divider')): ?>
<?= $listItem ?>
<?php else: ?>
<li>
<div class="quick-navigation-item">
<?= $listItem ?>
Comment on lines +10 to 15
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition str_contains($listItem, 'quick-navigation-section-header') is used to detect section headers, but this is fragile. If the section header text happens to appear in a regular list item (e.g., in a category name or addon page title), it will be incorrectly treated as a header. Consider using a more robust method, such as checking if the string starts with a specific pattern or using an array structure with type indicators instead of HTML string detection.

Suggested change
<?php if (str_contains($listItem, 'quick-navigation-section-header') || str_contains($listItem, 'quick-navigation-section-divider')): ?>
<?= $listItem ?>
<?php else: ?>
<li>
<div class="quick-navigation-item">
<?= $listItem ?>
<?php if ($listItem['type'] === 'header'): ?>
<?= $listItem['content'] ?>
<?php elseif ($listItem['type'] === 'divider'): ?>
<?= $listItem['content'] ?>
<?php else: ?>
<li>
<div class="quick-navigation-item">
<?= $listItem['content'] ?>

Copilot uses AI. Check for mistakes.
</div>
</li>
<?php endif ?>
<?php endforeach ?>
</ul>
13 changes: 13 additions & 0 deletions lang/de_de.lang
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ quick_navigation_favorite = Favoriten
quick_navigation_manage_favorite = Favoriten anlegen
quick_navigation_favorite_article_add = Artikel hinzufügen in:
quick_navigation_favorite_category_add = Kategorie hinzufügen in:
quick_navigation_addon_pages = AddOn-Seiten
quick_navigation_structure_favs = Struktur
quick_navigation_addon_pages_selection = AddOn-Seiten Favoriten
quick_navigation_yform = YForm
quick_navigation_yform_add = Datensatz hinzufügen in:

Expand All @@ -35,3 +38,13 @@ quick_navigation_media_live_search_placeholder = Live-Suche...

# Live Search
quick_navigation_media_live_search_placeholder = Live-Suche...
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate key definition. The key quick_navigation_media_live_search_placeholder is defined twice (lines 37 and 40), which will cause the second definition to override the first. Remove the duplicate entry.

Suggested change
quick_navigation_media_live_search_placeholder = Live-Suche...

Copilot uses AI. Check for mistakes.

# Button Management
quick_navigation_button_management = Buttons ausblenden (Opt-Out)
quick_navigation_button_management_note = Wähle die Buttons aus, die du NICHT sehen möchtest. Alle anderen Buttons bleiben sichtbar.
quick_navigation_button_article_navigation = Artikelnavigation (Blättern)
quick_navigation_button_watson = Watson
quick_navigation_button_category = Kategorie
quick_navigation_button_article_history = Artikel-Historie
quick_navigation_button_yform = YForm
quick_navigation_button_favorite = Favoriten
13 changes: 13 additions & 0 deletions lang/en_gb.lang
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ quick_navigation_favorite = Favorites
quick_navigation_manage_favorite = Add favorites
quick_navigation_favorite_article_add = Add article to:
quick_navigation_favorite_category_add = Add category to:
quick_navigation_addon_pages = AddOn Pages
quick_navigation_structure_favs = Structure
quick_navigation_addon_pages_selection = AddOn Pages Favorites
quick_navigation_yform = YForm
quick_navigation_yform_add = Add dataset to:

Expand All @@ -32,3 +35,13 @@ quick_navigation_media_sort_title = Sort by title (A-Z)
# Media Live-Search
quick_navigation_media_livesearch = Enable media live search?
quick_navigation_media_live_search_placeholder = Live search...

# Button Management
quick_navigation_button_management = Hide buttons (Opt-Out)
quick_navigation_button_management_note = Select the buttons you DON'T want to see. All other buttons remain visible.
quick_navigation_button_article_navigation = Article Navigation (Browse)
quick_navigation_button_watson = Watson
quick_navigation_button_category = Category
quick_navigation_button_article_history = Article History
quick_navigation_button_yform = YForm
quick_navigation_button_favorite = Favorites
73 changes: 69 additions & 4 deletions lib/Button/ButtonRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,103 @@

namespace FriendsOfRedaxo\QuickNavigation\Button;

use rex;
use rex_addon;

class ButtonRegistry
{
/**
* @var array<array{instance: mixed, priority: int}> an array that contains button instances and their priorities
* @var array<array{id: string, label: string, instance: mixed, priority: int}> an array that contains button instances and their priorities
*/
protected static array $buttons = [];

/**
* Registers a button with an optional priority.
* Lower priority values cause the button to appear earlier in the list.
*
* @param string $id Unique identifier for the button (e.g. 'article_navigation', 'watson')
* @param string $label Human readable label for config (e.g. 'Artikelnavigation', 'Watson')
*/
public static function registerButton(ButtonInterface $buttonInstance, int $priority = 10): void
public static function registerButton(ButtonInterface $buttonInstance, int $priority = 10, string $id = '', string $label = ''): void
{
self::$buttons[] = ['instance' => $buttonInstance, 'priority' => $priority];
// Generate ID from class name if not provided
if ($id === '') {
$className = get_class($buttonInstance);
$id = strtolower(str_replace('Button', '', substr($className, strrpos($className, '\\') + 1)));
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The automatic ID generation using strtolower(str_replace('Button', '', substr($className, strrpos($className, '\\') + 1))) could produce duplicate IDs if two button classes with the same name exist in different namespaces. For example, MyNamespace\FavoriteButton and OtherNamespace\FavoriteButton would both generate the ID 'favorite'. Consider including more of the class name or namespace in the ID generation, or documenting that unique IDs should always be explicitly provided.

Suggested change
$id = strtolower(str_replace('Button', '', substr($className, strrpos($className, '\\') + 1)));
// Use fully qualified class name, replace \ with _, strip 'Button' suffix, and lowercase
$id = strtolower(
preg_replace(
'/button$/i',
'',
str_replace('\\', '_', $className)
)
);

Copilot uses AI. Check for mistakes.
}

// Use ID as label if not provided
if ($label === '') {
$label = ucfirst(str_replace('_', ' ', $id));
}

self::$buttons[] = [
'id' => $id,
'label' => $label,
'instance' => $buttonInstance,
'priority' => $priority
];
}

/**
* Returns the buttons sorted by their priority.
* Returns the buttons sorted by their priority, filtered by user preferences.
*/
public static function getButtonsOutput(): string
{
$user = rex::getUser();
if (!$user) {
return '';
}

$userId = $user->getId();
$addon = rex_addon::get('quick_navigation');

// Get disabled buttons for this user (Opt-Out)
$disabledButtons = $addon->getConfig('quick_navigation_disabled_buttons' . $userId, []);
if (!is_array($disabledButtons)) {
$disabledButtons = [];
}

// Sorts the buttons based on their priority
usort(self::$buttons, static function (array $a, array $b): int {
return $a['priority'] <=> $b['priority'];
});

$resultString = '';
foreach (self::$buttons as $button) {
// Skip if button is disabled for this user
if (in_array($button['id'], $disabledButtons, true)) {
continue;
}

// Since all instances implement ButtonInterface, it's guaranteed that get() exists.
$resultString .= $button['instance']->get();
}

return $resultString;
}

/**
* Returns all available buttons with their metadata for configuration.
* @return array<array{id: string, label: string, priority: int}>
*/
public static function getAvailableButtons(): array
{
// Sort by priority
$sortedButtons = self::$buttons;
usort($sortedButtons, static function (array $a, array $b): int {
return $a['priority'] <=> $b['priority'];
});

$result = [];
foreach ($sortedButtons as $button) {
$result[] = [
'id' => $button['id'],
'label' => $button['label'],
'priority' => $button['priority']
];
}

return $result;
}
}
Loading