Skip to content

Commit

Permalink
Add menu search feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Jun 28, 2024
1 parent 75d6219 commit 1a38009
Show file tree
Hide file tree
Showing 10 changed files with 1,940 additions and 0 deletions.
56 changes: 56 additions & 0 deletions packages/ckeditor5-ui/src/button/buttonlabelwithhighlightview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module ui/button/buttonlabelwithhighlightview
*/

import type ButtonLabel from './buttonlabel.js';
import HighlightedTextView from '../highlightedtext/highlightedtextview.js';

/**
* A button label view that can highlight a text fragment.
*/
export default class ButtonLabelWithHighlightView extends HighlightedTextView implements ButtonLabel {
/**
* @inheritDoc
*/
declare public style: string | undefined;

/**
* @inheritDoc
*/
declare public text: string | undefined;

/**
* @inheritDoc
*/
declare public id: string | undefined;

/**
* @inheritDoc
*/
constructor() {
super();

this.set( {
style: undefined,
text: undefined,
id: undefined
} );

const bind = this.bindTemplate;

this.extendTemplate( {
attributes: {
class: [
'ck-button__label'
],
style: bind.to( 'style' ),
id: bind.to( 'id' )
}
} );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module ui/dropdown/menu/filterview/dropdownmenulistfilteredview
*/

import type { Editor } from '@ckeditor/ckeditor5-core';
import type { DropdownMenuDefinitions } from '../definition/dropdownmenudefinitiontypings.js';
import type FilteredView from '../../../search/filteredview.js';

import { filterDropdownMenuTreeByRegExp, type DropdownMenuSearchResult } from '../tree/dropdownmenutreefilterutils.js';

import View from '../../../view.js';
import DropdownMenuListFoundItemsView from './dropdownmenulistfounditemsview.js';
import DropdownMenuRootListView, { type DropdownMenuRootListViewAttributes } from '../dropdownmenurootlistview.js';

/**
* Represents a filtered view for a dropdown menu list.
* This class extends the `View` class and implements the `FilteredView` interface.
*/
export default class DropdownMenuListFilteredView extends View implements FilteredView {
/**
* The root list view of the dropdown menu.
*/
protected _menuView: DropdownMenuRootListView;

/**
* The found list view of the dropdown menu.
*/
protected _foundListView: DropdownMenuListFoundItemsView | null = null;

/**
* Represents a filtered view for the dropdown menu list.
*/
constructor(
editor: Editor,
definitions: DropdownMenuDefinitions,
menuAttributes?: DropdownMenuRootListViewAttributes
) {
super( editor.locale );

this._menuView = new DropdownMenuRootListView( editor, definitions, menuAttributes );
this.setTemplate( {
tag: 'div',

attributes: {
class: [
'ck',
'ck-dropdown-menu-filter'
],
tabindex: -1
},

children: [
this._menuView
]
} );
}

/**
* The root list view of the dropdown menu.
*/
public get menuView(): DropdownMenuRootListView {
return this._menuView;
}

/**
* Gets the found list view of the dropdown menu.
*/
public get foundListView(): DropdownMenuListFoundItemsView | null {
return this._foundListView;
}

/**
* Filters the dropdown menu list based on the provided regular expression.
*
* @param regExp The regular expression to filter the list.
* @returns An object containing the number of filtered results and the total number of items in the list.
*/
public filter( regExp: RegExp | null ): DropdownMenuSearchResult {
const { element } = this;

if ( regExp ) {
// Preload all menus to ensure that all items are available for filtering.
this._menuView.preloadAllMenus();
}

const { filteredTree, resultsCount, totalItemsCount } = filterDropdownMenuTreeByRegExp(
regExp,
this._menuView.tree
);

element!.innerHTML = '';

if ( this._foundListView ) {
this._foundListView.destroy();
this._foundListView = null;
}

if ( resultsCount !== totalItemsCount ) {
this._foundListView = new DropdownMenuListFoundItemsView( this.locale!, filteredTree, {
highlightRegex: regExp,
limitFoundItemsCount: 25
} );

this._menuView.close();
this._foundListView.render();

element!.appendChild( this._foundListView.element! );
} else {
element!.appendChild( this._menuView.element! );
}

return {
filteredTree,
resultsCount,
totalItemsCount
};
}

/**
* Sets the focus on the dropdown menu list.
*/
public focus(): void {
const { _menuView, _foundListView } = this;

if ( _foundListView ) {
_foundListView.focus();
} else {
_menuView.focus();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module ui/dropdown/menu/filterview/dropdownmenulistfounditemsview
*/

import type { Locale } from '@ckeditor/ckeditor5-utils';
import {
groupDropdownTreeByFirstFoundParent,
type DropdownMenusViewsFilteredFlatItem,
type DropdownMenusViewsFilteredTreeNode
} from '../tree/dropdownmenutreefilterutils.js';

import ButtonLabelWithHighlightView from '../../../button/buttonlabelwithhighlightview.js';
import ButtonView from '../../../button/buttonview.js';
import LabelWithHighlightView from '../../../label/labelwithhighlightview.js';
import ListItemGroupView from '../../../list/listitemgroupview.js';
import ListItemView from '../../../list/listitemview.js';
import ListView from '../../../list/listview.js';

/**
* Represents a view for the found list in the dropdown menu list.
*/
export default class DropdownMenuListFoundItemsView extends ListView {
/**
* The maximum number of found items to display. It prevents slow rendering when there are too many items.
*/
private readonly config: FoundItemsViewRenderConfig;

/**
* Creates a new instance of the DropdownMenuListFoundItemsView class.
*
* @param locale The locale object.
* @param tree The filtered tree node.
* @param config The configuration object.
*/
constructor(
locale: Locale,
tree: DropdownMenusViewsFilteredTreeNode,
config: FoundItemsViewRenderConfig
) {
super( locale );

this.config = config;
this.role = 'listbox';

const items = this._createFilteredTreeListBox( tree );

if ( items.length ) {
this.items.addMany( items );
}
}

/**
* Creates a filtered tree list box based on the provided highlight regex and tree data.
*
* @param highlightRegex The regular expression used for highlighting the filtered items.
* @param tree The tree data used to create the filtered tree list box.
* @returns An array of ListItemGroupView or ListItemView objects representing the filtered tree list box.
*/
private _createFilteredTreeListBox(
tree: DropdownMenusViewsFilteredTreeNode
): Array<ListItemGroupView | ListItemView> {
const { locale, config } = this;
const { highlightRegex, limitFoundItemsCount } = config;

const groupedFlatEntries = groupDropdownTreeByFirstFoundParent( tree );

// Map each flat child node to a ListItemView
const mapFlatChildNodeToView = ( entry: DropdownMenusViewsFilteredFlatItem ): ListItemView => {
const listItemView = new ListItemView( locale );
const labelView = new ButtonLabelWithHighlightView();
const button = new ButtonView( locale, labelView );

button.set( {
label: entry.search.raw,
withText: true,
role: 'option'
} );

listItemView.children.add( button );
labelView.highlightText( highlightRegex );

button.delegate( 'execute' ).to( entry.item );
button.bind( 'isEnabled' ).to( entry.item, 'isEnabled' );

return listItemView;
};

// The total number of items rendered in the dropdown menu list.
let totalRenderedItems = 0;

// Create the filtered tree list box
return groupedFlatEntries.flatMap<ListItemGroupView | ListItemView>( ( { parent, children } ) => {
const listItems = children
.slice( 0, limitFoundItemsCount - totalRenderedItems )
.map( mapFlatChildNodeToView );

if ( !listItems.length ) {
return [];
}

totalRenderedItems += listItems.length;

if ( parent.type === 'Root' ) {
return listItems;
}

const labelView = new LabelWithHighlightView();
const groupView = new ListItemGroupView( locale, labelView );

groupView.label = parent.search.raw;
groupView.items.addMany( listItems );

labelView.highlightText( highlightRegex );

return [ groupView ];
} );
}
}

/**
* Configuration options for rendering the FoundItemsView in the DropdownMenuList.
*/
type FoundItemsViewRenderConfig = {

/**
* A regular expression used to highlight matching items in the view. If set to `null`, highlighting will be disabled.
*/
highlightRegex: RegExp | null;

/**
* The maximum number of found items to display in the view.
*/
limitFoundItemsCount: number;
};
Loading

0 comments on commit 1a38009

Please sign in to comment.