search input in popover

This commit is contained in:
Peter Savchenko 2022-03-02 14:54:17 +03:00
commit a020ca5fd5
No known key found for this signature in database
GPG key ID: E68306B1AB0F727C
8 changed files with 128 additions and 30 deletions

3
src/assets/search.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M12.711 2.18a7.448 7.448 0 0 1 .79 9.603l2.143 2.144a1.214 1.214 0 1 1-1.717 1.717L11.783 13.5a7.446 7.446 0 1 1 .928-11.32ZM11.39 3.61a5.5 5.5 0 1 0-7.778 7.78 5.5 5.5 0 0 0 7.778-7.78Z" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -13,7 +13,9 @@
},
"toolbar": {
"toolbox": {
"Add": ""
"Add": "",
"Filter": "",
"Noting found": ""
}
}
},

View file

@ -431,6 +431,10 @@ export default class Toolbar extends Module<ToolbarNodes> {
this.toolboxInstance = new Toolbox({
api: this.Editor.API.methods,
tools: this.Editor.Tools.blockTools,
i18nLabels: {
filter: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Filter'),
nothingFound: I18n.ui(I18nInternalNS.ui.toolbar.toolbox, 'Noting found'),
},
});
this.toolboxInstance.on(ToolboxEvent.Opened, () => {

View file

@ -9,12 +9,12 @@ import Popover from '../utils/popover';
/**
* @todo check small tools number there should not be a scroll
* @todo add search in popover
* @todo hide toolbar after some toolbox item clicked (and the new block inserted)
* @todo do not show Block Tunes Toggler near only-one block
* @todo Plus Button should be appeared near all blocks (even non-empty)
* @todo the first Tab on the Block focus Plus Button, the second focus Block Tunes Toggler, the third focus next Block
* @todo use i18n for search labels
* @todo clear filter on every toolbox opening
* @todo arrows inside the search field
*
*/
/**
@ -37,6 +37,8 @@ export enum ToolboxEvent {
BlockAdded = 'toolbox-block-added',
}
type toolboxTextLabelsKeys = 'filter'|'nothingFound';
/**
* Toolbox
* This UI element contains list of Block Tools available to be inserted
@ -76,6 +78,11 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
*/
private tools: ToolsCollection<BlockTool>;
/**
* Text labels used in the Toolbox. Should be passed from the i18n module
*/
private i18nLabels: Record<toolboxTextLabelsKeys, string>;
/**
* Current module HTML Elements
*/
@ -108,11 +115,12 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
* @param options.api - Editor API methods
* @param options.tools - Tools available to check whether some of them should be displayed at the Toolbox or not
*/
constructor({ api, tools }) {
constructor({ api, tools, i18nLabels }: {api: API; tools: ToolsCollection<BlockTool>; i18nLabels: Record<toolboxTextLabelsKeys, string>}) {
super();
this.api = api;
this.tools = tools;
this.i18nLabels = i18nLabels;
}
/**
@ -122,10 +130,8 @@ export default class Toolbox extends EventsDispatcher<ToolboxEvent> {
this.popover = new Popover({
className: Toolbox.CSS.toolbox,
searchable: true,
// searchParams: {
// inputPlaceholder: 'Filter',
// noFoundMessage: 'Nothing found'
// },
filterLabel: this.i18nLabels.filter,
nothingFoundLabel: this.i18nLabels.nothingFound,
items: this.toolsToBeDisplayed.map(tool => {
return {
icon: tool.toolbox.icon,

View file

@ -45,9 +45,11 @@ export default class Popover {
private nodes: {
wrapper: HTMLElement;
items: HTMLElement;
nothingFound: HTMLElement;
} = {
wrapper: null,
items: null,
nothingFound: null,
}
/**
@ -70,6 +72,8 @@ export default class Popover {
*/
private searchable: boolean;
private search: SearchInput;
private filterLabel: string;
private nothingFoundLabel: string;
/**
* Style classes
@ -85,6 +89,7 @@ export default class Popover {
itemIcon: string;
itemSecondaryLabel: string;
noFoundMessage: string;
noFoundMessageShown: string;
} {
return {
popover: 'ce-popover',
@ -97,6 +102,7 @@ export default class Popover {
itemIcon: 'ce-popover__item-icon',
itemSecondaryLabel: 'ce-popover__item-secondary-label',
noFoundMessage: 'ce-popover__no-found',
noFoundMessageShown: 'ce-popover__no-found--shown',
};
}
@ -106,13 +112,24 @@ export default class Popover {
* @param options - config
* @param options.items - config for items to be displayed
* @param options.className - additional class name to be added to the popover wrapper
* @param options.filterLabel - label for the search Field
* @param options.nothingFoundLabel - label of the 'nothing found' message
*/
constructor({ items, className, searchable }: {items: PopoverItem[]; className?: string, searchable?: boolean}) {
constructor({ items, className, searchable, filterLabel, nothingFoundLabel }: {
items: PopoverItem[];
className?: string;
searchable?: boolean;
filterLabel: string;
nothingFoundLabel: string;
}) {
this.items = items;
this.className = className || '';
this.searchable = searchable;
this.listeners = new Listeners();
this.filterLabel = filterLabel;
this.nothingFoundLabel = nothingFoundLabel;
this.render();
this.enableFlipper();
}
@ -177,6 +194,11 @@ export default class Popover {
});
this.nodes.wrapper.appendChild(this.nodes.items);
this.nodes.nothingFound = Dom.make('div', [Popover.CSS.noFoundMessage], {
textContent: this.nothingFoundLabel,
});
this.nodes.wrapper.appendChild(this.nodes.nothingFound);
this.listeners.on(this.nodes.wrapper, 'click', (event: KeyboardEvent|MouseEvent) => {
const clickedItem = (event.target as HTMLElement).closest(`.${Popover.CSS.item}`) as HTMLElement;
@ -195,6 +217,7 @@ export default class Popover {
private addSearch(holder: HTMLElement): void {
this.search = new SearchInput({
items: this.items,
placeholder: this.filterLabel,
onSearch: (filteredItems): void => {
const itemsVisible = [];
@ -209,10 +232,7 @@ export default class Popover {
}
});
if (itemsVisible.length === 0) {
const noFoundMessage = Dom.make('div', Popover.CSS.noFoundMessage)
}
this.nodes.nothingFound.classList.toggle(Popover.CSS.noFoundMessageShown, itemsVisible.length === 0);
/**
* Update flipper items with only visible
@ -223,7 +243,7 @@ export default class Popover {
},
});
const searchField = this.search.getInput();
const searchField = this.search.getElement();
holder.appendChild(searchField);
}

View file

@ -1,5 +1,6 @@
import Dom from '../dom';
import Listeners from './listeners';
import $ from "../dom";
/**
* Item that could be searched
@ -12,6 +13,8 @@ interface SearchableItem {
* Provides search input element and search logic
*/
export default class SearchInput {
private wrapper: HTMLElement;
private input: HTMLInputElement;
private listeners: Listeners;
private items: SearchableItem[];
@ -23,29 +26,37 @@ export default class SearchInput {
*/
private static get CSS(): {
input: string;
wrapper: string;
} {
return {
input: 'cdx-filter-input',
wrapper: 'cdx-search-field',
input: 'cdx-search-field__input',
};
}
/**
* @param items - searchable items list
* @param onSearch - search callback
* @param options - available config
* @param options.items - searchable items list
* @param options.onSearch - search callback
* @param options.placeholder - input placeholder
*/
constructor({ items, onSearch }: { items: SearchableItem[], onSearch: (items: SearchableItem[]) => void }) {
constructor({ items, onSearch, placeholder }: {
items: SearchableItem[];
onSearch: (items: SearchableItem[]) => void;
placeholder: string;
}) {
this.listeners = new Listeners();
this.items = items;
this.onSearch = onSearch;
this.render();
this.render(placeholder);
}
/**
* Returns search field element
*/
public getInput(): HTMLElement {
return this.input;
public getElement(): HTMLElement {
return this.wrapper;
}
/**
@ -64,12 +75,20 @@ export default class SearchInput {
/**
* Creates the search field
*
* @param placeholder - input placeholder
*/
private render(): void {
private render(placeholder: string): void {
this.wrapper = Dom.make('div', SearchInput.CSS.wrapper);
const icon = $.svg('search', 16, 16);
this.input = Dom.make('input', SearchInput.CSS.input, {
type: 'search',
placeholder,
}) as HTMLInputElement;
this.wrapper.appendChild(icon);
this.wrapper.appendChild(this.input);
this.listeners.on(this.input, 'input', () => {
this.searchQuery = this.input.value;

View file

@ -1,8 +1,36 @@
.cdx-filter-input {
img, video, audio, canvas, input,
select, button, progress { max-width: 100%; }
.cdx-search-field {
background: rgba(232,232,235,0.49);
border: 1px solid rgba(226,226,229,0.20);
border-radius: 5px;
padding: 7px;
font-size: 14px;
outline: none;
padding: 4px 7px;
display: flex;
align-items: center;
.icon {
width: 14px;
height: 14px;
margin-right: 17px;
margin-left: 2px;
color: var(--grayText);
}
&__input {
font-size: 14px;
outline: none;
font-weight: 500;
font-family: inherit;
border: 0;
background: transparent;
margin: 0;
padding: 0;
line-height: 22px;
&::placeholder {
color: var(--grayText);
font-weight: 500;
}
}
}

View file

@ -6,12 +6,11 @@
display: flex;
flex-direction: column;
padding: 4px;
min-width: 180px;
min-width: 200px;
max-height: 284px;
overflow: hidden;
box-sizing: border-box;
flex-shrink: 0;
overscroll-behavior: contain;
@apply --overlay-pane;
@ -37,6 +36,7 @@
&__items {
overflow-y: auto;
margin-top: 5px;
overscroll-behavior: contain;
}
&__item {
@ -73,4 +73,20 @@
opacity: 0.6;
}
}
&__no-found {
@apply --popover-button;
color: var(--grayText);
display: none;
cursor: default;
&--shown {
display: block;
}
&:hover {
background-color: transparent;
}
}
}