mirror of
https://github.com/codex-team/editor.js
synced 2026-03-17 16:10:07 +01:00
search input in popover
This commit is contained in:
parent
06a5766e6c
commit
a020ca5fd5
8 changed files with 128 additions and 30 deletions
3
src/assets/search.svg
Normal file
3
src/assets/search.svg
Normal 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 |
|
|
@ -13,7 +13,9 @@
|
|||
},
|
||||
"toolbar": {
|
||||
"toolbox": {
|
||||
"Add": ""
|
||||
"Add": "",
|
||||
"Filter": "",
|
||||
"Noting found": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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, () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue