Skip to content

Commit 514e3cf

Browse files
committed
feat(treeview): add a search field to lists
This change came about because of the difficulty of finding items in really long dropdown lists. We can make a fairly quick solution to this using the existing filtering functions, tied to an input element which sticks to the top of the dropdown list... I'm a little unconvinced by using position: sticky, since as overscroll will cause the search bar to move (which is cosmetically weird, though not actually a functional problem). Nevertheless, I couldn't find something obviously better without making a risky change to treeviews at large. I suspect you can do some position: absolute thing to minimize trouble, but whatever it is it won't be clean. This depends on https://gerrit.libreoffice.org/c/core/+/195719 to hide the search field with checkbox lists to avoid duplicates there We also explicitly ignore some dropdown lists - the search results and the mention popup - since as having a search there doesn't make a lot of sense... Signed-off-by: Skyler Grey <[email protected]> Change-Id: Ib8000bc3e5566993fa6eea89adcc617b6a6a6964
1 parent b6d0770 commit 514e3cf

File tree

7 files changed

+90
-6
lines changed

7 files changed

+90
-6
lines changed

browser/css/jsdialogs.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,33 @@ algned to the bottom */
640640
scrollbar-color: var(--scrollbar-color);
641641
}
642642

643+
.ui-treeview:has(> .ui-treeview-search-container) {
644+
padding-top: 0;
645+
/* otherwise position: sticky will leave a gap. We can't top: -1 on the search container or it'll scroll... */
646+
}
647+
648+
.ui-treeview-search-container {
649+
/* we're using position: sticky at all since as we don't want to nest this in
650+
another div and we don't want to scroll offscreen... it's a little ugly, but I
651+
didn't think position: fixed and margin hacks would go down any more nicely...
652+
*/
653+
position: sticky;
654+
top: 0;
655+
grid-column: 1 / -1;
656+
background-color: var(--color-background-lighter);
657+
658+
/* margin-left and margin-right can counterbalance the .ui-treeview padding here */
659+
margin-left: -1px;
660+
margin-right: -1px;
661+
padding-left: 1px;
662+
padding-right: 1px;
663+
}
664+
665+
[id^='ui-treeview-search-input'] {
666+
width: 100%;
667+
box-sizing: border-box;
668+
}
669+
643670
#__MENU__.ui-treeview {
644671
min-height: min-content;
645672
}

browser/src/canvas/sections/CommentListSection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ export class CommentSection extends CanvasSectionObject {
560560
fireKeyEvents: true,
561561
hideIfEmpty: true,
562562
entries: [] as Array<TreeEntryJSON>,
563+
noSearchField: true,
563564
},
564565
{
565566
id: '',

browser/src/control/AutoCompletePopup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ abstract class AutoCompletePopup {
9999
singleclickactivate: false,
100100
fireKeyEvents: true,
101101
entries: [] as Array<TreeEntryJSON>,
102+
noSearchField: true,
102103
} as TreeWidgetJSON;
103104
}
104105

browser/src/control/Control.QuickFindPanel.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ class QuickFindPanel extends SidebarBase {
3636
if (
3737
data.control.id === 'searchfinds' &&
3838
data.control.type === 'treelistbox'
39-
)
39+
) {
4040
e.data.control.ignoreFocus = true;
41+
e.data.control.noSearchField = true;
42+
}
4143

4244
if (!super.onJSUpdate(e)) return false;
4345

@@ -75,6 +77,30 @@ class QuickFindPanel extends SidebarBase {
7577
quickFindControls.classList.toggle('hidden', isEmpty);
7678
}
7779

80+
removeSearchFields(quickFindData: any): any {
81+
if (quickFindData.children) {
82+
const modifiedData = JSON.parse(JSON.stringify(quickFindData));
83+
84+
const _removeSearchFields = (data: any) => {
85+
for (const child of data.children) {
86+
if (child.type === 'treelistbox') {
87+
child.noSearchField = true;
88+
}
89+
90+
if (child.children) {
91+
_removeSearchFields(child);
92+
}
93+
}
94+
};
95+
96+
_removeSearchFields(modifiedData);
97+
98+
return modifiedData;
99+
}
100+
101+
return quickFindData;
102+
}
103+
78104
addPlaceholderIfEmpty(quickFindData: any): any {
79105
const hasEntries =
80106
quickFindData.children &&
@@ -114,7 +140,8 @@ class QuickFindPanel extends SidebarBase {
114140

115141
this.container.innerHTML = '';
116142

117-
const modifiedData = this.addPlaceholderIfEmpty(quickFindData);
143+
let modifiedData = this.removeSearchFields(quickFindData);
144+
modifiedData = this.addPlaceholderIfEmpty(modifiedData);
118145

119146
this.builder?.build(this.container, [modifiedData], false);
120147

browser/src/control/jsdialog/Definitions.Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ interface TreeWidgetJSON extends WidgetJSON {
376376
highlightTerm?: string; // what, if any, entries are we highlighting?
377377
ignoreFocus?: boolean; // When true, does't focus to selected item automatically.
378378
customEntryRenderer?: boolean;
379+
noSearchField?: boolean; // When true, the widget shouldn't have a search field added
379380
}
380381

381382
interface IconViewEntry {

browser/src/control/jsdialog/Widget.TreeView.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class TreeViewControl {
7373
_tbody: HTMLElement;
7474
_thead: HTMLElement = null;
7575
_columns: number;
76+
_maxColumnsIncludingState: number = 0;
7677
_hasState: boolean;
7778
_hasIcon: boolean;
7879
_isNavigator: boolean;
@@ -369,6 +370,9 @@ class TreeViewControl {
369370
let dummyColumns = 0;
370371
if (this._hasState) dummyColumns++;
371372
tr.style.gridColumn = '1 / ' + (this._columns + dummyColumns + 1);
373+
if (this._columns + dummyColumns + 1 > this._maxColumnsIncludingState) {
374+
this._maxColumnsIncludingState = this._columns + dummyColumns + 1;
375+
}
372376
if (
373377
this.isPageDivider(entry, this.PAGE_ENTRY_PREFIX, this.PAGE_ENTRY_SUFFIX)
374378
) {
@@ -1620,6 +1624,22 @@ class TreeViewControl {
16201624
if (level === 1 && !hasSelectedEntry) this.makeTreeViewFocusable(true);
16211625
}
16221626

1627+
showSearchBar(parent: HTMLElement) {
1628+
const searchBox = document.createElement('input');
1629+
searchBox.id = JSDialog.MakeIdUnique('ui-treeview-search-input'); // Form fields should have either a name or an ID - using this instead of a class
1630+
searchBox.placeholder = 'Search...';
1631+
searchBox.addEventListener('input', () =>
1632+
this.filterEntries(searchBox.value),
1633+
);
1634+
1635+
const searchContainer = document.createElement('div');
1636+
searchContainer.className = 'ui-treeview-search-container';
1637+
searchContainer.style.gridColumn = '1 / ' + this._maxColumnsIncludingState;
1638+
searchContainer.appendChild(searchBox);
1639+
1640+
parent.insertAdjacentElement('afterbegin', searchContainer);
1641+
}
1642+
16231643
fillEntry(
16241644
data: TreeWidgetJSON,
16251645
entry: TreeEntryJSON,
@@ -1643,6 +1663,9 @@ class TreeViewControl {
16431663
let dummyColumns = 0;
16441664
if (this._hasState) dummyColumns++;
16451665
subGrid.style.gridColumn = '1 / ' + (this._columns + dummyColumns + 1);
1666+
if (this._columns + dummyColumns + 1 > this._maxColumnsIncludingState) {
1667+
this._maxColumnsIncludingState = this._columns + dummyColumns + 1;
1668+
}
16461669

16471670
this.fillEntries(data, entry.children, builder, level + 1, subGrid);
16481671
}
@@ -1776,6 +1799,10 @@ class TreeViewControl {
17761799
this.fillHeaders(data.headers, builder);
17771800
this.fillEntries(data, data.entries, builder, 1, this._tbody);
17781801

1802+
if (this._isListbox && !data.noSearchField) {
1803+
this.showSearchBar(this._container);
1804+
}
1805+
17791806
return true;
17801807
}
17811808
}

cypress_test/integration_tests/desktop/calc/autofilter_spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe(['tagdesktop', 'tagnextcloud', 'tagproxy'], 'AutoFilter', function() {
102102
it('Filter empty/non-empty cells', function() {
103103
//empty
104104
calcHelper.openAutoFilterMenu(true);
105-
cy.cGet('#check_list_box .ui-treeview-entry:nth-child(1) > div > input').click();
105+
cy.cGet('#check_list_box :nth-child(1 of .ui-treeview-entry) > div > input').click();
106106
cy.cGet('#ok').click();
107107
// Wait for autofilter dialog to close
108108
cy.cGet('div.autofilter').should('not.exist');
@@ -165,16 +165,16 @@ describe(['tagdesktop', 'tagnextcloud', 'tagproxy'], 'AutoFilter', function() {
165165
it('Disable already filtered', function () {
166166
// Filter row with ['Test 4', ''] on the first column
167167
calcHelper.openAutoFilterMenu();
168-
cy.cGet('#check_list_box .ui-treeview-entry:nth-child(4) > div > input').click();
168+
cy.cGet('#check_list_box :nth-child(4 of .ui-treeview-entry) > div > input').click();
169169
cy.cGet('#ok').click();
170170
// Wait for autofilter dialog to close
171171
cy.cGet('div.autofilter').should('not.exist');
172172

173173
// Open autofilter menu on the second column
174174
calcHelper.openAutoFilterMenu(true);
175175
// Check that '(empty)' option is disabled
176-
cy.cGet('#check_list_box .ui-treeview-entry:nth-child(3) > div').should('contain.text', '(empty)');
177-
cy.cGet('#check_list_box .ui-treeview-entry:nth-child(3) > div > input').should('be.disabled');
176+
cy.cGet('#check_list_box :nth-child(3 of .ui-treeview-entry) > div').should('contain.text', '(empty)');
177+
cy.cGet('#check_list_box :nth-child(3 of .ui-treeview-entry) > div > input').should('be.disabled');
178178

179179
});
180180
});

0 commit comments

Comments
 (0)