Skip to content

Commit 0ac3e05

Browse files
committed
feat: add a search for categories in category select boxes (#3563)
1 parent b9ed752 commit 0ac3e05

File tree

14 files changed

+272
-122
lines changed

14 files changed

+272
-122
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"bootstrap-datepicker": "^1.10.0",
3838
"bootstrap-icons": "^1.13.1",
3939
"chart.js": "^4.5.0",
40+
"choices.js": "^11.1.0",
4041
"handlebars": "4.7.8",
4142
"highlight.js": "^11.11.1",
4243
"jodit": "^4.6.2",

phpmyfaq/add.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
use phpMyFAQ\Enums\PermissionType;
2121
use phpMyFAQ\Filter;
2222
use phpMyFAQ\Forms;
23-
use phpMyFAQ\Helper\CategoryHelper as HelperCategory;
2423
use phpMyFAQ\Question;
2524
use phpMyFAQ\Strings;
2625
use phpMyFAQ\System;
2726
use phpMyFAQ\Twig\TwigWrapper;
2827
use phpMyFAQ\Translation;
2928
use Symfony\Component\HttpFoundation\RedirectResponse;
3029
use Symfony\Component\HttpFoundation\Request;
30+
use Twig\TwigFilter;
3131

3232
if (!defined('IS_VALID_PHPMYFAQ')) {
3333
http_response_code(400);
@@ -78,20 +78,21 @@
7878
$displayFullForm = true;
7979
}
8080

81+
$category = new Category($faqConfig, $currentGroups);
82+
$category->transform(0);
8183
$category->buildCategoryTree();
8284

83-
$categoryHelper = new HelperCategory();
84-
$categoryHelper->setCategory($category);
85-
8685
$captchaHelper = $container->get('phpmyfaq.captcha.helper.captcha_helper');
8786

8887
$forms = new Forms($faqConfig);
8988
$formData = $forms->getFormData(FormIds::ADD_NEW_FAQ->value);
9089

91-
$category = new Category($faqConfig);
9290
$categories = $category->getAllCategoryIds();
9391

9492
$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/');
93+
$twig->addFilter(new TwigFilter('repeat', function ($string, $times) {
94+
return str_repeat($string, $times);
95+
}));
9596
$twigTemplate = $twig->loadTemplate('./add.twig');
9697

9798
// Twig template variables
@@ -108,7 +109,8 @@
108109
'msgNewContentName' => Translation::get('msgNewContentName'),
109110
'msgNewContentMail' => Translation::get('msgNewContentMail'),
110111
'msgNewContentCategory' => Translation::get('msgNewContentCategory'),
111-
'renderCategoryOptions' => $categoryHelper->renderOptions($selectedCategory),
112+
'selectedCategory' => $selectedCategory,
113+
'categories' => $category->getCategoryTree(),
112114
'msgNewContentTheme' => Translation::get('msgNewContentTheme'),
113115
'readonly' => $readonly,
114116
'printQuestion' => $question,

phpmyfaq/ask.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
* @since 2002-09-17
1616
*/
1717

18+
use phpMyFAQ\Category;
1819
use phpMyFAQ\Enums\Forms\FormIds;
1920
use phpMyFAQ\Filter;
2021
use phpMyFAQ\Forms;
21-
use phpMyFAQ\Helper\CategoryHelper as HelperCategory;
2222
use phpMyFAQ\Twig\TwigWrapper;
2323
use phpMyFAQ\Translation;
2424
use Symfony\Component\HttpFoundation\RedirectResponse;
2525
use Symfony\Component\HttpFoundation\Request;
26+
use Twig\TwigFilter;
2627

2728
if (!defined('IS_VALID_PHPMYFAQ')) {
2829
http_response_code(400);
@@ -47,13 +48,12 @@
4748

4849
$faqSession->userTracking('ask_question', 0);
4950

51+
$category = new Category($faqConfig, $currentGroups);
52+
$category->transform(0);
5053
$category->buildCategoryTree();
5154

5255
$categoryId = Filter::filterVar($request->query->get('category_id'), FILTER_VALIDATE_INT, 0);
5356

54-
$categoryHelper = new HelperCategory();
55-
$categoryHelper->setCategory($category);
56-
5757
$captchaHelper = $container->get('phpmyfaq.captcha.helper.captcha_helper');
5858

5959
$forms = new Forms($faqConfig);
@@ -62,6 +62,9 @@
6262
$categories = $category->getAllCategoryIds();
6363

6464
$twig = new TwigWrapper(PMF_ROOT_DIR . '/assets/templates/');
65+
$twig->addFilter(new TwigFilter('repeat', function ($string, $times) {
66+
return str_repeat($string, $times);
67+
}));
6568
$twigTemplate = $twig->loadTemplate('./ask.twig');
6669

6770
$templateVars = [
@@ -73,7 +76,8 @@
7376
'lang' => $Language->getLanguage(),
7477
'defaultContentMail' => ($user->getUserId() > 0) ? $user->getUserData('email') : '',
7578
'defaultContentName' => ($user->getUserId() > 0) ? $user->getUserData('display_name') : '',
76-
'renderCategoryOptions' => $categoryHelper->renderOptions($categoryId),
79+
'selectedCategory' => $categoryId,
80+
'categories' => $category->getCategoryTree(),
7781
'captchaFieldset' =>
7882
$captchaHelper->renderCaptcha($captcha, 'ask', Translation::get('msgCaptcha'), $user->isLoggedIn()),
7983
'msgNewContentSubmit' => Translation::get('msgNewContentSubmit'),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import 'choices.js/src/styles/choices';

phpmyfaq/assets/scss/style.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
@import 'layout/category';
3737
@import 'layout/faq';
3838
@import 'layout/navigation';
39+
@import 'layout/search';
3940
@import 'layout/setup';
4041
@import 'layout/startpage';
4142

phpmyfaq/assets/src/frontend.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
handleShowFaq,
2424
handleUserVoting,
2525
} from './faq';
26-
import { handleAutoComplete, handleQuestion } from './search';
26+
import { handleAutoComplete, handleCategorySelection, handleQuestion } from './search';
2727
import {
2828
handleDeleteBookmarks,
2929
handleRegister,
@@ -36,9 +36,9 @@ import { calculateReadingTime, handlePasswordStrength, handlePasswordToggle, han
3636
import './utils/tooltip';
3737
import { handleWebAuthn } from './webauthn/webauthn';
3838

39-
document.addEventListener('DOMContentLoaded', () => {
39+
document.addEventListener('DOMContentLoaded', (): void => {
4040
// Reload Captchas
41-
const reloadButton = document.querySelector('#captcha-button');
41+
const reloadButton: HTMLButtonElement | null = document.querySelector('#captcha-button');
4242
if (reloadButton !== null) {
4343
handleReloadCaptcha(reloadButton);
4444
}
@@ -48,7 +48,7 @@ document.addEventListener('DOMContentLoaded', () => {
4848
handlePasswordStrength();
4949

5050
// Calculate reading time
51-
const faqBody = document.querySelector('.pmf-faq-body');
51+
const faqBody: HTMLElement | null = document.querySelector('.pmf-faq-body');
5252
if (faqBody !== null) {
5353
calculateReadingTime();
5454
}
@@ -74,7 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
7474
handleDeleteBookmarks();
7575
handleRemoveAllBookmarks();
7676

77-
// Handle user control panel
77+
// Handle the user control panel
7878
handleUserControlPanel();
7979

8080
// Handle user password
@@ -91,11 +91,12 @@ document.addEventListener('DOMContentLoaded', () => {
9191
handleWebAuthn();
9292

9393
// Masonry on startpage
94-
const masonryElement = document.querySelector('.masonry-grid');
94+
const masonryElement: HTMLElement | null = document.querySelector('.masonry-grid');
9595
if (masonryElement) {
9696
new Masonry(masonryElement, { columnWidth: 0 });
9797
}
9898

9999
// AutoComplete
100100
handleAutoComplete();
101+
handleCategorySelection();
101102
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Category selection functionality for the search page
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public License,
5+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
6+
* obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* @package phpMyFAQ
9+
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
10+
* @copyright 2025 phpMyFAQ Team
11+
* @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
12+
* @link https://www.phpmyfaq.de
13+
* @since 2025-07-27
14+
*/
15+
16+
import Choices from 'choices.js';
17+
18+
export const handleCategorySelection = (): void => {
19+
const element: HTMLElement | null = document.getElementById('pmf-search-category');
20+
if (element) {
21+
new Choices(element, {
22+
silent: false,
23+
items: [],
24+
choices: [],
25+
renderChoiceLimit: -1,
26+
maxItemCount: -1,
27+
closeDropdownOnSelect: 'auto',
28+
singleModeForMultiSelect: false,
29+
addChoices: false,
30+
addItems: true,
31+
addItemFilter: (value: string): boolean => !!value && value !== '',
32+
removeItems: true,
33+
removeItemButton: false,
34+
removeItemButtonAlignLeft: false,
35+
editItems: false,
36+
allowHTML: false,
37+
allowHtmlUserInput: false,
38+
duplicateItemsAllowed: true,
39+
delimiter: ',',
40+
paste: true,
41+
searchEnabled: true,
42+
searchChoices: true,
43+
searchFloor: 1,
44+
searchResultLimit: 4,
45+
searchFields: ['label', 'value'],
46+
position: 'auto',
47+
resetScrollPosition: true,
48+
shouldSort: true,
49+
shouldSortItems: false,
50+
shadowRoot: null,
51+
placeholder: true,
52+
placeholderValue: null,
53+
searchPlaceholderValue: 'Type to search categories',
54+
prependValue: null,
55+
appendValue: null,
56+
renderSelectedChoices: 'auto',
57+
loadingText: 'Loading...',
58+
noResultsText: 'No results found',
59+
noChoicesText: 'No choices to choose from',
60+
itemSelectText: 'Press to select',
61+
uniqueItemText: 'Only unique values can be added',
62+
customAddItemText: 'Only values matching specific conditions can be added',
63+
valueComparer: (value1: string, value2: string): boolean => {
64+
return value1 === value2;
65+
},
66+
classNames: {
67+
containerOuter: ['choices'],
68+
containerInner: ['choices__inner'],
69+
input: ['choices__input'],
70+
inputCloned: ['choices__input--cloned'],
71+
list: ['choices__list'],
72+
listItems: ['choices__list--multiple'],
73+
listSingle: ['choices__list--single'],
74+
listDropdown: ['choices__list--dropdown'],
75+
item: ['choices__item'],
76+
itemSelectable: ['choices__item--selectable'],
77+
itemDisabled: ['choices__item--disabled'],
78+
itemChoice: ['choices__item--choice'],
79+
description: ['choices__description'],
80+
placeholder: ['choices__placeholder'],
81+
group: ['choices__group'],
82+
groupHeading: ['choices__heading'],
83+
button: ['choices__button'],
84+
activeState: ['is-active'],
85+
focusState: ['is-focused'],
86+
openState: ['is-open'],
87+
disabledState: ['is-disabled'],
88+
highlightedState: ['is-highlighted'],
89+
selectedState: ['is-selected'],
90+
flippedState: ['is-flipped'],
91+
loadingState: ['is-loading'],
92+
notice: ['choices__notice'],
93+
addChoice: ['choices__item--selectable', 'add-choice'],
94+
noResults: ['has-no-results'],
95+
noChoices: ['has-no-choices'],
96+
},
97+
fuseOptions: {
98+
includeScore: true,
99+
},
100+
labelId: '',
101+
callbackOnInit: null,
102+
callbackOnCreateTemplates: null,
103+
appendGroupInSearch: false,
104+
});
105+
}
106+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './autocomplete';
2+
export * from './category';
23
export * from './question';

phpmyfaq/assets/templates/default/add.twig

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@
5454
</label>
5555
<div class="col-sm-9">
5656
<select name="rubrik" class="form-control" id="rubrik" multiple="multiple" size="5" {{ id5_required }}>
57-
{{ renderCategoryOptions|raw }}
57+
{% for cat in categories %}
58+
{% set indent = '...' | repeat(cat.indent) %}
59+
<option value="{{ cat.id }}" {% if cat.id in categories %}selected{% endif %}>
60+
{{ indent }} {{ cat.name }}
61+
</option>
62+
{% endfor %}
5863
</select>
5964
</div>
6065
</div>

phpmyfaq/assets/templates/default/ask.twig

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@
5555
</label>
5656
<div class="col-sm-9">
5757
<select name="category" class="form-select" id="category" {{ id5_required }}>
58-
{{ renderCategoryOptions|raw }}
58+
{{ dump(categories) }}
59+
60+
{% for cat in categories %}
61+
{% set indent = '...' | repeat(cat.indent) %}
62+
<option value="{{ cat.id }}" {% if cat.id in categories %}selected{% endif %}>
63+
{{ indent }} {{ cat.name }}
64+
</option>
65+
{% endfor %}
5966
</select>
6067
</div>
6168
</div>

0 commit comments

Comments
 (0)